updates for company effects

This commit is contained in:
Casey 2026-01-20 00:58:03 -06:00
parent 98ec082394
commit 7710a7c8fe
22 changed files with 941 additions and 186 deletions

View File

@ -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:

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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
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
)

View File

@ -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
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 }
)

View File

@ -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": "",

View File

@ -0,0 +1 @@
[]

View File

@ -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"]
]
}
]

View File

@ -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"
)
]
}

View File

@ -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;
}

View File

@ -3,11 +3,8 @@
<Tabs value="0">
<TabList>
<Tab value="0">Bids</Tab>
<Tab value="1">Install</Tab>
<Tab value="1">Projects</Tab>
<Tab value="2">Service</Tab>
<Tab value="3">Lowe Fencing</Tab>
<Tab value="4">Daniel's Landscaping</Tab>
<Tab value="5">Nuco Yardcare</Tab>
<Tab value="6">Warranties</Tab>
</TabList>
<TabPanels>
@ -22,21 +19,6 @@
<p>Service feature coming soon!</p>
</div>
</TabPanel>
<TabPanel header="Lowe Fencing" value="3">
<div class="coming-soon">
<p>Lowe Fencing calendar coming soon!</p>
</div>
</TabPanel>
<TabPanel header="Daniel's Landscaping" value="4">
<div class="coming-soon">
<p>Daniel's Calendar coming soon!</p>
</div>
</TabPanel>
<TabPanel header="Nuco Yardcare" value="5">
<div class="coming-soon">
<p>Nuco calendar coming soon!</p>
</div>
</TabPanel>
<TabPanel header="Warranties" value="6">
<div class="coming-soon">
<p>Warranties Calendar coming soon!</p>
@ -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();
<style scoped>
.calendar-navigation {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.calendar-navigation :deep(.p-tabs) {
height: 100%;
display: flex;
flex-direction: column;
}
.calendar-navigation :deep(.p-tabpanels) {
flex: 1;
overflow: hidden;
}
.calendar-navigation :deep(.p-tabpanel) {
height: 100%;
}
.coming-soon {

View File

@ -226,12 +226,14 @@ import BidMeetingModal from "../../modals/BidMeetingModal.vue";
import MeetingDetailsModal from "../../modals/MeetingDetailsModal.vue";
import { useLoadingStore } from "../../../stores/loading";
import { useNotificationStore } from "../../../stores/notifications-primevue";
import { useCompanyStore } from "../../../stores/company";
import Api from "../../../api";
const route = useRoute();
const router = useRouter();
const loadingStore = useLoadingStore();
const notificationStore = useNotificationStore();
const companyStore = useCompanyStore();
// Query parameters
const isNewMode = computed(() => route.query.new === "true");
@ -816,7 +818,7 @@ const handleDropToUnscheduled = async (event) => {
const loadUnscheduledMeetings = async () => {
loadingStore.setLoading(true);
try {
const result = await Api.getUnscheduledBidMeetings();
const result = await Api.getUnscheduledBidMeetings(companyStore.currentCompany);
// Ensure we always have an array
unscheduledMeetings.value = Array.isArray(result) ? result : [];
console.log("Loaded unscheduled meetings:", unscheduledMeetings.value);
@ -865,7 +867,7 @@ const loadWeekMeetings = async () => {
// Try to get meetings from API
try {
const apiResult = await Api.getWeekBidMeetings(weekStartStr, weekEndStr);
const apiResult = await Api.getWeekBidMeetings(weekStartStr, weekEndStr, companyStore.currentCompany);
if (Array.isArray(apiResult)) {
// Transform the API data to match what the calendar expects
meetings.value = apiResult
@ -1085,6 +1087,15 @@ watch(currentWeekStart, () => {
loadWeekMeetings();
});
// Watch for company changes
watch(
() => companyStore.currentCompany,
async () => {
await loadWeekMeetings();
await loadUnscheduledMeetings();
}
);
watch(
() => route.query.new,
(newVal) => {
@ -1098,9 +1109,10 @@ watch(
<style scoped>
.schedule-bid-container {
padding: 20px;
height: 100vh;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
@ -1110,6 +1122,7 @@ watch(
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
}
.header-controls {
@ -1150,9 +1163,9 @@ watch(
padding: 0 16px;
display: flex;
flex-direction: column;
overflow-y: auto;
max-height: calc(100vh - 150px);
overflow: hidden;
transition: width 0.3s ease;
flex-shrink: 0;
}
.sidebar.collapsed {
@ -1182,11 +1195,13 @@ watch(
.sidebar-header h4 {
font-size: 1.1em;
margin: 0;
flex-shrink: 0;
}
.unscheduled-meetings-list {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 0;
}

View File

@ -1,7 +1,7 @@
<template>
<div class="calendar-container">
<div class="calendar-header">
<h2>Daily Schedule - Installs</h2>
<h2>Daily Schedule - {{ companyStore.currentCompany }}</h2>
<div class="header-controls">
<v-btn
@click="previousDay"
@ -26,6 +26,67 @@
<v-btn @click="goToToday" variant="outlined" size="small" class="ml-4"
>Today</v-btn
>
<v-menu
v-model="showTemplateMenu"
:close-on-content-click="false"
location="bottom"
>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
variant="outlined"
size="small"
class="ml-4"
>
<v-icon left size="small">mdi-file-document-multiple</v-icon>
Project Template ({{ selectedProjectTemplates.length }})
<v-icon right size="small">mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-card min-width="300" max-width="400">
<v-card-title class="text-subtitle-1 py-2">
Select Project Templates
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-2">
<v-list density="compact">
<v-list-item @click="toggleAllTemplates">
<template v-slot:prepend>
<v-checkbox-btn
:model-value="selectedProjectTemplates.length === projectTemplates.length"
:indeterminate="selectedProjectTemplates.length > 0 && selectedProjectTemplates.length < projectTemplates.length"
></v-checkbox-btn>
</template>
<v-list-item-title>Select All</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item
v-for="template in projectTemplates"
:key="template.name"
@click="toggleTemplate(template.name)"
>
<template v-slot:prepend>
<v-checkbox-btn
:model-value="selectedProjectTemplates.includes(template.name)"
></v-checkbox-btn>
</template>
<v-list-item-title>{{ template.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
variant="flat"
size="small"
@click="applyTemplateFilter"
>
Apply
</v-btn>
</v-card-actions>
</v-card>
</v-menu>
<v-menu
v-model="showForemenMenu"
:close-on-content-click="false"
@ -303,14 +364,41 @@
import { ref, onMounted, computed, watch } from "vue";
import Api from "../../../api";
import { useNotificationStore } from "../../../stores/notifications-primevue";
import { useCompanyStore } from "../../../stores/company";
const notifications = useNotificationStore();
const companyStore = useCompanyStore()
// Reactive data
const services = ref([]);
const currentDate = ref(new Date().toISOString().split("T")[0]);
const eventDialog = ref(false);
const selectedEvent = ref(null);
const projectTemplates = ref([]);
// Helper function to get crew name from foreman ID
const getCrewNameFromForemanId = (foremanId) => {
if (!foremanId) return null;
// If it's already a crew name format, return it
if (foremanId.startsWith('Crew ')) return foremanId;
// For now, we'll need to map the foreman employee ID to crew names
// This could be enhanced with an API call to get foreman details
return foremanId; // Return as-is for now
};
// Helper function to calculate duration in minutes from start and end times
const calculateDuration = (startTime, endTime) => {
if (!startTime || !endTime) return 120; // Default 2 hours
const [startHours, startMinutes] = startTime.split(':').map(Number);
const [endHours, endMinutes] = endTime.split(':').map(Number);
const startTotalMinutes = startHours * 60 + startMinutes;
const endTotalMinutes = endHours * 60 + endMinutes;
return endTotalMinutes - startTotalMinutes;
};
// Drag and drop state
const draggedService = ref(null);
@ -340,14 +428,10 @@ const showForemenMenu = ref(false);
const showDatePicker = ref(false);
const selectedDate = ref(null);
// Computed properties
const scheduledServices = computed(() =>
services.value.filter((service) => service.status === "scheduled" && service.foreman),
);
// Project template filter
const selectedProjectTemplates = ref([]);
const showTemplateMenu = ref(false);
const unscheduledServices = computed(() =>
services.value.filter((service) => service.status === "unscheduled" || !service.foreman),
);
// Daily calendar computed properties
const dayDisplayText = computed(() => {
@ -394,6 +478,15 @@ const timeSlots = computed(() => {
return slots;
});
// Computed properties
const scheduledServices = computed(() =>
services.value.filter((service) => service.status === "scheduled" && service.foreman),
);
const unscheduledServices = computed(() =>
services.value.filter((service) => service.status === "unscheduled" || !service.foreman),
);
// Methods
const getUnscheduledCount = () => unscheduledServices.value.length;
@ -551,6 +644,31 @@ const toggleForeman = (foremanId) => {
}
};
// Project template selection methods
const toggleAllTemplates = () => {
if (selectedProjectTemplates.value.length === projectTemplates.value.length) {
// Deselect all
selectedProjectTemplates.value = [];
} else {
// Select all
selectedProjectTemplates.value = projectTemplates.value.map(t => t.name);
}
};
const toggleTemplate = (templateName) => {
const index = selectedProjectTemplates.value.indexOf(templateName);
if (index > -1) {
selectedProjectTemplates.value.splice(index, 1);
} else {
selectedProjectTemplates.value.push(templateName);
}
};
const applyTemplateFilter = async () => {
showTemplateMenu.value = false;
await fetchProjects(currentDate.value);
};
// Date picker methods
const onDateSelected = (date) => {
if (date) {
@ -742,10 +860,12 @@ const handleDrop = async (event, foremanId, time) => {
try {
await Api.upsertJob({
id: draggedService.value.id,
scheduledDate: currentDate.value,
foreman: foreman.name
expectedStartDate: currentDate.value,
expectedStartTime: time,
customForeman: foreman.name // This should ideally be the foreman employee ID
});
notifications.addSuccess("Job scheduled successfully");
notifications.addWarning("This feature is currently still in development. This job has not actually been scheduled yet.");
} catch (error) {
console.error("Error scheduling job:", error);
notifications.addError("Failed to schedule job");
@ -793,8 +913,9 @@ const handleUnscheduledDrop = async (event) => {
try {
await Api.upsertJob({
id: draggedService.value.id,
scheduledDate: null,
foreman: null
expectedStartDate: null,
expectedStartTime: null,
customForeman: null
});
notifications.addSuccess("Job unscheduled successfully");
} catch (error) {
@ -812,31 +933,116 @@ const handleUnscheduledDrop = async (event) => {
const fetchProjects = async (date) => {
try {
const data = await Api.getInstallProjects(date, date);
services.value = data;
console.log("Loaded install projects:", data);
const data = await Api.getJobsForCalendar(date, companyStore.currentCompany, selectedProjectTemplates.value);
// Transform the API response into the format the component expects
const transformedServices = [];
// Process scheduled projects
if (data.projects && Array.isArray(data.projects)) {
data.projects.forEach(project => {
const crewName = getCrewNameFromForemanId(project.customForeman);
const duration = calculateDuration(project.expectedStartTime, project.expectedEndTime);
transformedServices.push({
id: project.name,
title: project.projectName || project.jobAddress || 'Unnamed Project',
serviceType: project.projectName || project.jobAddress || 'Install Project',
customer: project.customer || 'Unknown Customer',
address: project.jobAddress || project.customInstallationAddress || '',
scheduledDate: project.expectedStartDate || date,
scheduledTime: project.expectedStartTime || '08:00',
duration: duration,
foreman: crewName,
priority: (project.priority || 'Medium').toLowerCase(),
estimatedCost: project.totalSalesAmount || project.estimatedCosting || 0,
notes: project.notes || '',
status: 'scheduled'
});
});
}
// Process unscheduled projects
if (data.unscheduledProjects && Array.isArray(data.unscheduledProjects)) {
data.unscheduledProjects.forEach(project => {
const duration = calculateDuration(project.expectedStartTime, project.expectedEndTime);
transformedServices.push({
id: project.name,
title: project.projectName || project.jobAddress || 'Unnamed Project',
serviceType: project.projectName || project.jobAddress || 'Install Project',
customer: project.customer || 'Unknown Customer',
address: project.jobAddress || project.customInstallationAddress || '',
scheduledDate: null,
scheduledTime: null,
duration: duration,
foreman: null,
priority: (project.priority || 'Medium').toLowerCase(),
estimatedCost: project.totalSalesAmount || project.estimatedCosting || 0,
notes: project.notes || '',
status: 'unscheduled'
});
});
}
services.value = transformedServices;
console.log("Loaded install projects:", transformedServices);
} catch (error) {
console.error("Error loading install projects:", error);
notifications.addError("Failed to load install projects");
}
};
const fetchForemen = async () => {
// try {
// const data = await Api.getForemen(companyStore.currentCompany);
// foremen.value = data;
// console.log("Loaded foremen:", data);
// } catch (error) {
// console.error("Error loading foremen:", error);
// notifications.addError("Failed to load foremen");
// }
};
const fetchProjectTemplates = async () => {
try {
const data = await Api.getJobTemplates(companyStore.currentCompany);
projectTemplates.value = data;
// Select all templates by default
selectedProjectTemplates.value = data.map(t => t.name);
console.log("Loaded project templates:", data);
}
catch (error) {
console.error("Error loading project templates:", error);
notifications.addError("Failed to load project templates");
}
}
watch(currentDate, async (newDate) => {
await fetchProjects(newDate);
});
watch(companyStore, async (newCompany) => {
await fetchForemen();
await fetchProjectTemplates();
await fetchProjects(currentDate.value);
}, { deep: true });
// Lifecycle
onMounted(async () => {
await fetchProjects(currentDate.value);
await fetchForemen();
await fetchProjectTemplates();
});
</script>
<style scoped>
.calendar-container {
padding: 20px;
height: 100vh;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.calendar-header {
@ -846,6 +1052,7 @@ onMounted(async () => {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--surface-border);
flex-shrink: 0;
}
.header-controls {
@ -1071,6 +1278,8 @@ onMounted(async () => {
padding-left: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
}
.unscheduled-header {
@ -1078,6 +1287,7 @@ onMounted(async () => {
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-shrink: 0;
}
.unscheduled-header h4 {
@ -1088,7 +1298,7 @@ onMounted(async () => {
.unscheduled-list {
flex: 1;
overflow-y: auto;
max-height: calc(100vh - 200px);
overflow-x: hidden;
transition: all 0.3s ease;
border-radius: 8px;
}

View File

@ -27,7 +27,7 @@
</template>
<!-- SNW Installation Status -->
<div v-if="!isNew && !editMode" class="install-status-section">
<div v-if="!isNew && !editMode && isSNWClient" class="install-status-section">
<InstallStatus
:onsite-meeting-status="snwInstallData.onsiteMeetingStatus"
:estimate-sent-status="snwInstallData.estimateSentStatus"
@ -438,6 +438,13 @@ const fullAddress = computed(() => {
return DataUtils.calculateFullAddress(selectedAddressData.value);
});
// Check if client is associated with Sprinklers Northwest
const isSNWClient = computed(() => {
// if (!props.clientData?.companies || !Array.isArray(props.clientData.companies)) return false;
// return props.clientData.companies.some((c) => c.company === "Sprinklers Northwest");
return companyStore.currentCompany === "Sprinklers Northwest";
});
// Computed data for SNW Install status
const snwInstallData = computed(() => {
if (!selectedAddressData.value) {

View File

@ -783,7 +783,9 @@ const hasExactlyOneRowSelected = computed(() => {
onMounted(() => {
const currentFilters = filtersStore.getTableFilters(props.tableName);
filterableColumns.value.forEach((col) => {
pendingFilters.value[col.fieldName] = currentFilters[col.fieldName]?.value || "";
// Use defaultValue from column definition if provided, otherwise use stored filter value
const storedValue = currentFilters[col.fieldName]?.value || "";
pendingFilters.value[col.fieldName] = col.defaultValue || storedValue;
});
});

View File

@ -1,15 +1,14 @@
<template>
<div
v-if="showOverlay"
class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-[9999] transition-opacity duration-200"
:class="{ 'opacity-100': showOverlay, 'opacity-0 pointer-events-none': !showOverlay }"
class="global-loading-overlay"
>
<div class="bg-white rounded-lg p-6 shadow-xl max-w-sm w-full mx-4 text-center">
<div class="mb-4">
<i class="pi pi-spin pi-spinner text-4xl text-blue-500"></i>
<div class="loading-content">
<div class="spinner-container">
<i class="pi pi-spin pi-spinner"></i>
</div>
<h3 class="text-lg font-semibold text-gray-800 mb-2">Loading</h3>
<p class="text-gray-600">{{ loadingMessage }}</p>
<h3 class="loading-title">Loading</h3>
<p class="loading-message">{{ loadingMessage }}</p>
</div>
</div>
</template>
@ -46,15 +45,51 @@ const loadingMessage = computed(() => {
</script>
<style scoped>
/* Additional styling for better visual appearance */
.bg-opacity-30 {
background-color: rgba(0, 0, 0, 0.3);
.global-loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.2s ease;
}
/* Backdrop blur effect for modern browsers */
@supports (backdrop-filter: blur(4px)) {
.fixed.inset-0 {
backdrop-filter: blur(4px);
}
.loading-content {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 90%;
text-align: center;
}
.spinner-container {
margin-bottom: 1rem;
}
.spinner-container i {
font-size: 3rem;
color: #1976d2;
}
.loading-title {
font-size: 1.25rem;
font-weight: 600;
color: #333;
margin-bottom: 0.5rem;
}
.loading-message {
color: #666;
font-size: 0.95rem;
}
</style>

View File

@ -7,4 +7,7 @@ import CalendarNavigation from '@/components/calendar/CalendarNavigation.vue'
</script>
<style scoped>
:deep(.calendar-navigation) {
height: 100%;
}
</style>

View File

@ -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,

View File

@ -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([]);

View File

@ -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

View File

@ -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");
});
</script>
<style lang="css">