all flow
This commit is contained in:
parent
c81beb5290
commit
38514fef47
24 changed files with 2087 additions and 322 deletions
|
|
@ -3,7 +3,7 @@ from frappe.utils.pdf import get_pdf
|
||||||
from custom_ui.api.db.general import get_doc_history
|
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.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.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
|
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["address_details"] = address_doc
|
||||||
|
|
||||||
est_dict["history"] = get_doc_history("Quotation", estimate_name)
|
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)
|
return build_success_response(est_dict)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -532,7 +533,9 @@ def upsert_estimate(data):
|
||||||
# AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name)
|
# AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name)
|
||||||
# ClientService.append_link(data.get("customer"), "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)
|
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:
|
except Exception as e:
|
||||||
print(f"DEBUG: Error in upsert_estimate: {str(e)}")
|
print(f"DEBUG: Error in upsert_estimate: {str(e)}")
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import frappe, json
|
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.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
|
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService, ProjectService
|
||||||
from frappe.utils import getdate
|
from frappe.utils import getdate
|
||||||
|
|
||||||
# ===============================================================================
|
# ===============================================================================
|
||||||
|
|
@ -119,13 +119,7 @@ def get_job(job_id=""):
|
||||||
"""Get particular Job from DB"""
|
"""Get particular Job from DB"""
|
||||||
print("DEBUG: Loading Job from database:", job_id)
|
print("DEBUG: Loading Job from database:", job_id)
|
||||||
try:
|
try:
|
||||||
project = frappe.get_doc("Project", job_id)
|
project = ProjectService.get_full_project_details(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]
|
|
||||||
return build_success_response(project)
|
return build_success_response(project)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
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):
|
def get_jobs_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||||
"""Get paginated job table data with filtering and sorting support."""
|
"""Get paginated job table data with filtering and sorting support."""
|
||||||
print("DEBUG: Raw job options received:", filters, sortings, page, page_size)
|
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)
|
processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@ import frappe, json
|
||||||
from custom_ui.db_utils import build_success_response, build_error_response
|
from custom_ui.db_utils import build_success_response, build_error_response
|
||||||
from custom_ui.services import ServiceAppointmentService
|
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()
|
@frappe.whitelist()
|
||||||
def get_service_appointments(companies, filters={}):
|
def get_service_appointments(companies, filters={}):
|
||||||
"""Get Service Appointments for given companies."""
|
"""Get Service Appointments for given companies."""
|
||||||
|
|
@ -65,5 +74,19 @@ def update_service_appointment_scheduled_dates(service_appointment_name: str, st
|
||||||
end_time
|
end_time
|
||||||
)
|
)
|
||||||
return build_success_response(updated_service_appointment.as_dict())
|
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:
|
except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
@ -26,6 +26,26 @@ def half_down_stripe_payment(sales_order):
|
||||||
)
|
)
|
||||||
frappe.local.response["type"] = "redirect"
|
frappe.local.response["type"] = "redirect"
|
||||||
frappe.local.response["location"] = stripe_session.url
|
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)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def stripe_webhook():
|
def stripe_webhook():
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ def map_field_name(frontend_field):
|
||||||
"job_status": "custom_job_status",
|
"job_status": "custom_job_status",
|
||||||
"installation_address": "custom_installation_address",
|
"installation_address": "custom_installation_address",
|
||||||
"warranty_id": "name",
|
"warranty_id": "name",
|
||||||
"customer": "customer_name",
|
"customer": "customer",
|
||||||
"fromCompany": "from_company",
|
"fromCompany": "from_company",
|
||||||
"warranty_status": "warranty_amc_status"
|
"warranty_status": "warranty_amc_status"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ def after_insert(doc, method):
|
||||||
)
|
)
|
||||||
if task_names:
|
if task_names:
|
||||||
doc.save(ignore_permissions=True)
|
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
|
doc.is_scheduled = 0
|
||||||
event = TaskService.determine_event(doc)
|
event = TaskService.determine_event(doc)
|
||||||
if event:
|
if event:
|
||||||
TaskService.calculate_and_set_due_dates(
|
TaskService.fire_task_triggers(
|
||||||
[task.task for task in doc.tasks],
|
[task.task for task in doc.tasks],
|
||||||
event,
|
event,
|
||||||
current_triggering_dict=doc.as_dict()
|
current_triggering_dict=doc.as_dict()
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,14 @@ def on_submit(doc, method):
|
||||||
print("DEBUG: Preparing to send invoice email for", doc.name)
|
print("DEBUG: Preparing to send invoice email for", doc.name)
|
||||||
EmailService.send_invoice_email(doc.name)
|
EmailService.send_invoice_email(doc.name)
|
||||||
print("DEBUG: Invoice email sent successfully for", 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:
|
except Exception as e:
|
||||||
print(f"ERROR: Failed to send invoice email: {str(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
|
# 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")
|
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")
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ def on_update(doc, method):
|
||||||
def after_insert(doc, method):
|
def after_insert(doc, method):
|
||||||
print("DEBUG: After Insert Triggered for Service Appointment")
|
print("DEBUG: After Insert Triggered for Service Appointment")
|
||||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
|
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):
|
def before_save(doc, method):
|
||||||
print("DEBUG: Before Save Triggered for Service Appointment")
|
print("DEBUG: Before Save Triggered for Service Appointment")
|
||||||
|
|
@ -28,4 +28,6 @@ def before_save(doc, method):
|
||||||
event = TaskService.determine_event(doc)
|
event = TaskService.determine_event(doc)
|
||||||
if event:
|
if event:
|
||||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
|
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")
|
||||||
|
|
@ -23,12 +23,33 @@ def after_insert(doc, method):
|
||||||
doc.customer, "tasks", {"task": doc.name, "project_template": doc.project_template }
|
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)]
|
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):
|
def before_save(doc, method):
|
||||||
print("DEBUG: Before Save Triggered for Task:", doc.name)
|
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)
|
event = TaskService.determine_event(doc)
|
||||||
if event:
|
if event:
|
||||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
|
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")
|
||||||
|
|
@ -246,13 +246,13 @@ fixtures = [
|
||||||
# Scheduled Tasks
|
# Scheduled Tasks
|
||||||
# ---------------
|
# ---------------
|
||||||
|
|
||||||
# scheduler_events = {
|
scheduler_events = {
|
||||||
# "all": [
|
# "all": [
|
||||||
# "custom_ui.tasks.all"
|
# "custom_ui.tasks.all"
|
||||||
# ],
|
# ],
|
||||||
# "daily": [
|
"daily": [
|
||||||
# "custom_ui.tasks.daily"
|
"custom_ui.scheduled_tasks.daily"
|
||||||
# ],
|
],
|
||||||
# "hourly": [
|
# "hourly": [
|
||||||
# "custom_ui.tasks.hourly"
|
# "custom_ui.tasks.hourly"
|
||||||
# ],
|
# ],
|
||||||
|
|
@ -262,7 +262,7 @@ fixtures = [
|
||||||
# "monthly": [
|
# "monthly": [
|
||||||
# "custom_ui.tasks.monthly"
|
# "custom_ui.tasks.monthly"
|
||||||
# ],
|
# ],
|
||||||
# }
|
}
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
# -------
|
# -------
|
||||||
|
|
|
||||||
8
custom_ui/scheduled_tasks.py
Normal file
8
custom_ui/scheduled_tasks.py
Normal 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()
|
||||||
|
|
@ -22,6 +22,7 @@ class EstimateService:
|
||||||
print("DEBUG: Quotation document not found.")
|
print("DEBUG: Quotation document not found.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_or_throw(estimate_name: str) -> frappe._dict:
|
def get_or_throw(estimate_name: str) -> frappe._dict:
|
||||||
"""Retrieve a Quotation document by name or throw an error if not found."""
|
"""Retrieve a Quotation document by name or throw an error if not found."""
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import frappe
|
import frappe
|
||||||
|
from custom_ui.services import TaskService, AddressService
|
||||||
|
|
||||||
class ProjectService:
|
class ProjectService:
|
||||||
|
|
||||||
|
|
@ -9,4 +10,20 @@ class ProjectService:
|
||||||
item_groups_str = frappe.db.get_value("Project Template", project_template, "item_groups") or ""
|
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()]
|
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}")
|
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
|
||||||
|
|
@ -15,6 +15,11 @@ class SalesOrderService:
|
||||||
sales_invoice.remarks = f"Auto-generated from Sales Order {sales_order_doc.name}"
|
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.job_address = sales_order_doc.custom_job_address
|
||||||
sales_invoice.project_template = sales_order_doc.custom_project_template
|
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.insert()
|
||||||
sales_invoice.submit()
|
sales_invoice.submit()
|
||||||
return sales_invoice.name
|
return sales_invoice.name
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,14 @@ class ServiceAppointmentService:
|
||||||
setattr(service_appointment, field, value)
|
setattr(service_appointment, field, value)
|
||||||
service_appointment.save()
|
service_appointment.save()
|
||||||
print(f"DEBUG: Updated fields for Service Appointment {service_appointment_name}")
|
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
|
return service_appointment
|
||||||
|
|
@ -6,10 +6,10 @@ class TaskService:
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@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."""
|
"""Calculate the due date for a list of tasks based on their expected end dates."""
|
||||||
for task_name in task_names:
|
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
|
@staticmethod
|
||||||
|
|
@ -20,49 +20,60 @@ class TaskService:
|
||||||
return tasks
|
return tasks
|
||||||
|
|
||||||
@staticmethod
|
@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."""
|
"""Determine the triggering configuration for a given task."""
|
||||||
task_type_doc = TaskService.get_task_type_doc(task_name)
|
task_type_doc = TaskService.get_task_type_doc(task_name)
|
||||||
if task_type_doc.no_due_date:
|
schedule_trigger = task_type_doc.triggering_doctype == current_triggering_dict.get("doctype") if current_triggering_dict else False
|
||||||
print(f"DEBUG: Task {task_name} is marked as no due date, skipping calculation.")
|
completion_trigger = task_type_doc.custom_completion_trigger_doctype == current_triggering_dict.get("doctype") if current_triggering_dict else False
|
||||||
return
|
match_schedule_event = task_type_doc.trigger == event
|
||||||
if task_type_doc.triggering_doctype != current_triggering_dict.get("doctype") and current_triggering_dict:
|
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.")
|
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
|
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.")
|
print(f"DEBUG: Task {task_name} trigger {task_type_doc.trigger} does not match event {event}, skipping calculation.")
|
||||||
return
|
return
|
||||||
if task_type_doc.logic_key:
|
if task_type_doc.logic_key:
|
||||||
print(f"DEBUG: Task {task_name} has a logic key set, skipping calculations and running logic.")
|
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})
|
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
|
return
|
||||||
calculate_from = task_type_doc.calculate_from
|
if schedule_trigger:
|
||||||
trigger = task_type_doc.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)
|
||||||
print(f"DEBUG: Calculating triggering data for Task {task_name} from {calculate_from} on trigger {trigger}")
|
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,
|
||||||
calculated_due_date, calculated_start_date = TaskService.calculate_dates(
|
triggering_doc_dict=triggering_doc_dict,
|
||||||
task_name=task_name,
|
task_type_doc=task_type_doc
|
||||||
triggering_doc_dict=triggering_doc_dict,
|
)
|
||||||
task_type_doc=task_type_doc
|
|
||||||
)
|
update_required = TaskService.determine_due_date_update_required(
|
||||||
|
|
||||||
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(
|
|
||||||
task_name=task_name,
|
task_name=task_name,
|
||||||
calculated_due_date=calculated_due_date,
|
calculated_due_date=calculated_due_date,
|
||||||
calculated_start_date=calculated_start_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
|
@staticmethod
|
||||||
def get_task_type_doc(task_name: str):
|
def get_task_type_doc(task_name: str):
|
||||||
|
|
@ -94,7 +105,7 @@ class TaskService:
|
||||||
return calculated_due_date, calculated_start_date
|
return calculated_due_date, calculated_start_date
|
||||||
|
|
||||||
@staticmethod
|
@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_due_date = frappe.get_value("Task", task_name, "exp_end_date")
|
||||||
current_start_date = frappe.get_value("Task", task_name, "exp_start_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:
|
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.")
|
print(f"DEBUG: No update required for Task {task_name}. Dates are up to date.")
|
||||||
return False
|
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
|
@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")
|
project_name = frappe.get_value("Task", task_name, "project")
|
||||||
print(f"DEBUG: Project name: {project_name}")
|
print(f"DEBUG: Project name: {project_name}")
|
||||||
dict = None
|
dict = None
|
||||||
if task_type_doc.calculate_from == "Project":
|
if doctype == "Project":
|
||||||
dict = frappe.get_doc("Project", project_name).as_dict()
|
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")
|
service_name = frappe.get_value("Project", project_name, "service_appointment")
|
||||||
dict = frappe.get_doc("Service Address 2", service_name).as_dict()
|
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)
|
project_doc = frappe.get_doc("Project", project_name)
|
||||||
for task in project_doc.tasks:
|
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()
|
dict = frappe.get_doc("Task", task.task).as_dict()
|
||||||
print(f"DEBUG: Triggering doc dict for Task {task_name}: {dict}")
|
print(f"DEBUG: Triggering doc dict for Task {task_name}: {dict}")
|
||||||
return dict
|
return dict
|
||||||
|
|
||||||
@staticmethod
|
@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 = frappe.get_doc("Task", task_name)
|
||||||
task_doc.exp_end_date = calculated_due_date
|
if calculated_due_date is not None:
|
||||||
task_doc.exp_start_date = calculated_start_date
|
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)
|
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
|
@staticmethod
|
||||||
def map_base_date_to_field(base_date: str, triggering_doctype: str) -> str:
|
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 task_date_field_map.get(base_date, "exp_end_date")
|
||||||
return base_date_field_map.get(base_date, "expected_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
|
@staticmethod
|
||||||
def determine_event(triggering_doc) -> str | None:
|
def determine_event(triggering_doc) -> str | None:
|
||||||
print("DEBUG: Current Document:", triggering_doc.as_dict())
|
print("DEBUG: Current Document:", triggering_doc.as_dict())
|
||||||
|
|
@ -168,4 +218,16 @@ class TaskService:
|
||||||
return "Completed"
|
return "Completed"
|
||||||
else:
|
else:
|
||||||
return None
|
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()
|
||||||
|
|
||||||
0
custom_ui/templates/emails/payment-confirmation.html
Normal file
0
custom_ui/templates/emails/payment-confirmation.html
Normal 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_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";
|
const FRAPPE_GET_DOC_LIST_METHOD = "custom_ui.api.db.general.get_doc_list";
|
||||||
// Service Appointment methods
|
// 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_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_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 {
|
class Api {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CORE REQUEST METHOPD
|
// CORE REQUEST METHOPD
|
||||||
|
|
@ -457,6 +459,10 @@ class Api {
|
||||||
// SERVICE APPOINTMENT METHODS
|
// SERVICE APPOINTMENT METHODS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
static async getServiceAppointment(serviceAppointmentName) {
|
||||||
|
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENT_METHOD, { serviceAppointmentName });
|
||||||
|
}
|
||||||
|
|
||||||
static async getServiceAppointments(companies = [], filters = {}) {
|
static async getServiceAppointments(companies = [], filters = {}) {
|
||||||
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD, { 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
|
// TASK METHODS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="calendar-navigation">
|
<div class="calendar-navigation">
|
||||||
<Tabs value="0" v-if="companyStore.currentCompany == 'Sprinklers Northwest'">
|
<Tabs :value="defaultTab" v-if="companyStore.currentCompany == 'Sprinklers Northwest'">
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value="0">Bids</Tab>
|
<Tab value="0">Bids</Tab>
|
||||||
<Tab value="1">Projects</Tab>
|
<Tab value="1">Projects</Tab>
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Tabs v-else value="0">
|
<Tabs v-else :value="defaultTab">
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value="0">Bids</Tab>
|
<Tab value="0">Bids</Tab>
|
||||||
<Tab value="1">Projects</Tab>
|
<Tab value="1">Projects</Tab>
|
||||||
|
|
@ -61,20 +61,36 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import Tab from 'primevue/tab';
|
import Tab from 'primevue/tab';
|
||||||
import Tabs from 'primevue/tabs';
|
import Tabs from 'primevue/tabs';
|
||||||
import TabList from 'primevue/tablist';
|
import TabList from 'primevue/tablist';
|
||||||
import TabPanel from 'primevue/tabpanel';
|
import TabPanel from 'primevue/tabpanel';
|
||||||
import TabPanels from 'primevue/tabpanels';
|
import TabPanels from 'primevue/tabpanels';
|
||||||
import ScheduleBid from '../calendar/bids/ScheduleBid.vue';
|
import ScheduleBid from '../calendar/bids/ScheduleBid.vue';
|
||||||
import JobsCalendar from '../calendar/jobs/JobsCalendar.vue';
|
|
||||||
import SNWProjectCalendar from './jobs/SNWProjectCalendar.vue';
|
import SNWProjectCalendar from './jobs/SNWProjectCalendar.vue';
|
||||||
import { useNotificationStore } from '../../stores/notifications-primevue';
|
import { useNotificationStore } from '../../stores/notifications-primevue';
|
||||||
import { useCompanyStore } from '../../stores/company';
|
import { useCompanyStore } from '../../stores/company';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
const notifications = useNotificationStore();
|
|
||||||
const companyStore = useCompanyStore();
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -330,10 +330,14 @@ import { ref, onMounted, computed, watch } from "vue";
|
||||||
import Api from "../../../api";
|
import Api from "../../../api";
|
||||||
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
||||||
import { useCompanyStore } from "../../../stores/company";
|
import { useCompanyStore } from "../../../stores/company";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import JobDetailsModal from "../../modals/JobDetailsModal.vue";
|
import JobDetailsModal from "../../modals/JobDetailsModal.vue";
|
||||||
|
|
||||||
const notifications = useNotificationStore();
|
const notifications = useNotificationStore();
|
||||||
const companyStore = useCompanyStore();
|
const companyStore = useCompanyStore();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const serviceAptToFind = route.query.apt || null;
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const scheduledServices = ref([]);
|
const scheduledServices = ref([]);
|
||||||
|
|
@ -1302,6 +1306,40 @@ const stopResize = async () => {
|
||||||
originalEndDate.value = null;
|
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) => {
|
const fetchServiceAppointments = async (currentDate) => {
|
||||||
try {
|
try {
|
||||||
// Calculate date range for the week
|
// Calculate date range for the week
|
||||||
|
|
@ -1397,6 +1435,9 @@ onMounted(async () => {
|
||||||
// await fetchProjects();
|
// await fetchProjects();
|
||||||
await fetchServiceAppointments();
|
await fetchServiceAppointments();
|
||||||
await fetchHolidays();
|
await fetchHolidays();
|
||||||
|
|
||||||
|
// Handle serviceAptToFind query parameter
|
||||||
|
await handleServiceAptToFind();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,17 @@
|
||||||
<v-icon left>mdi-open-in-new</v-icon>
|
<v-icon left>mdi-open-in-new</v-icon>
|
||||||
View Job
|
View Job
|
||||||
</v-btn>
|
</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-spacer></v-spacer>
|
||||||
<v-btn variant="text" @click="handleClose">Close</v-btn>
|
<v-btn variant="text" @click="handleClose">Close</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
@ -196,6 +207,10 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
import Api from "../../api";
|
||||||
|
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||||
|
|
||||||
|
const notifications = useNotificationStore();
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -214,7 +229,10 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(["update:modelValue", "close"]);
|
const emit = defineEmits(["update:modelValue", "close", "statusUpdated"]);
|
||||||
|
|
||||||
|
// State
|
||||||
|
const isUpdatingStatus = ref(false);
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const showModal = 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 = () => {
|
const viewJob = () => {
|
||||||
if (props.job?.name) {
|
if (props.job?.name) {
|
||||||
window.location.href = `/job?name=${encodeURIComponent(props.job.name)}`;
|
window.location.href = `/job?name=${encodeURIComponent(props.job.name)}`;
|
||||||
|
|
|
||||||
|
|
@ -1163,6 +1163,8 @@ onMounted(async () => {
|
||||||
qty: item.qty,
|
qty: item.qty,
|
||||||
rate: item.rate,
|
rate: item.rate,
|
||||||
standardRate: item.rate,
|
standardRate: item.rate,
|
||||||
|
bom: item.bom || null,
|
||||||
|
uom: item.uom || item.stockUom || item.stock_uom || 'Nos',
|
||||||
discountAmount: discountAmount === 0 ? null : discountAmount,
|
discountAmount: discountAmount === 0 ? null : discountAmount,
|
||||||
discountPercentage: discountPercentage === 0 ? null : discountPercentage,
|
discountPercentage: discountPercentage === 0 ? null : discountPercentage,
|
||||||
discountType: discountPercentage > 0 ? 'percentage' : 'currency'
|
discountType: discountPercentage > 0 ? 'percentage' : 'currency'
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -136,9 +136,9 @@ const chartData = ref({
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = [
|
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: "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: "Overall Status", fieldName: "status", type: "status", sortable: true },
|
||||||
{ label: "Invoice Status", fieldName: "invoiceStatus", type: "text", sortable: true },
|
{ label: "Invoice Status", fieldName: "invoiceStatus", type: "text", sortable: true },
|
||||||
{ label: "Progress", fieldName: "percentComplete", type: "text", sortable: true }
|
{ label: "Progress", fieldName: "percentComplete", type: "text", sortable: true }
|
||||||
|
|
@ -275,7 +275,6 @@ const loadChartData = async () => {
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
notifications.addWarning("Jobs page coming soon");
|
|
||||||
// Initialize pagination and filters
|
// Initialize pagination and filters
|
||||||
paginationStore.initializeTablePagination("jobs", { rows: 10 });
|
paginationStore.initializeTablePagination("jobs", { rows: 10 });
|
||||||
filtersStore.initializeTableFilters("jobs", columns);
|
filtersStore.initializeTableFilters("jobs", columns);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue