This commit is contained in:
Casey 2026-02-09 07:44:50 -06:00
parent c81beb5290
commit 38514fef47
24 changed files with 2087 additions and 322 deletions

View File

@ -3,7 +3,7 @@ from frappe.utils.pdf import get_pdf
from custom_ui.api.db.general import get_doc_history
from custom_ui.db_utils import DbUtils, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer
from custom_ui.services import DbService, ClientService, AddressService, ContactService, EstimateService, ItemService
from custom_ui.services import ItemService, DbService, ClientService, AddressService, ContactService, EstimateService, ItemService
from frappe.email.doctype.email_template.email_template import get_email_template
# ===============================================================================
@ -145,6 +145,7 @@ def get_estimate(estimate_name):
est_dict["address_details"] = address_doc
est_dict["history"] = get_doc_history("Quotation", estimate_name)
est_dict["items"] = [ItemService.get_full_dict(item.item_code) for item in estimate.items]
return build_success_response(est_dict)
except Exception as e:
@ -532,7 +533,9 @@ def upsert_estimate(data):
# AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name)
# ClientService.append_link(data.get("customer"), "quotations", "quotation", new_estimate.name)
print("DEBUG: New estimate created with name:", new_estimate.name)
return build_success_response(new_estimate.as_dict())
dict = new_estimate.as_dict()
dict["items"] = [ItemService.get_full_dict(item.item_code) for item in new_estimate.items]
return build_success_response(dict)
except Exception as e:
print(f"DEBUG: Error in upsert_estimate: {str(e)}")
return build_error_response(str(e), 500)

View File

@ -1,6 +1,6 @@
import frappe, json
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response, process_sorting
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService, ProjectService
from frappe.utils import getdate
# ===============================================================================
@ -119,13 +119,7 @@ def get_job(job_id=""):
"""Get particular Job from DB"""
print("DEBUG: Loading Job from database:", job_id)
try:
project = frappe.get_doc("Project", job_id)
address_doc = AddressService.get_or_throw(project.job_address)
project = project.as_dict()
project["job_address"] = address_doc
project["client"] = ClientService.get_client_or_throw(project.customer)
task_names = frappe.get_all("Task", filters={"project": job_id})
project["tasks"] = [frappe.get_doc("Task", task_name).as_dict() for task_name in task_names]
project = ProjectService.get_full_project_details(job_id)
return build_success_response(project)
except Exception as e:
return build_error_response(str(e), 500)
@ -187,6 +181,8 @@ def get_job_task_list(job_id=""):
def get_jobs_table_data(filters={}, sortings=[], page=1, page_size=10):
"""Get paginated job table data with filtering and sorting support."""
print("DEBUG: Raw job options received:", filters, sortings, page, page_size)
filters = json.loads(filters) if isinstance(filters, str) else filters
sortings = json.loads(sortings) if isinstance(sortings, str) else sortings
processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size)

View File

@ -2,6 +2,15 @@ import frappe, json
from custom_ui.db_utils import build_success_response, build_error_response
from custom_ui.services import ServiceAppointmentService
@frappe.whitelist()
def get_service_appointment(service_appointment_name):
"""Get a single Service Appointment by name."""
try:
service_appointment = ServiceAppointmentService.get_full_dict(service_appointment_name)
return build_success_response(service_appointment)
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_service_appointments(companies, filters={}):
"""Get Service Appointments for given companies."""
@ -65,5 +74,19 @@ def update_service_appointment_scheduled_dates(service_appointment_name: str, st
end_time
)
return build_success_response(updated_service_appointment.as_dict())
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def update_service_appointment_status(service_appointment_name: str, new_status: str):
"""Update status for a Service Appointment."""
print(f"DEBUG: Updating status for Service Appointment {service_appointment_name} to new status: {new_status}")
try:
updated_service_appointment = ServiceAppointmentService.update_status(
service_appointment_name,
new_status
)
return build_success_response(updated_service_appointment.as_dict())
except Exception as e:
return build_error_response(str(e), 500)

View File

@ -26,6 +26,26 @@ def half_down_stripe_payment(sales_order):
)
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = stripe_session.url
@frappe.whitelist(allow_guest=True)
def invoice_stripe_payment(sales_invoice):
"""Public endpoint for initiating a full payment for a sales invoice."""
if not DbService.exists("Sales Invoice", sales_invoice):
frappe.throw("Sales Invoice does not exist.")
si = DbService.get_or_throw("Sales Invoice", sales_invoice)
if si.docstatus != 1:
frappe.throw("Sales Invoice must be submitted to proceed with payment.")
if si.outstanding_amount <= 0:
frappe.throw("This invoice has already been paid.")
stripe_session = StripeService.create_checkout_session(
company=si.company,
amount=si.outstanding_amount,
service=si.project_template,
order_num=si.name,
for_advance_payment=False
)
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = stripe_session.url
@frappe.whitelist(allow_guest=True)
def stripe_webhook():

View File

@ -11,7 +11,7 @@ def map_field_name(frontend_field):
"job_status": "custom_job_status",
"installation_address": "custom_installation_address",
"warranty_id": "name",
"customer": "customer_name",
"customer": "customer",
"fromCompany": "from_company",
"warranty_status": "warranty_amc_status"
}

View File

@ -50,7 +50,7 @@ def after_insert(doc, method):
)
if task_names:
doc.save(ignore_permissions=True)
TaskService.calculate_and_set_due_dates(task_names, "Created", current_triggering_dict=doc.as_dict())
TaskService.fire_task_triggers(task_names, "Created", current_triggering_dict=doc.as_dict())
@ -74,7 +74,7 @@ def before_save(doc, method):
doc.is_scheduled = 0
event = TaskService.determine_event(doc)
if event:
TaskService.calculate_and_set_due_dates(
TaskService.fire_task_triggers(
[task.task for task in doc.tasks],
event,
current_triggering_dict=doc.as_dict()

View File

@ -9,8 +9,14 @@ def on_submit(doc, method):
print("DEBUG: Preparing to send invoice email for", doc.name)
EmailService.send_invoice_email(doc.name)
print("DEBUG: Invoice email sent successfully for", doc.name)
frappe.set_value("Project", doc.project, "invoice_status", "Invoice Sent")
except Exception as e:
print(f"ERROR: Failed to send invoice email: {str(e)}")
# Don't raise the exception - we don't want to block the invoice submission
frappe.log_error(f"Failed to send invoice email for {doc.name}: {str(e)}", "Invoice Email Error")
def after_insert(doc, method):
print("DEBUG: After Insert Triggered for Sales Invoice:", doc.name)
# Additional logic can be added here if needed after invoice creation
frappe.set_value("Project", doc.project, "invoice_status", "Invoice Created")

View File

@ -13,7 +13,7 @@ def on_update(doc, method):
def after_insert(doc, method):
print("DEBUG: After Insert Triggered for Service Appointment")
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
TaskService.calculate_and_set_due_dates(task_names=task_names, event="Created", current_triggering_dict=doc.as_dict())
TaskService.fire_task_triggers(task_names=task_names, event="Created", current_triggering_dict=doc.as_dict())
def before_save(doc, method):
print("DEBUG: Before Save Triggered for Service Appointment")
@ -28,4 +28,6 @@ def before_save(doc, method):
event = TaskService.determine_event(doc)
if event:
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
TaskService.calculate_and_set_due_dates(task_names=task_names, event=event, current_triggering_dict=doc.as_dict())
TaskService.fire_task_triggers(task_names=task_names, event=event, current_triggering_dict=doc.as_dict())
if doc.status == "Completed" and frappe.get_value("Service Address 2", doc.name, "status") != "Completed":
frappe.set_value("Project", doc.project, "invoice_status", "Ready to Invoice")

View File

@ -23,12 +23,33 @@ def after_insert(doc, method):
doc.customer, "tasks", {"task": doc.name, "project_template": doc.project_template }
)
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
TaskService.calculate_and_set_due_dates(task_names, "Created", current_triggering_dict=doc.as_dict())
TaskService.fire_task_triggers(task_names, "Created", current_triggering_dict=doc.as_dict())
def before_save(doc, method):
print("DEBUG: Before Save Triggered for Task:", doc.name)
task_type_weight = frappe.get_value("Task Type", doc.type, "weight") or 0
if doc.task_weight != task_type_weight:
print(f"DEBUG: Updating Task weight from {doc.task_weight} to {task_type_weight}")
doc.task_weight = task_type_weight
event = TaskService.determine_event(doc)
if event:
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
TaskService.calculate_and_set_due_dates(task_names, event, current_triggering_dict=doc.as_dict())
TaskService.fire_task_triggers(task_names, event, current_triggering_dict=doc.as_dict())
def after_save(doc, method):
print("DEBUG: After Save Triggered for Task:", doc.name)
if doc.project and doc.status == "Completed":
print("DEBUG: Task is completed, checking if project has calculated 100% Progress.")
project_doc = frappe.get_doc("Project", doc.project)
if project_doc.percent_complete == 100:
project_update_required = False
if project_doc.status == "Completed" and project_doc.customCompletionDate is None:
print("DEBUG: Project is marked as Completed but customCompletionDate is not set, updating customCompletionDate.")
project_doc.customCompletionDate = frappe.utils.nowdate()
project_update_required = True
if project_doc.invoice_status == "Not Ready":
project_doc.invoice_status = "Ready to Invoice"
project_update_required = True
if project_update_required:
project_doc.save(ignore_permissions=True)
print("DEBUG: Updated Project document after Task completion")

View File

@ -246,13 +246,13 @@ fixtures = [
# Scheduled Tasks
# ---------------
# scheduler_events = {
scheduler_events = {
# "all": [
# "custom_ui.tasks.all"
# ],
# "daily": [
# "custom_ui.tasks.daily"
# ],
"daily": [
"custom_ui.scheduled_tasks.daily"
],
# "hourly": [
# "custom_ui.tasks.hourly"
# ],
@ -262,7 +262,7 @@ fixtures = [
# "monthly": [
# "custom_ui.tasks.monthly"
# ],
# }
}
# Testing
# -------

View File

@ -0,0 +1,8 @@
import frappe
from custom_ui.services import TaskService
def daily_task():
"""Scheduled task to run daily."""
print("#################### Running Daily Task ####################")
print("DEBUG: Checking Task due dates")
TaskService.find_and_update_overdue_tasks()

View File

@ -22,6 +22,7 @@ class EstimateService:
print("DEBUG: Quotation document not found.")
return None
@staticmethod
def get_or_throw(estimate_name: str) -> frappe._dict:
"""Retrieve a Quotation document by name or throw an error if not found."""

View File

@ -1,4 +1,5 @@
import frappe
from custom_ui.services import TaskService, AddressService
class ProjectService:
@ -9,4 +10,20 @@ class ProjectService:
item_groups_str = frappe.db.get_value("Project Template", project_template, "item_groups") or ""
item_groups = [item_group.strip() for item_group in item_groups_str.split(",") if item_group.strip()]
print(f"DEBUG: Retrieved item groups: {item_groups}")
return item_groups
return item_groups
@staticmethod
def get_full_project_details(project_name: str) -> dict:
"""Retrieve comprehensive details for a given project, including linked sales order and invoice information."""
print(f"DEBUG: Getting full project details for project: {project_name}")
project = frappe.get_doc("Project", project_name).as_dict()
project["tasks"] = [frappe.get_doc("Task", task["task"]).as_dict() for task in project["tasks"] if task.get("task")]
for task in project["tasks"]:
task["type"] = frappe.get_doc("Task Type", task["type"]).as_dict() if task.get("type") else None
project["job_address"] = frappe.get_doc("Address", project["job_address"]).as_dict() if project["job_address"] else None
project["service_appointment"] = frappe.get_doc("Service Address 2", project["service_appointment"]).as_dict() if project["service_appointment"] else None
project["client"] = frappe.get_doc("Customer", project["customer"]).as_dict() if project["customer"] else None
project["sales_order"] = frappe.get_doc("Sales Order", project["sales_order"]).as_dict() if project["sales_order"] else None
project["billing_address"] = frappe.get_doc("Address", project["client"]["custom_billing_address"]).as_dict() if project["client"] and project["client"].get("custom_billing_address") else None
project["invoice"] = frappe.get_doc("Sales Invoice", {"project": project["name"]}).as_dict() if frappe.db.exists("Sales Invoice", {"project": project["name"]}) else None
return project

View File

@ -15,6 +15,11 @@ class SalesOrderService:
sales_invoice.remarks = f"Auto-generated from Sales Order {sales_order_doc.name}"
sales_invoice.job_address = sales_order_doc.custom_job_address
sales_invoice.project_template = sales_order_doc.custom_project_template
sales_invoice.set_advances()
sales_invoice.set_missing_values()
sales_invoice.calculate_taxes_and_totals()
sales_invoice.insert()
sales_invoice.submit()
return sales_invoice.name

View File

@ -51,4 +51,14 @@ class ServiceAppointmentService:
setattr(service_appointment, field, value)
service_appointment.save()
print(f"DEBUG: Updated fields for Service Appointment {service_appointment_name}")
return service_appointment
@staticmethod
def update_status(service_appointment_name: str, new_status: str):
"""Update the status of a Service Appointment."""
print(f"DEBUG: Updating status for Service Appointment {service_appointment_name} to {new_status}")
service_appointment = DbService.get_or_throw("Service Address 2", service_appointment_name)
service_appointment.status = new_status
service_appointment.save()
print(f"DEBUG: Updated status for Service Appointment {service_appointment_name} to {new_status}")
return service_appointment

View File

@ -6,10 +6,10 @@ class TaskService:
@staticmethod
def calculate_and_set_due_dates(task_names: list[str], event: str, current_triggering_dict=None):
def fire_task_triggers(task_names: list[str], event: str, current_triggering_dict=None):
"""Calculate the due date for a list of tasks based on their expected end dates."""
for task_name in task_names:
TaskService.check_and_update_task_due_date(task_name, event, current_triggering_dict)
TaskService.fire_task_trigger(task_name, event, current_triggering_dict)
@staticmethod
@ -20,49 +20,60 @@ class TaskService:
return tasks
@staticmethod
def check_and_update_task_due_date(task_name: str, event: str, current_triggering_dict=None):
def fire_task_trigger(task_name: str, event: str, current_triggering_dict=None):
"""Determine the triggering configuration for a given task."""
task_type_doc = TaskService.get_task_type_doc(task_name)
if task_type_doc.no_due_date:
print(f"DEBUG: Task {task_name} is marked as no due date, skipping calculation.")
return
if task_type_doc.triggering_doctype != current_triggering_dict.get("doctype") and current_triggering_dict:
schedule_trigger = task_type_doc.triggering_doctype == current_triggering_dict.get("doctype") if current_triggering_dict else False
completion_trigger = task_type_doc.custom_completion_trigger_doctype == current_triggering_dict.get("doctype") if current_triggering_dict else False
match_schedule_event = task_type_doc.trigger == event
match_completion_event = task_type_doc.custom_completion_trigger == event
if not schedule_trigger and not completion_trigger:
print(f"DEBUG: Task {task_name} triggering doctype {task_type_doc.triggering_doctype} does not match triggering doctype {current_triggering_dict.get('doctype')}, skipping calculation.")
return
if task_type_doc.trigger != event:
if not match_schedule_event and not match_completion_event:
print(f"DEBUG: Task {task_name} trigger {task_type_doc.trigger} does not match event {event}, skipping calculation.")
return
if task_type_doc.logic_key:
print(f"DEBUG: Task {task_name} has a logic key set, skipping calculations and running logic.")
safe_eval(task_type_doc.logic_key, {"task_name": task_name, "task_type_doc": task_type_doc})
if task_type_doc.no_due_date:
print(f"DEBUG: Task {task_name} is marked as no due date, skipping calculation.")
return
calculate_from = task_type_doc.calculate_from
trigger = task_type_doc.trigger
print(f"DEBUG: Calculating triggering data for Task {task_name} from {calculate_from} on trigger {trigger}")
triggering_doc_dict = current_triggering_dict if current_triggering_dict else TaskService.get_triggering_doc_dict(task_name=task_name, task_type_doc=task_type_doc)
calculated_due_date, calculated_start_date = TaskService.calculate_dates(
task_name=task_name,
triggering_doc_dict=triggering_doc_dict,
task_type_doc=task_type_doc
)
update_required = TaskService.determine_update_required(
task_name=task_name,
calculated_due_date=calculated_due_date,
calculated_start_date=calculated_start_date
)
if update_required:
TaskService.update_task_dates(
if schedule_trigger:
triggering_doc_dict = current_triggering_dict if current_triggering_dict else TaskService.get_triggering_doc_dict(task_name=task_name, doctype=task_type_doc.triggering_doctype, task_type_calculate_from=task_type_doc.task_type_calculate_from)
calculate_from = task_type_doc.calculate_from
trigger = task_type_doc.trigger
print(f"DEBUG: Calculating triggering data for Task {task_name} from {calculate_from} on trigger {trigger}")
calculated_due_date, calculated_start_date = TaskService.calculate_dates(
task_name=task_name,
triggering_doc_dict=triggering_doc_dict,
task_type_doc=task_type_doc
)
update_required = TaskService.determine_due_date_update_required(
task_name=task_name,
calculated_due_date=calculated_due_date,
calculated_start_date=calculated_start_date
)
if update_required:
TaskService.update_task(
task_name=task_name,
calculated_due_date=calculated_due_date,
calculated_start_date=calculated_start_date
)
if completion_trigger:
triggering_doc_dict = current_triggering_dict if current_triggering_dict else TaskService.get_triggering_doc_dict(task_name=task_name, doctype=task_type_doc.custom_completion_trigger_doctype)
print(f"DEBUG: Running completion trigger logic for Task {task_name}")
update_required = TaskService.determine_completion_update_required(
task_name=task_name,
task_type_doc=task_type_doc,
)
if update_required:
TaskService.update_task(
task_name=task_name,
status="Completed"
)
print(f"DEBUG: Marked Task {task_name} as Completed due to completion trigger.")
@staticmethod
def get_task_type_doc(task_name: str):
@ -94,7 +105,7 @@ class TaskService:
return calculated_due_date, calculated_start_date
@staticmethod
def determine_update_required(task_name: str, calculated_due_date: date | None, calculated_start_date: date | None) -> bool:
def determine_due_date_update_required(task_name: str, calculated_due_date: date | None, calculated_start_date: date | None) -> bool:
current_due_date = frappe.get_value("Task", task_name, "exp_end_date")
current_start_date = frappe.get_value("Task", task_name, "exp_start_date")
if current_due_date != calculated_due_date or current_start_date != calculated_start_date:
@ -104,32 +115,60 @@ class TaskService:
print(f"DEBUG: No update required for Task {task_name}. Dates are up to date.")
return False
@staticmethod
def determine_completion_update_required(task_name: str, task_type_doc) -> bool:
current_status = frappe.get_value("Task", task_name, "status")
determination = False
if current_status == "Completed":
print(f"DEBUG: Task {task_name} is already marked as Completed, no update required.")
return False
else:
triggering_doc_dict = TaskService.get_triggering_doc_dict(task_name=task_name, doctype=task_type_doc.custom_completion_trigger_doctype)
check_field = TaskService.map_completion_check_field(task_type_doc.custom_completion_trigger)
check_value = triggering_doc_dict.get(check_field)
trigger = task_type_doc.custom_completion_trigger
if trigger == "Completed" and check_value == "Completed" and current_status != "Completed":
determination = True
elif trigger == "Percentage Reached" and check_value >= task_type_doc.custom_target_percent and current_status != "Completed":
determination = True
elif trigger in ["Scheduled", "Started", "Created"] and check_value and current_status != "Completed":
determination = True
print(f"DEBUG: Completion trigger '{trigger}' met for Task {task_name}, check field {check_field} has value {check_value}.")
return determination
@staticmethod
def get_triggering_doc_dict(task_name: str, task_type_doc) -> dict | None:
def get_triggering_doc_dict(task_name: str, doctype, task_type_calculate_from = None) -> dict | None:
project_name = frappe.get_value("Task", task_name, "project")
print(f"DEBUG: Project name: {project_name}")
dict = None
if task_type_doc.calculate_from == "Project":
if doctype == "Project":
dict = frappe.get_doc("Project", project_name).as_dict()
if task_type_doc.calculate_from == "Service Address 2":
if doctype == "Service Address 2":
service_name = frappe.get_value("Project", project_name, "service_appointment")
dict = frappe.get_doc("Service Address 2", service_name).as_dict()
if task_type_doc.calculate_from == "Task":
if doctype == "Task":
project_doc = frappe.get_doc("Project", project_name)
for task in project_doc.tasks:
if task.task_type == task_type_doc.task_type_calculate_from:
if task.task_type == task_type_calculate_from:
dict = frappe.get_doc("Task", task.task).as_dict()
print(f"DEBUG: Triggering doc dict for Task {task_name}: {dict}")
return dict
@staticmethod
def update_task_dates(task_name: str, calculated_due_date: date | None, calculated_start_date: date | None):
def update_task(task_name: str, calculated_due_date: date | None = None, calculated_start_date: date | None = None, status: str | None = None):
task_doc = frappe.get_doc("Task", task_name)
task_doc.exp_end_date = calculated_due_date
task_doc.exp_start_date = calculated_start_date
if calculated_due_date is not None:
task_doc.exp_end_date = calculated_due_date
if calculated_start_date is not None:
task_doc.exp_start_date = calculated_start_date
if status is not None:
task_doc.status = status
if status == "Completed":
task_doc.actual_end_date = datetime.now()
task_doc.save(ignore_permissions=True)
print(f"DEBUG: Updated Task {task_name} with new dates - Start: {calculated_start_date}, End: {calculated_due_date}")
print(f"DEBUG: Updated Task {task_name} with new dates - Start: {calculated_start_date}, End: {calculated_due_date}, Status: {status}")
@staticmethod
def map_base_date_to_field(base_date: str, triggering_doctype: str) -> str:
@ -150,6 +189,17 @@ class TaskService:
return task_date_field_map.get(base_date, "exp_end_date")
return base_date_field_map.get(base_date, "expected_end_date")
@staticmethod
def map_completion_check_field(completion_trigger: str) -> str:
completion_check_field_map = {
"Completed": "status",
"Scheduled": "expected_end_date",
"Created": "creation",
"Started": "actual_start_date",
"Percentage Reached": "progress"
}
return completion_check_field_map.get(completion_trigger, "status")
@staticmethod
def determine_event(triggering_doc) -> str | None:
print("DEBUG: Current Document:", triggering_doc.as_dict())
@ -168,4 +218,16 @@ class TaskService:
return "Completed"
else:
return None
@staticmethod
def find_and_update_overdue_tasks():
today = date.today()
overdue_tasks = frappe.get_all("Task", filters={"exp_end_date": ("<", today), "status": ["not in", ["Completed", "Template", "Cancelled", "Overdue"]]}, pluck="name")
print(f"DEBUG: Found {len(overdue_tasks)} overdue tasks.")
for task_name in overdue_tasks:
task_doc = frappe.get_doc("Task", task_name)
task_doc.status = "Overdue"
task_doc.save(ignore_permissions=True)
print(f"DEBUG: Updated Task {task_name} to Overdue status.")
frappe.db.commit()

View File

@ -70,8 +70,10 @@ const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_em
const FRAPPE_GET_WEEK_HOLIDAYS_METHOD = "custom_ui.api.db.general.get_week_holidays";
const FRAPPE_GET_DOC_LIST_METHOD = "custom_ui.api.db.general.get_doc_list";
// Service Appointment methods
const FRAPPE_GET_SERVICE_APPOINTMENT_METHOD = "custom_ui.api.db.service_appointments.get_service_appointment";
const FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD = "custom_ui.api.db.service_appointments.get_service_appointments";
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_SCHEDULED_DATES_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_scheduled_dates";
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_STATUS_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_status";
class Api {
// ============================================================================
// CORE REQUEST METHOPD
@ -457,6 +459,10 @@ class Api {
// SERVICE APPOINTMENT METHODS
// ============================================================================
static async getServiceAppointment(serviceAppointmentName) {
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENT_METHOD, { serviceAppointmentName });
}
static async getServiceAppointments(companies = [], filters = {}) {
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD, { companies, filters });
}
@ -472,6 +478,10 @@ class Api {
})
}
static async setServiceAppointmentStatus(serviceAppointmentName, newStatus) {
return await this.request(FRAPPE_UPDATE_SERVICE_APPOINTMENT_STATUS_METHOD, { serviceAppointmentName, newStatus });
}
// ============================================================================
// TASK METHODS
// ============================================================================

View File

@ -1,6 +1,6 @@
<template>
<div class="calendar-navigation">
<Tabs value="0" v-if="companyStore.currentCompany == 'Sprinklers Northwest'">
<Tabs :value="defaultTab" v-if="companyStore.currentCompany == 'Sprinklers Northwest'">
<TabList>
<Tab value="0">Bids</Tab>
<Tab value="1">Projects</Tab>
@ -26,7 +26,7 @@
</TabPanel>
</TabPanels>
</Tabs>
<Tabs v-else value="0">
<Tabs v-else :value="defaultTab">
<TabList>
<Tab value="0">Bids</Tab>
<Tab value="1">Projects</Tab>
@ -61,20 +61,36 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import Tab from 'primevue/tab';
import Tabs from 'primevue/tabs';
import TabList from 'primevue/tablist';
import TabPanel from 'primevue/tabpanel';
import TabPanels from 'primevue/tabpanels';
import ScheduleBid from '../calendar/bids/ScheduleBid.vue';
import JobsCalendar from '../calendar/jobs/JobsCalendar.vue';
import SNWProjectCalendar from './jobs/SNWProjectCalendar.vue';
import { useNotificationStore } from '../../stores/notifications-primevue';
import { useCompanyStore } from '../../stores/company';
import { useRoute } from 'vue-router';
const notifications = useNotificationStore();
const companyStore = useCompanyStore();
const route = useRoute();
const defaultTab = ref('0');
const mappedTabs = {
'bid': '0',
'projects': '1',
'service': '2',
'warranties': companyStore.currentCompany == 'Sprinklers Northwest' ? '6' : '3'
};
onMounted(() => {
// Check for route query parameter to set default tab
if (route.query.tab) {
defaultTab.value = mappedTabs[route.query.tab] || '0';
}
});
</script>
<style scoped>

View File

@ -330,10 +330,14 @@ import { ref, onMounted, computed, watch } from "vue";
import Api from "../../../api";
import { useNotificationStore } from "../../../stores/notifications-primevue";
import { useCompanyStore } from "../../../stores/company";
import { useRoute } from "vue-router";
import JobDetailsModal from "../../modals/JobDetailsModal.vue";
const notifications = useNotificationStore();
const companyStore = useCompanyStore();
const route = useRoute();
const serviceAptToFind = route.query.apt || null;
// Reactive data
const scheduledServices = ref([]);
@ -1302,6 +1306,40 @@ const stopResize = async () => {
originalEndDate.value = null;
};
const handleServiceAptToFind = async () => {
if (!serviceAptToFind) return;
try {
// Fetch the specific service appointment
const appointment = await Api.getServiceAppointment(serviceAptToFind);
if (appointment && appointment.expectedStartDate) {
// Navigate to the week containing this appointment
weekStartDate.value = getWeekStart(parseLocalDate(appointment.expectedStartDate));
// Wait for data to load
await fetchServiceAppointments();
// Find the appointment in the loaded data
const foundAppointment = scheduledServices.value.find(s => s.name === serviceAptToFind) ||
unscheduledServices.value.find(s => s.name === serviceAptToFind);
if (foundAppointment) {
// Open the details modal
selectedEvent.value = foundAppointment;
eventDialog.value = true;
} else {
notifications.addWarning("Service appointment found but not visible in current filters");
}
} else {
notifications.addWarning("Service appointment not scheduled yet");
}
} catch (error) {
console.error("Error finding service appointment:", error);
notifications.addError("Failed to find service appointment");
}
};
const fetchServiceAppointments = async (currentDate) => {
try {
// Calculate date range for the week
@ -1397,6 +1435,9 @@ onMounted(async () => {
// await fetchProjects();
await fetchServiceAppointments();
await fetchHolidays();
// Handle serviceAptToFind query parameter
await handleServiceAptToFind();
});
</script>

View File

@ -187,6 +187,17 @@
<v-icon left>mdi-open-in-new</v-icon>
View Job
</v-btn>
<v-btn
v-if="job.status !== 'Completed'"
color="success"
variant="flat"
@click="updateStatus('Completed')"
:loading="isUpdatingStatus"
:disabled="isUpdatingStatus"
>
<v-icon left>mdi-check-circle</v-icon>
Mark as Completed
</v-btn>
<v-spacer></v-spacer>
<v-btn variant="text" @click="handleClose">Close</v-btn>
</v-card-actions>
@ -196,6 +207,10 @@
<script setup>
import { ref, computed } from "vue";
import Api from "../../api";
import { useNotificationStore } from "../../stores/notifications-primevue";
const notifications = useNotificationStore();
// Props
const props = defineProps({
@ -214,7 +229,10 @@ const props = defineProps({
});
// Emits
const emit = defineEmits(["update:modelValue", "close"]);
const emit = defineEmits(["update:modelValue", "close", "statusUpdated"]);
// State
const isUpdatingStatus = ref(false);
// Computed
const showModal = computed({
@ -304,6 +322,34 @@ const getPriorityColor = (priority) => {
}
};
const updateStatus = async (newStatus) => {
if (!props.job?.name) return;
isUpdatingStatus.value = true;
try {
await Api.setServiceAppointmentStatus(props.job.name, newStatus);
notifications.addSuccess(`Service appointment marked as ${newStatus}`);
// Update local job status
if (props.job) {
props.job.status = newStatus;
}
// Emit event to parent to refresh data
emit("statusUpdated", { name: props.job.name, status: newStatus });
// Close modal after short delay
setTimeout(() => {
handleClose();
}, 1000);
} catch (error) {
console.error("Error updating service appointment status:", error);
notifications.addError("Failed to update service appointment status");
} finally {
isUpdatingStatus.value = false;
}
}
const viewJob = () => {
if (props.job?.name) {
window.location.href = `/job?name=${encodeURIComponent(props.job.name)}`;

View File

@ -1163,6 +1163,8 @@ onMounted(async () => {
qty: item.qty,
rate: item.rate,
standardRate: item.rate,
bom: item.bom || null,
uom: item.uom || item.stockUom || item.stock_uom || 'Nos',
discountAmount: discountAmount === 0 ? null : discountAmount,
discountPercentage: discountPercentage === 0 ? null : discountPercentage,
discountType: discountPercentage > 0 ? 'percentage' : 'currency'

File diff suppressed because it is too large Load Diff

View File

@ -136,9 +136,9 @@ const chartData = ref({
})
const columns = [
{ label: "Job ID", fieldName: "name", type: "text", sortable: true, filterable: true },
{ label: "Address", fieldName: "jobAddress", type: "text", sortable: true },
{ label: "Customer", fieldName: "customer", type: "text", sortable: true, filterable: true },
{ label: "Address", fieldName: "jobAddress", type: "text", sortable: true },
{ label: "Job ID", fieldName: "name", type: "text", sortable: true, filterable: true },
{ label: "Overall Status", fieldName: "status", type: "status", sortable: true },
{ label: "Invoice Status", fieldName: "invoiceStatus", type: "text", sortable: true },
{ label: "Progress", fieldName: "percentComplete", type: "text", sortable: true }
@ -275,7 +275,6 @@ const loadChartData = async () => {
// Load initial data
onMounted(async () => {
notifications.addWarning("Jobs page coming soon");
// Initialize pagination and filters
paginationStore.initializeTablePagination("jobs", { rows: 10 });
filtersStore.initializeTableFilters("jobs", columns);