From 7710a7c8fed93bbea03c9ee7a81a91b3cd06f16f Mon Sep 17 00:00:00 2001 From: Casey Date: Tue, 20 Jan 2026 00:58:03 -0600 Subject: [PATCH] updates for company effects --- custom_ui/api/db/bid_meetings.py | 11 +- custom_ui/api/db/clients.py | 77 +++- custom_ui/api/db/jobs.py | 69 ++-- custom_ui/db_utils.py | 4 + custom_ui/events/jobs.py | 25 +- custom_ui/events/task.py | 21 +- custom_ui/fixtures/doctype.json | 382 +++++++++++++++--- custom_ui/fixtures/property_setter.json | 1 + custom_ui/hooks.py | 17 +- custom_ui/install.py | 56 ++- frontend/src/api.js | 29 +- .../calendar/CalendarNavigation.vue | 40 +- .../components/calendar/bids/ScheduleBid.vue | 25 +- ...tallsCalendar.vue => ProjectsCalendar.vue} | 244 ++++++++++- .../components/clientSubPages/Overview.vue | 9 +- frontend/src/components/common/DataTable.vue | 4 +- .../common/GlobalLoadingOverlay.vue | 65 ++- frontend/src/components/pages/Calendar.vue | 3 + frontend/src/components/pages/Clients.vue | 27 +- frontend/src/components/pages/Job.vue | 2 +- frontend/src/components/pages/Jobs.vue | 2 +- frontend/src/components/pages/Tasks.vue | 14 +- 22 files changed, 941 insertions(+), 186 deletions(-) create mode 100644 custom_ui/fixtures/property_setter.json rename frontend/src/components/calendar/jobs/{InstallsCalendar.vue => ProjectsCalendar.vue} (80%) diff --git a/custom_ui/api/db/bid_meetings.py b/custom_ui/api/db/bid_meetings.py index bc73060..3be0432 100644 --- a/custom_ui/api/db/bid_meetings.py +++ b/custom_ui/api/db/bid_meetings.py @@ -4,7 +4,7 @@ from custom_ui.db_utils import build_error_response, build_success_response, pro from custom_ui.services import DbService, ClientService, AddressService, ContactService @frappe.whitelist() -def get_week_bid_meetings(week_start, week_end): +def get_week_bid_meetings(week_start, week_end, company): """Get On-Site Meetings scheduled within a specific week.""" try: meetings = frappe.db.get_all( @@ -12,7 +12,8 @@ def get_week_bid_meetings(week_start, week_end): fields=["*"], filters=[ ["start_time", ">=", week_start], - ["start_time", "<=", week_end] + ["start_time", "<=", week_end], + ["company", "=", company] ], order_by="start_time asc" ) @@ -27,7 +28,7 @@ def get_week_bid_meetings(week_start, week_end): return build_error_response(str(e), 500) @frappe.whitelist() -def get_bid_meetings(fields=["*"], filters={}): +def get_bid_meetings(fields=["*"], filters={}, company=None): """Get paginated On-Site Meetings with filtering and sorting support.""" try: print("DEBUG: Raw bid meeting options received:", filters) @@ -53,13 +54,13 @@ def get_bid_meetings(fields=["*"], filters={}): @frappe.whitelist() -def get_unscheduled_bid_meetings(): +def get_unscheduled_bid_meetings(company): """Get On-Site Meetings that are unscheduled.""" try: meetings = frappe.db.get_all( "On-Site Meeting", fields=["*"], - filters={"status": "Unscheduled"}, + filters={"status": "Unscheduled", "company": company}, order_by="creation desc" ) for meeting in meetings: diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index 0edb984..b306528 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -1,5 +1,5 @@ import frappe, json -from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, map_lead_client, build_address_title +from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, map_lead_client, build_address_title, normalize_name from erpnext.crm.doctype.lead.lead import make_customer from custom_ui.api.db.addresses import address_exists from custom_ui.api.db.contacts import check_and_get_contact, create_contact, create_contact_links @@ -167,6 +167,81 @@ def get_client_v2(client_name): return build_error_response(str(ve), 400) except Exception as e: return build_error_response(str(e), 500) + + + +@frappe.whitelist() +def get_clients_table_data_v2(filters={}, sortings=[], page=1, page_size=10): + """Get paginated client table data with filtering and sorting support.""" + try: + filters = json.loads(filters) if isinstance(filters, str) else filters + sortings = json.loads(sortings) if isinstance(sortings, str) else sortings + page = int(page) + page_size = int(page_size) + print("DEBUG: Raw client table query received:", { + "filters": filters, + "sortings": sortings, + "page": page, + "page_size": page_size + }) + where_clauses = [] + values = [] + if filters.get("company"): + where_clauses.append("c.company = %s") + values.append(filters["company"]["value"]) + + if filters.get("address"): + where_clauses.append("a.full_address LIKE %s") + values.append(f"%{filters['address']['value']}%") + + if filters.get("customer_name"): + where_clauses.append("a.customer_name LIKE %s") + values.append(f"%{filters['customer_name']['value']}%") + + where_sql = "" + if where_clauses: + where_sql = "WHERE " + " AND ".join(where_clauses) + + offset = (page - 1) * page_size + + address_names = frappe.db.sql(f""" + SELECT DISTINCT a.name + FROM `tabAddress` a + LEFT JOIN `tabAddress Company Link` c ON c.parent = a.name + {where_sql} + ORDER BY a.modified DESC + LIMIT %s OFFSET %s + """, values + [page_size, offset], as_dict=True) + print("DEBUG: Address names retrieved:", address_names) + + count = frappe.db.sql(f""" + SELECT COUNT(DISTINCT a.name) as count + FROM `tabAddress` a + LEFT JOIN `tabAddress Company Link` c ON c.parent = a.name + {where_sql} + """, values, as_dict=True)[0]["count"] + tableRows = [] + for address_name in address_names: + address = AddressService.get_or_throw(address_name["name"]) + tableRow = {} + tableRow["id"] = address.name + tableRow["address"] = address.full_address + tableRow["client_type"] = address.customer_type + tableRow["customer_name"] = normalize_name(address.customer_name, "-#-") + tableRow["companies"] = ", ".join([link.company for link in address.get("companies", [])]) + tableRows.append(tableRow) + + table_data = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size) + + return build_success_response(table_data) + except frappe.ValidationError as ve: + return build_error_response(str(ve), 400) + except Exception as e: + print("ERROR in get_clients_table_data_v2:", str(e)) + return build_error_response(str(e), 500) + + + @frappe.whitelist() diff --git a/custom_ui/api/db/jobs.py b/custom_ui/api/db/jobs.py index 9b7b419..5efe3a0 100644 --- a/custom_ui/api/db/jobs.py +++ b/custom_ui/api/db/jobs.py @@ -50,6 +50,8 @@ def get_job(job_id=""): 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) except Exception as e: return build_error_response(str(e), 500) @@ -167,55 +169,32 @@ def upsert_job(data): return {"status": "error", "message": str(e)} @frappe.whitelist() -def get_install_projects(start_date=None, end_date=None): +def get_projects_for_calendar(date, company=None, project_templates=[]): """Get install projects for the calendar.""" + # Parse project_templates if it's a JSON string + if isinstance(project_templates, str): + project_templates = json.loads(project_templates) + + # put some emojis in the print to make it stand out + print("📅📅📅", date, "company:", company, "project_templates:", project_templates, "type:", type(project_templates)) try: - filters = {"project_template": "SNW Install"} + filters = {"company": company} if company else {} + if project_templates and len(project_templates) > 0: + filters["project_template"] = ["in", project_templates] + unscheduled_filters = filters.copy() + unscheduled_filters["is_scheduled"] = 0 + filters["expected_start_date"] = date # If date range provided, we could filter, but for now let's fetch all open/active ones # or maybe filter by status not Closed/Completed if we want active ones. # The user said "unscheduled" are those with status "Open" (and no date). + # extend filters into unscheduled_filters - projects = frappe.get_all("Project", fields=["*"], filters=filters) - - calendar_events = [] - for project in projects: - # Determine status - status = "unscheduled" - if project.get("expected_start_date"): - status = "scheduled" - - # Map to calendar event format - event = { - "id": project.name, - "serviceType": project.project_name, # Using project name as service type/title - "customer": project.customer, - "status": status, - "scheduledDate": project.expected_start_date, - "scheduledTime": "08:00", # Default time if not specified? Project doesn't seem to have time. - "duration": 480, # Default 8 hours? - "foreman": project.get("custom_install_crew"), - "crew": [], # Need to map crew - "estimatedCost": project.estimated_costing, - "priority": project.priority.lower() if project.priority else "medium", - "notes": project.notes, - "address": project.custom_installation_address - } - - calendar_events.append(event) - - return {"status": "success", "data": calendar_events} + project_names = frappe.get_all("Project", pluck="name", filters=filters) + print("DEBUG: Found scheduled project names:", project_names) + unscheduled_project_names = frappe.get_all("Project", pluck="name", filters=unscheduled_filters) + print("DEBUG: Found unscheduled project names:", unscheduled_project_names) + projects = [frappe.get_doc("Project", name).as_dict() for name in project_names] + unscheduled_projects = [frappe.get_doc("Project", name).as_dict() for name in unscheduled_project_names] + return build_success_response({ "projects": projects, "unscheduled_projects": unscheduled_projects }) except Exception as e: - return {"status": "error", "message": str(e)} - -@frappe.whitelist() -def get_project_templates_for_company(company_name): - """Get project templates for a specific company.""" - try: - templates = frappe.get_all( - "Project Template", - fields=["*"], - filters={"company": company_name} - ) - return build_success_response(templates) - except Exception as e: - return build_error_response(str(e), 500), + return build_error_response(str(e), 500) \ No newline at end of file diff --git a/custom_ui/db_utils.py b/custom_ui/db_utils.py index 3c577a8..0bf9915 100644 --- a/custom_ui/db_utils.py +++ b/custom_ui/db_utils.py @@ -229,3 +229,7 @@ def build_history_entries(comments, versions): # Sort by timestamp descending history.sort(key=lambda x: x["timestamp"], reverse=True) return history + +def normalize_name(name: str, split_target: str = "_") -> str: + """Normalize a name by splitting off anything after and including the split_target.""" + return name.split(split_target)[0] if split_target in name else name diff --git a/custom_ui/events/jobs.py b/custom_ui/events/jobs.py index aa80dc0..7801611 100644 --- a/custom_ui/events/jobs.py +++ b/custom_ui/events/jobs.py @@ -25,4 +25,27 @@ def after_insert(doc, method): def before_insert(doc, method): # This is where we will add logic to set tasks and other properties of a job based on it's project_template - pass \ No newline at end of file + pass + +def before_save(doc, method): + print("DEBUG: Before Save Triggered for Project:", doc.name) + if doc.expected_start_date and doc.expected_end_date: + doc.is_scheduled = 1 + else: + doc.is_scheduled = 0 + +def after_save(doc, method): + print("DEBUG: After Save Triggered for Project:", doc.name) + if doc.project_template == "SNW Install": + print("DEBUG: Project template is SNW Install, updating Address Job Status based on Project status") + status_mapping = { + "Open": "In Progress", + "Completed": "Completed", + "Closed": "Completed" + } + new_status = status_mapping.get(doc.status, "In Progress") + AddressService.update_value( + doc.job_address, + "job_status", + new_status + ) \ No newline at end of file diff --git a/custom_ui/events/task.py b/custom_ui/events/task.py index 2e32836..fe226a5 100644 --- a/custom_ui/events/task.py +++ b/custom_ui/events/task.py @@ -1,7 +1,24 @@ import frappe +from custom_ui.services import AddressService, ClientService def before_insert(doc, method): """Set values before inserting a Task.""" + print("DEBUG: Before Insert Triggered for Task") project_doc = frappe.get_doc("Project", doc.project) - if project_doc.custom_installation_address: - doc.custom_property = project_doc.custom_installation_address \ No newline at end of file + doc.project_template = project_doc.project_template + if project_doc.job_address: + doc.custom_property = project_doc.job_address + +def after_insert(doc, method): + print("DEBUG: After Insert Triggered for Task") + print("DEBUG: Linking Task to Customer and Address") + AddressService.append_link_v2( + doc.custom_property, "tasks", {"task": doc.name, "project_template": doc.project_template } + ) + AddressService.append_link_v2( + doc.custom_property, "links", {"link_doctype": "Task", "link_name": doc.name} + ) + ClientService.append_link_v2( + doc.customer, "tasks", {"task": doc.name, "project_template": doc.project_template } + ) + \ No newline at end of file diff --git a/custom_ui/fixtures/doctype.json b/custom_ui/fixtures/doctype.json index 67f0e8c..d2859ca 100644 --- a/custom_ui/fixtures/doctype.json +++ b/custom_ui/fixtures/doctype.json @@ -44,7 +44,7 @@ "documentation_url": null, "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "company", + "fieldname": "task", "fieldtype": "Link", "hidden": 0, "hide_border": 0, @@ -58,7 +58,7 @@ "in_preview": 0, "in_standard_filter": 0, "is_virtual": 0, - "label": "Company", + "label": "Task", "length": 0, "link_filters": null, "make_attachment_public": 0, @@ -68,8 +68,8 @@ "non_negative": 0, "oldfieldname": null, "oldfieldtype": null, - "options": "Company", - "parent": "Lead Company Link", + "options": "Task", + "parent": "Customer Task Link", "parentfield": "fields", "parenttype": "DocType", "permlevel": 0, @@ -93,6 +93,70 @@ "trigger": null, "unique": 0, "width": null + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "documentation_url": null, + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "project_template", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "is_virtual": 0, + "label": "Project Template", + "length": 0, + "link_filters": null, + "make_attachment_public": 0, + "mandatory_depends_on": null, + "max_height": null, + "no_copy": 0, + "non_negative": 0, + "oldfieldname": null, + "oldfieldtype": null, + "options": "Project Template", + "parent": "Customer Task Link", + "parentfield": "fields", + "parenttype": "DocType", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "show_dashboard": 0, + "show_on_timeline": 0, + "show_preview_popup": 0, + "sort_options": 0, + "translatable": 0, + "trigger": null, + "unique": 0, + "width": null } ], "force_re_route_to_default_view": 0, @@ -114,10 +178,228 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:39.197431", - "module": "Custom", - "name": "Lead Company Link", + "migration_hash": null, + "modified": "2026-01-19 18:10:16.782664", + "module": "Custom UI", + "name": "Customer Task Link", + "naming_rule": "", + "nsm_parent_field": null, + "parent_node": null, + "permissions": [], + "print_outline": null, + "protect_attached_files": 0, + "queue_in_background": 0, + "quick_entry": 0, + "read_only": 0, + "recipient_account_field": null, + "restrict_to_domain": null, + "route": null, + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "search_fields": null, + "sender_field": null, + "sender_name_field": null, + "show_name_in_global_search": 0, + "show_preview_popup": 0, + "show_title_field_in_link": 0, + "smallicon": null, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "subject": null, + "subject_field": null, + "tag_fields": null, + "timeline_field": null, + "title_field": null, + "track_changes": 0, + "track_seen": 0, + "track_views": 0, + "translated_doctype": 0, + "website_search_field": null + }, + { + "_assign": null, + "_comments": null, + "_last_update": null, + "_liked_by": null, + "_user_tags": null, + "actions": [], + "allow_auto_repeat": 0, + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 1, + "app": null, + "autoname": null, + "beta": 0, + "color": null, + "colour": null, + "custom": 1, + "default_email_template": null, + "default_print_format": null, + "default_view": null, + "description": null, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "documentation": null, + "editable_grid": 1, + "email_append_to": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "documentation_url": null, + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "task", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 0, + "is_virtual": 0, + "label": "Task", + "length": 0, + "link_filters": null, + "make_attachment_public": 0, + "mandatory_depends_on": null, + "max_height": null, + "no_copy": 0, + "non_negative": 0, + "oldfieldname": null, + "oldfieldtype": null, + "options": "Task", + "parent": "Address Task Link", + "parentfield": "fields", + "parenttype": "DocType", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "show_dashboard": 0, + "show_on_timeline": 0, + "show_preview_popup": 0, + "sort_options": 0, + "translatable": 0, + "trigger": null, + "unique": 0, + "width": null + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "documentation_url": null, + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "project_template", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "is_virtual": 0, + "label": "Project Template", + "length": 0, + "link_filters": null, + "make_attachment_public": 0, + "mandatory_depends_on": null, + "max_height": null, + "no_copy": 0, + "non_negative": 0, + "oldfieldname": null, + "oldfieldtype": null, + "options": "Project Template", + "parent": "Address Task Link", + "parentfield": "fields", + "parenttype": "DocType", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "show_dashboard": 0, + "show_on_timeline": 0, + "show_preview_popup": 0, + "sort_options": 0, + "translatable": 0, + "trigger": null, + "unique": 0, + "width": null + } + ], + "force_re_route_to_default_view": 0, + "grid_page_length": 50, + "has_web_view": 0, + "hide_toolbar": 0, + "icon": null, + "image_field": null, + "in_create": 0, + "index_web_pages_for_search": 1, + "is_calendar_and_gantt": 0, + "is_published_field": null, + "is_submittable": 0, + "is_tree": 0, + "is_virtual": 0, + "issingle": 0, + "istable": 1, + "links": [], + "make_attachments_public": 0, + "max_attachments": 0, + "menu_index": null, + "migration_hash": null, + "modified": "2026-01-19 18:10:02.359022", + "module": "Custom UI", + "name": "Address Task Link", "naming_rule": "", "nsm_parent_field": null, "parent_node": null, @@ -268,8 +550,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.521684", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.097017", "module": "Custom", "name": "Lead Companies Link", "naming_rule": "", @@ -486,8 +768,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.576521", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.150584", "module": "Custom", "name": "Address Project Link", "naming_rule": "", @@ -704,8 +986,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.628136", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.203403", "module": "Custom", "name": "Address Quotation Link", "naming_rule": "", @@ -922,8 +1204,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.681893", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.255846", "module": "Custom", "name": "Address On-Site Meeting Link", "naming_rule": "", @@ -1140,8 +1422,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.737017", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.309600", "module": "Custom", "name": "Address Sales Order Link", "naming_rule": "", @@ -1294,8 +1576,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.787995", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.361237", "module": "Custom", "name": "Contact Address Link", "naming_rule": "", @@ -1448,8 +1730,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.837721", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.412683", "module": "Custom", "name": "Lead On-Site Meeting Link", "naming_rule": "", @@ -2050,8 +2332,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.906370", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.483924", "module": "Selling", "name": "Quotation Template", "naming_rule": "", @@ -2548,8 +2830,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:34.977831", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.558008", "module": "Selling", "name": "Quotation Template Item", "naming_rule": "", @@ -2702,8 +2984,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.031029", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.609372", "module": "Custom UI", "name": "Customer Company Link", "naming_rule": "", @@ -2856,8 +3138,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.084461", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.660893", "module": "Custom UI", "name": "Customer Address Link", "naming_rule": "", @@ -3010,8 +3292,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.135851", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.712878", "module": "Custom UI", "name": "Customer Contact Link", "naming_rule": "", @@ -3164,8 +3446,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.184768", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.765849", "module": "Custom", "name": "Address Contact Link", "naming_rule": "", @@ -3318,8 +3600,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.236428", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.818352", "module": "Custom", "name": "Customer On-Site Meeting Link", "naming_rule": "", @@ -3472,8 +3754,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.287145", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.870984", "module": "Custom", "name": "Customer Project Link", "naming_rule": "", @@ -3626,8 +3908,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.338967", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.922695", "module": "Custom", "name": "Customer Quotation Link", "naming_rule": "", @@ -3780,8 +4062,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.388711", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:17.975165", "module": "Custom", "name": "Customer Sales Order Link", "naming_rule": "", @@ -3934,8 +4216,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.441876", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:18.027046", "module": "Custom", "name": "Lead Address Link", "naming_rule": "", @@ -4088,8 +4370,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.492936", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:18.078476", "module": "Custom", "name": "Lead Contact Link", "naming_rule": "", @@ -4242,8 +4524,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.545465", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:18.170095", "module": "Custom", "name": "Lead Quotation Link", "naming_rule": "", @@ -4396,8 +4678,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "7c3c71cf20b258daa783e541cb045a4b", - "modified": "2026-01-16 04:11:35.604415", + "migration_hash": "0df0ede31f640435231ba887f40eca91", + "modified": "2026-01-19 20:52:18.238066", "module": "Custom", "name": "Address Company Link", "naming_rule": "", diff --git a/custom_ui/fixtures/property_setter.json b/custom_ui/fixtures/property_setter.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/custom_ui/fixtures/property_setter.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/custom_ui/hooks.py b/custom_ui/hooks.py index 1759776..e7ec24b 100644 --- a/custom_ui/hooks.py +++ b/custom_ui/hooks.py @@ -181,10 +181,13 @@ doc_events = { }, "Project": { "before_insert": "custom_ui.events.jobs.before_insert", - "after_insert": "custom_ui.events.jobs.after_insert" + "after_insert": "custom_ui.events.jobs.after_insert", + "before_save": "custom_ui.events.jobs.before_save", + "on_update": "custom_ui.events.jobs.after_save" }, "Task": { - "before_insert": "custom_ui.events.task.before_insert" + "before_insert": "custom_ui.events.task.before_insert", + "after_insert": "custom_ui.events.task.after_insert" } } @@ -216,6 +219,8 @@ fixtures = [ "Address Contact Link", "Address Company Link", "Contact Address Link", + "Address Task Link", + "Customer Task Link" ]] ] }, @@ -249,7 +254,15 @@ fixtures = [ ["dt", "=", "Project Template"], ["fieldname", "=", "company"] ] + }, + { + "dt": "Property Setter", + "filters": [ + ["doc_type", "=", "Lead"], + ["doc_type", "=", "Project"] + ] } + ] diff --git a/custom_ui/install.py b/custom_ui/install.py index 014cf70..0453f49 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -30,7 +30,7 @@ def after_migrate(): frappe.clear_cache(doctype=doctype) frappe.reload_doctype(doctype) - update_address_fields() + # update_address_fields() # build_frontend() @@ -146,6 +146,13 @@ def add_custom_fields(): fieldtype="Link", options="Contact", insert_after="contacts" + ), + dict( + fieldname="tasks", + label="Tasks", + fieldtype="Table", + options="Customer Task Link", + insert_after="projects" ) ], "Lead": [ @@ -327,6 +334,13 @@ def add_custom_fields(): fieldtype="Table", options="Address Company Link", insert_after="contacts" + ), + dict( + fieldname="tasks", + label="Tasks", + fieldtype="Table", + options="Address Task Link", + insert_after="projects" ) ], "Contact": [ @@ -539,6 +553,37 @@ def add_custom_fields(): options="Customer", insert_after="job_address", description="The customer for whom the project is being executed." + ), + dict( + fieldname="expected_start_time", + label="Expected Start Time", + fieldtype="Time", + insert_after="expected_start_date" + ), + dict( + fieldname="expected_end_time", + label="Expected End Time", + fieldtype="Time", + insert_after="expected_end_date" + ), + dict( + fieldname="actual_start_time", + label="Actual Start Time", + fieldtype="Time", + insert_after="actual_start_date" + ), + dict( + fieldname="actual_end_time", + label="Actual End Time", + fieldtype="Time", + insert_after="actual_end_date" + ), + dict( + fieldname="is_scheduled", + label="Is Scheduled", + fieldtype="Check", + default=0, + insert_after="expected_end_time" ) ], "Project Template": [ @@ -550,6 +595,15 @@ def add_custom_fields(): insert_after="project_type", description="The company associated with this project template." ) + ], + "Task": [ + dict( + fieldname="project_template", + label="Project Template", + fieldtype="Link", + options="Project Template", + insert_after="project" + ) ] } diff --git a/frontend/src/api.js b/frontend/src/api.js index 936cae9..3663e52 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -18,8 +18,10 @@ const FRAPPE_CREATE_ESTIMATE_TEMPLATE_METHOD = "custom_ui.api.db.estimates.creat const FRAPPE_GET_JOB_METHOD = "custom_ui.api.db.jobs.get_job"; const FRAPPE_GET_JOBS_METHOD = "custom_ui.api.db.jobs.get_jobs_table_data"; const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.jobs.upsert_job"; -const FRAPPE_GET_JOB_TASK_LIST_METHOD = "custom_ui.api.db.jobs.get_job_task_table_data"; +const FRAPPE_GET_JOB_TASK_TABLE_DATA_METHOD = "custom_ui.api.db.jobs.get_job_task_table_data"; +const FRAPPE_GET_JOB_TASK_LIST_METHOD = "custom_ui.api.db.jobs.get_job_task_list"; const FRAPPE_GET_INSTALL_PROJECTS_METHOD = "custom_ui.api.db.jobs.get_install_projects"; +const FRAPPE_GET_JOBS_FOR_CALENDAR_METHOD = "custom_ui.api.db.jobs.get_projects_for_calendar"; const FRAPPE_GET_JOB_TEMPLATES_METHOD = "custom_ui.api.db.jobs.get_job_templates"; // Task methods const FRAPPE_GET_TASKS_METHOD = "custom_ui.api.db.tasks.get_tasks_table_data"; @@ -42,6 +44,7 @@ const FRAPPE_GET_ADDRESSES_METHOD = "custom_ui.api.db.addresses.get_addresses"; const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client"; const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.clients.get_client_status_counts"; const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_clients_table_data"; +const FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD = "custom_ui.api.db.clients.get_clients_table_data_v2"; const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2"; const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names"; @@ -87,6 +90,17 @@ class Api { return await this.request(FRAPPE_GET_CLIENT_METHOD, { clientName }); } + static async getPaginatedClientDetailsV2(paginationParams = {}, filters = {}, sortings = []) { + const { page = 0, pageSize = 10 } = paginationParams; + const result = await this.request(FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD, { + filters, + sortings, + page: page + 1, + pageSize, + }); + return result; + } + /** * Get paginated client data with filtering and sorting * @param {Object} paginationParams - Pagination parameters from store @@ -138,9 +152,10 @@ class Api { // ON-SITE MEETING METHODS // ============================================================================ - static async getUnscheduledBidMeetings() { + static async getUnscheduledBidMeetings(company) { return await this.request( "custom_ui.api.db.bid_meetings.get_unscheduled_bid_meetings", + { company } ); } @@ -148,8 +163,8 @@ class Api { return await this.request(FRAPPE_GET_ONSITE_MEETINGS_METHOD, { fields, filters }); } - static async getWeekBidMeetings(weekStart, weekEnd) { - return await this.request(FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD, { weekStart, weekEnd }); + static async getWeekBidMeetings(weekStart, weekEnd, company) { + return await this.request(FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD, { weekStart, weekEnd, company }); } static async updateBidMeeting(name, data) { @@ -300,6 +315,10 @@ class Api { return result; } + static async getJobsForCalendar(date, company = null, projectTemplates = []) { + return await this.request(FRAPPE_GET_JOBS_FOR_CALENDAR_METHOD, { date, company, projectTemplates }); + } + static async getJob(jobName) { if (frappe.db.exists("Project", jobName)) { const result = await this.request(FRAPPE_GET_JOB_METHOD, { jobId: jobName }) @@ -346,7 +365,7 @@ class Api { console.log("DEBUG: API - Sending job task options to backend:", options); - const result = await this.request(FRAPPE_GET_JOB_TASK_LIST_METHOD, { options, filters }); + const result = await this.request(FRAPPE_GET_JOB_TASK_TABLE_DATA_METHOD, { filters, sortings: sorting, page:page+1, pageSize }); return result; } diff --git a/frontend/src/components/calendar/CalendarNavigation.vue b/frontend/src/components/calendar/CalendarNavigation.vue index 6d8993a..d16cee5 100644 --- a/frontend/src/components/calendar/CalendarNavigation.vue +++ b/frontend/src/components/calendar/CalendarNavigation.vue @@ -3,11 +3,8 @@ Bids - Install + Projects Service - Lowe Fencing - Daniel's Landscaping - Nuco Yardcare Warranties @@ -22,21 +19,6 @@

Service feature coming soon!

- -
-

Lowe Fencing calendar coming soon!

-
-
- -
-

Daniel's Calendar coming soon!

-
-
- -
-

Nuco calendar coming soon!

-
-

Warranties Calendar coming soon!

@@ -56,7 +38,7 @@ 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 InstallsCalendar from '../calendar/jobs/InstallsCalendar.vue'; +import InstallsCalendar from './jobs/ProjectsCalendar.vue'; import { useNotificationStore } from '../../stores/notifications-primevue'; const notifications = useNotificationStore(); @@ -65,6 +47,24 @@ const notifications = useNotificationStore(); diff --git a/frontend/src/components/pages/Calendar.vue b/frontend/src/components/pages/Calendar.vue index f9371c8..895bfbb 100644 --- a/frontend/src/components/pages/Calendar.vue +++ b/frontend/src/components/pages/Calendar.vue @@ -7,4 +7,7 @@ import CalendarNavigation from '@/components/calendar/CalendarNavigation.vue' diff --git a/frontend/src/components/pages/Clients.vue b/frontend/src/components/pages/Clients.vue index 815836f..d5cc7f4 100644 --- a/frontend/src/components/pages/Clients.vue +++ b/frontend/src/components/pages/Clients.vue @@ -29,10 +29,12 @@ import { useFiltersStore } from "../../stores/filters"; import { useModalStore } from "../../stores/modal"; import { useRouter, useRoute } from "vue-router"; import { useNotificationStore } from "../../stores/notifications-primevue"; +import { useCompanyStore } from "../../stores/company"; import TodoChart from "../common/TodoChart.vue"; import { Calendar, Community, Hammer, PathArrowSolid, Clock, Shield, ShieldSearch, ClipboardCheck, DoubleCheck, CreditCard, CardNoAccess, ChatBubbleQuestion, Edit, -WateringSoil, Soil, Truck, SoilAlt } from "@iconoir/vue"; +WateringSoil, Soil, Truck, SoilAlt, +Filter} from "@iconoir/vue"; const notifications = useNotificationStore(); const loadingStore = useLoadingStore(); @@ -41,6 +43,7 @@ const filtersStore = useFiltersStore(); const modalStore = useModalStore(); const router = useRouter(); const route = useRoute(); +const companyStore = useCompanyStore(); const tableData = ref([]); const totalRecords = ref(0); @@ -50,10 +53,26 @@ const currentWeekParams = ref({}); const chartLoading = ref(true); // Start with loading state const lookup = route.query.lookup; +const lastLazyLoadEvent = ref(null); + +// Watch for company changes to reload data +watch( + () => companyStore.currentCompany, + async () => { + console.log("Company changed, reloading client data..."); + if (lastLazyLoadEvent.value) { + await handleLazyLoad(lastLazyLoadEvent.value); + } + // Also refresh status counts + await refreshStatusCounts(); + } +); // Computed property to get current filters for the chart const currentFilters = computed(() => { - return filtersStore.getTableFilters("clients"); + filters = { ...filtersStore.getTableFilters("clients"), + company: { value: companyStore.currentCompany, matchMode: FilterMatchMode.CONTAINS} + }; }); // Handle week change from chart @@ -220,6 +239,7 @@ const tableActions = [ // Handle lazy loading events from DataTable const handleLazyLoad = async (event) => { console.log("Clients page - handling lazy load:", event); + lastLazyLoadEvent.value = event; try { isLoading.value = true; @@ -263,8 +283,9 @@ const handleLazyLoad = async (event) => { filters, sortingArray, }); + filters["company"] = { value: companyStore.currentCompany, matchMode: FilterMatchMode.CONTAINS}; - const result = await Api.getPaginatedClientDetails( + const result = await Api.getPaginatedClientDetailsV2( paginationParams, filters, sortingArray, diff --git a/frontend/src/components/pages/Job.vue b/frontend/src/components/pages/Job.vue index 65e33a1..5b500a6 100644 --- a/frontend/src/components/pages/Job.vue +++ b/frontend/src/components/pages/Job.vue @@ -66,7 +66,7 @@ const notifications = useNotificationStore(); const route = useRoute(); -const jobIdQuery = computed(() => route.query.jobId || ""); +const jobIdQuery = computed(() => route.query.name || ""); const isNew = computed(() => route.query.new === "true"); const tableData = ref([]); diff --git a/frontend/src/components/pages/Jobs.vue b/frontend/src/components/pages/Jobs.vue index 9d116ef..879691e 100644 --- a/frontend/src/components/pages/Jobs.vue +++ b/frontend/src/components/pages/Jobs.vue @@ -257,7 +257,7 @@ const handleLazyLoad = async (event) => { const handleRowClick = (event) => { const rowData = event.data; - router.push(`/job?jobId=${rowData.name}`); + router.push(`/job?name=${rowData.name}`); } // Load initial data diff --git a/frontend/src/components/pages/Tasks.vue b/frontend/src/components/pages/Tasks.vue index 0e7a9ec..cd18d34 100644 --- a/frontend/src/components/pages/Tasks.vue +++ b/frontend/src/components/pages/Tasks.vue @@ -61,7 +61,7 @@ const currentFilters = computed(() => { const columns = [ { label: "Task", fieldName: "subject", type: "text", sortable: true, filterable: true, - filterInputID: "subjectFilterId" }, + filterInputID: "subjectFilterId", defaultValue: subject || null }, { label: "Job", fieldName: "project", type: "link", sortable: true, onLinkClick: (link, rowData) => handleProjectClick(link, rowData) }, @@ -220,11 +220,6 @@ watch(showCompleted, () => { // Load initial data onMounted(async () => { - if (subject) { - const inputElement = document.getElementById(`filter-subject`); - inputElement.text = subject; - } - notifications.addWarning("Tasks page coming soon"); // Initialize pagination and filters paginationStore.initializeTablePagination("tasks", { rows: 10 }); filtersStore.initializeTableFilters("tasks", columns); @@ -236,11 +231,8 @@ onMounted(async () => { const initialSorting = filtersStore.getTableSorting("tasks"); if (subject) { - console.log(subject); - console.log(initialFilters); - console.log(initialFilters.subject); + console.log("Setting subject filter from query param:", subject); initialFilters.subject.value = subject; - //initialFilters = {...initialFilters, subject: {value: subject, match_mode: "contains"}}; } const optionsResult = await Api.getTaskStatusOptions(); @@ -255,6 +247,8 @@ onMounted(async () => { sortOrder: initialSorting.order || initialPagination.sortOrder, filters: initialFilters, }); + + notifications.addWarning("Tasks page coming soon"); });