updates for company effects
This commit is contained in:
parent
98ec082394
commit
7710a7c8fe
22 changed files with 941 additions and 186 deletions
|
|
@ -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
|
from custom_ui.services import DbService, ClientService, AddressService, ContactService
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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."""
|
"""Get On-Site Meetings scheduled within a specific week."""
|
||||||
try:
|
try:
|
||||||
meetings = frappe.db.get_all(
|
meetings = frappe.db.get_all(
|
||||||
|
|
@ -12,7 +12,8 @@ def get_week_bid_meetings(week_start, week_end):
|
||||||
fields=["*"],
|
fields=["*"],
|
||||||
filters=[
|
filters=[
|
||||||
["start_time", ">=", week_start],
|
["start_time", ">=", week_start],
|
||||||
["start_time", "<=", week_end]
|
["start_time", "<=", week_end],
|
||||||
|
["company", "=", company]
|
||||||
],
|
],
|
||||||
order_by="start_time asc"
|
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)
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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."""
|
"""Get paginated On-Site Meetings with filtering and sorting support."""
|
||||||
try:
|
try:
|
||||||
print("DEBUG: Raw bid meeting options received:", filters)
|
print("DEBUG: Raw bid meeting options received:", filters)
|
||||||
|
|
@ -53,13 +54,13 @@ def get_bid_meetings(fields=["*"], filters={}):
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_unscheduled_bid_meetings():
|
def get_unscheduled_bid_meetings(company):
|
||||||
"""Get On-Site Meetings that are unscheduled."""
|
"""Get On-Site Meetings that are unscheduled."""
|
||||||
try:
|
try:
|
||||||
meetings = frappe.db.get_all(
|
meetings = frappe.db.get_all(
|
||||||
"On-Site Meeting",
|
"On-Site Meeting",
|
||||||
fields=["*"],
|
fields=["*"],
|
||||||
filters={"status": "Unscheduled"},
|
filters={"status": "Unscheduled", "company": company},
|
||||||
order_by="creation desc"
|
order_by="creation desc"
|
||||||
)
|
)
|
||||||
for meeting in meetings:
|
for meeting in meetings:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import frappe, json
|
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 erpnext.crm.doctype.lead.lead import make_customer
|
||||||
from custom_ui.api.db.addresses import address_exists
|
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
|
from custom_ui.api.db.contacts import check_and_get_contact, create_contact, create_contact_links
|
||||||
|
|
@ -169,6 +169,81 @@ def get_client_v2(client_name):
|
||||||
return build_error_response(str(e), 500)
|
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()
|
@frappe.whitelist()
|
||||||
def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
|
def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||||
"""Get paginated client table data with filtering and sorting support."""
|
"""Get paginated client table data with filtering and sorting support."""
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ def get_job(job_id=""):
|
||||||
project = project.as_dict()
|
project = project.as_dict()
|
||||||
project["job_address"] = address_doc
|
project["job_address"] = address_doc
|
||||||
project["client"] = ClientService.get_client_or_throw(project.customer)
|
project["client"] = ClientService.get_client_or_throw(project.customer)
|
||||||
|
task_names = frappe.get_all("Task", filters={"project": job_id})
|
||||||
|
project["tasks"] = [frappe.get_doc("Task", task_name).as_dict() for task_name in task_names]
|
||||||
return build_success_response(project)
|
return build_success_response(project)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
@ -167,55 +169,32 @@ def upsert_job(data):
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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."""
|
"""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:
|
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
|
# 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.
|
# 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).
|
# 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)
|
project_names = frappe.get_all("Project", pluck="name", filters=filters)
|
||||||
|
print("DEBUG: Found scheduled project names:", project_names)
|
||||||
calendar_events = []
|
unscheduled_project_names = frappe.get_all("Project", pluck="name", filters=unscheduled_filters)
|
||||||
for project in projects:
|
print("DEBUG: Found unscheduled project names:", unscheduled_project_names)
|
||||||
# Determine status
|
projects = [frappe.get_doc("Project", name).as_dict() for name in project_names]
|
||||||
status = "unscheduled"
|
unscheduled_projects = [frappe.get_doc("Project", name).as_dict() for name in unscheduled_project_names]
|
||||||
if project.get("expected_start_date"):
|
return build_success_response({ "projects": projects, "unscheduled_projects": unscheduled_projects })
|
||||||
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}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"status": "error", "message": str(e)}
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
@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),
|
|
||||||
|
|
@ -229,3 +229,7 @@ def build_history_entries(comments, versions):
|
||||||
# Sort by timestamp descending
|
# Sort by timestamp descending
|
||||||
history.sort(key=lambda x: x["timestamp"], reverse=True)
|
history.sort(key=lambda x: x["timestamp"], reverse=True)
|
||||||
return history
|
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
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,26 @@ def after_insert(doc, method):
|
||||||
def before_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
|
# 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
|
||||||
|
)
|
||||||
|
|
@ -1,7 +1,24 @@
|
||||||
import frappe
|
import frappe
|
||||||
|
from custom_ui.services import AddressService, ClientService
|
||||||
|
|
||||||
def before_insert(doc, method):
|
def before_insert(doc, method):
|
||||||
"""Set values before inserting a Task."""
|
"""Set values before inserting a Task."""
|
||||||
|
print("DEBUG: Before Insert Triggered for Task")
|
||||||
project_doc = frappe.get_doc("Project", doc.project)
|
project_doc = frappe.get_doc("Project", doc.project)
|
||||||
if project_doc.custom_installation_address:
|
doc.project_template = project_doc.project_template
|
||||||
doc.custom_property = project_doc.custom_installation_address
|
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 }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
"documentation_url": null,
|
"documentation_url": null,
|
||||||
"fetch_from": null,
|
"fetch_from": null,
|
||||||
"fetch_if_empty": 0,
|
"fetch_if_empty": 0,
|
||||||
"fieldname": "company",
|
"fieldname": "task",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"hide_border": 0,
|
"hide_border": 0,
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
"in_preview": 0,
|
"in_preview": 0,
|
||||||
"in_standard_filter": 0,
|
"in_standard_filter": 0,
|
||||||
"is_virtual": 0,
|
"is_virtual": 0,
|
||||||
"label": "Company",
|
"label": "Task",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"link_filters": null,
|
"link_filters": null,
|
||||||
"make_attachment_public": 0,
|
"make_attachment_public": 0,
|
||||||
|
|
@ -68,8 +68,8 @@
|
||||||
"non_negative": 0,
|
"non_negative": 0,
|
||||||
"oldfieldname": null,
|
"oldfieldname": null,
|
||||||
"oldfieldtype": null,
|
"oldfieldtype": null,
|
||||||
"options": "Company",
|
"options": "Task",
|
||||||
"parent": "Lead Company Link",
|
"parent": "Customer Task Link",
|
||||||
"parentfield": "fields",
|
"parentfield": "fields",
|
||||||
"parenttype": "DocType",
|
"parenttype": "DocType",
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
|
|
@ -93,6 +93,70 @@
|
||||||
"trigger": null,
|
"trigger": null,
|
||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"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,
|
"force_re_route_to_default_view": 0,
|
||||||
|
|
@ -114,10 +178,228 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
"migration_hash": null,
|
||||||
"modified": "2026-01-15 00:40:39.197431",
|
"modified": "2026-01-19 18:10:16.782664",
|
||||||
"module": "Custom",
|
"module": "Custom UI",
|
||||||
"name": "Lead Company Link",
|
"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": "",
|
"naming_rule": "",
|
||||||
"nsm_parent_field": null,
|
"nsm_parent_field": null,
|
||||||
"parent_node": null,
|
"parent_node": null,
|
||||||
|
|
@ -268,8 +550,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.521684",
|
"modified": "2026-01-19 20:52:17.097017",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Lead Companies Link",
|
"name": "Lead Companies Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -486,8 +768,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.576521",
|
"modified": "2026-01-19 20:52:17.150584",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Address Project Link",
|
"name": "Address Project Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -704,8 +986,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.628136",
|
"modified": "2026-01-19 20:52:17.203403",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Address Quotation Link",
|
"name": "Address Quotation Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -922,8 +1204,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.681893",
|
"modified": "2026-01-19 20:52:17.255846",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Address On-Site Meeting Link",
|
"name": "Address On-Site Meeting Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -1140,8 +1422,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.737017",
|
"modified": "2026-01-19 20:52:17.309600",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Address Sales Order Link",
|
"name": "Address Sales Order Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -1294,8 +1576,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.787995",
|
"modified": "2026-01-19 20:52:17.361237",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Contact Address Link",
|
"name": "Contact Address Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -1448,8 +1730,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.837721",
|
"modified": "2026-01-19 20:52:17.412683",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Lead On-Site Meeting Link",
|
"name": "Lead On-Site Meeting Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -2050,8 +2332,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.906370",
|
"modified": "2026-01-19 20:52:17.483924",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Quotation Template",
|
"name": "Quotation Template",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -2548,8 +2830,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:34.977831",
|
"modified": "2026-01-19 20:52:17.558008",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Quotation Template Item",
|
"name": "Quotation Template Item",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -2702,8 +2984,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.031029",
|
"modified": "2026-01-19 20:52:17.609372",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Customer Company Link",
|
"name": "Customer Company Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -2856,8 +3138,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.084461",
|
"modified": "2026-01-19 20:52:17.660893",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Customer Address Link",
|
"name": "Customer Address Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -3010,8 +3292,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.135851",
|
"modified": "2026-01-19 20:52:17.712878",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Customer Contact Link",
|
"name": "Customer Contact Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -3164,8 +3446,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.184768",
|
"modified": "2026-01-19 20:52:17.765849",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Address Contact Link",
|
"name": "Address Contact Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -3318,8 +3600,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.236428",
|
"modified": "2026-01-19 20:52:17.818352",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Customer On-Site Meeting Link",
|
"name": "Customer On-Site Meeting Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -3472,8 +3754,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.287145",
|
"modified": "2026-01-19 20:52:17.870984",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Customer Project Link",
|
"name": "Customer Project Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -3626,8 +3908,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.338967",
|
"modified": "2026-01-19 20:52:17.922695",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Customer Quotation Link",
|
"name": "Customer Quotation Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -3780,8 +4062,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.388711",
|
"modified": "2026-01-19 20:52:17.975165",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Customer Sales Order Link",
|
"name": "Customer Sales Order Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -3934,8 +4216,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.441876",
|
"modified": "2026-01-19 20:52:18.027046",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Lead Address Link",
|
"name": "Lead Address Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -4088,8 +4370,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.492936",
|
"modified": "2026-01-19 20:52:18.078476",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Lead Contact Link",
|
"name": "Lead Contact Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -4242,8 +4524,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.545465",
|
"modified": "2026-01-19 20:52:18.170095",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Lead Quotation Link",
|
"name": "Lead Quotation Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -4396,8 +4678,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": "7c3c71cf20b258daa783e541cb045a4b",
|
"migration_hash": "0df0ede31f640435231ba887f40eca91",
|
||||||
"modified": "2026-01-16 04:11:35.604415",
|
"modified": "2026-01-19 20:52:18.238066",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Address Company Link",
|
"name": "Address Company Link",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
|
||||||
1
custom_ui/fixtures/property_setter.json
Normal file
1
custom_ui/fixtures/property_setter.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
||||||
|
|
@ -181,10 +181,13 @@ doc_events = {
|
||||||
},
|
},
|
||||||
"Project": {
|
"Project": {
|
||||||
"before_insert": "custom_ui.events.jobs.before_insert",
|
"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": {
|
"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 Contact Link",
|
||||||
"Address Company Link",
|
"Address Company Link",
|
||||||
"Contact Address Link",
|
"Contact Address Link",
|
||||||
|
"Address Task Link",
|
||||||
|
"Customer Task Link"
|
||||||
]]
|
]]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -249,7 +254,15 @@ fixtures = [
|
||||||
["dt", "=", "Project Template"],
|
["dt", "=", "Project Template"],
|
||||||
["fieldname", "=", "company"]
|
["fieldname", "=", "company"]
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": "Property Setter",
|
||||||
|
"filters": [
|
||||||
|
["doc_type", "=", "Lead"],
|
||||||
|
["doc_type", "=", "Project"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ def after_migrate():
|
||||||
frappe.clear_cache(doctype=doctype)
|
frappe.clear_cache(doctype=doctype)
|
||||||
frappe.reload_doctype(doctype)
|
frappe.reload_doctype(doctype)
|
||||||
|
|
||||||
update_address_fields()
|
# update_address_fields()
|
||||||
# build_frontend()
|
# build_frontend()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -146,6 +146,13 @@ def add_custom_fields():
|
||||||
fieldtype="Link",
|
fieldtype="Link",
|
||||||
options="Contact",
|
options="Contact",
|
||||||
insert_after="contacts"
|
insert_after="contacts"
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
fieldname="tasks",
|
||||||
|
label="Tasks",
|
||||||
|
fieldtype="Table",
|
||||||
|
options="Customer Task Link",
|
||||||
|
insert_after="projects"
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
"Lead": [
|
"Lead": [
|
||||||
|
|
@ -327,6 +334,13 @@ def add_custom_fields():
|
||||||
fieldtype="Table",
|
fieldtype="Table",
|
||||||
options="Address Company Link",
|
options="Address Company Link",
|
||||||
insert_after="contacts"
|
insert_after="contacts"
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
fieldname="tasks",
|
||||||
|
label="Tasks",
|
||||||
|
fieldtype="Table",
|
||||||
|
options="Address Task Link",
|
||||||
|
insert_after="projects"
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
"Contact": [
|
"Contact": [
|
||||||
|
|
@ -539,6 +553,37 @@ def add_custom_fields():
|
||||||
options="Customer",
|
options="Customer",
|
||||||
insert_after="job_address",
|
insert_after="job_address",
|
||||||
description="The customer for whom the project is being executed."
|
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": [
|
"Project Template": [
|
||||||
|
|
@ -550,6 +595,15 @@ def add_custom_fields():
|
||||||
insert_after="project_type",
|
insert_after="project_type",
|
||||||
description="The company associated with this project template."
|
description="The company associated with this project template."
|
||||||
)
|
)
|
||||||
|
],
|
||||||
|
"Task": [
|
||||||
|
dict(
|
||||||
|
fieldname="project_template",
|
||||||
|
label="Project Template",
|
||||||
|
fieldtype="Link",
|
||||||
|
options="Project Template",
|
||||||
|
insert_after="project"
|
||||||
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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_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_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_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_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";
|
const FRAPPE_GET_JOB_TEMPLATES_METHOD = "custom_ui.api.db.jobs.get_job_templates";
|
||||||
// Task methods
|
// Task methods
|
||||||
const FRAPPE_GET_TASKS_METHOD = "custom_ui.api.db.tasks.get_tasks_table_data";
|
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_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_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_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_METHOD = "custom_ui.api.db.clients.get_client_v2";
|
||||||
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
|
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 });
|
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
|
* Get paginated client data with filtering and sorting
|
||||||
* @param {Object} paginationParams - Pagination parameters from store
|
* @param {Object} paginationParams - Pagination parameters from store
|
||||||
|
|
@ -138,9 +152,10 @@ class Api {
|
||||||
// ON-SITE MEETING METHODS
|
// ON-SITE MEETING METHODS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
static async getUnscheduledBidMeetings() {
|
static async getUnscheduledBidMeetings(company) {
|
||||||
return await this.request(
|
return await this.request(
|
||||||
"custom_ui.api.db.bid_meetings.get_unscheduled_bid_meetings",
|
"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 });
|
return await this.request(FRAPPE_GET_ONSITE_MEETINGS_METHOD, { fields, filters });
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getWeekBidMeetings(weekStart, weekEnd) {
|
static async getWeekBidMeetings(weekStart, weekEnd, company) {
|
||||||
return await this.request(FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD, { weekStart, weekEnd });
|
return await this.request(FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD, { weekStart, weekEnd, company });
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateBidMeeting(name, data) {
|
static async updateBidMeeting(name, data) {
|
||||||
|
|
@ -300,6 +315,10 @@ class Api {
|
||||||
return result;
|
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) {
|
static async getJob(jobName) {
|
||||||
if (frappe.db.exists("Project", jobName)) {
|
if (frappe.db.exists("Project", jobName)) {
|
||||||
const result = await this.request(FRAPPE_GET_JOB_METHOD, { jobId: 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);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,8 @@
|
||||||
<Tabs value="0">
|
<Tabs value="0">
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value="0">Bids</Tab>
|
<Tab value="0">Bids</Tab>
|
||||||
<Tab value="1">Install</Tab>
|
<Tab value="1">Projects</Tab>
|
||||||
<Tab value="2">Service</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>
|
<Tab value="6">Warranties</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
|
|
@ -22,21 +19,6 @@
|
||||||
<p>Service feature coming soon!</p>
|
<p>Service feature coming soon!</p>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</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">
|
<TabPanel header="Warranties" value="6">
|
||||||
<div class="coming-soon">
|
<div class="coming-soon">
|
||||||
<p>Warranties Calendar coming soon!</p>
|
<p>Warranties Calendar coming soon!</p>
|
||||||
|
|
@ -56,7 +38,7 @@ import TabPanel from 'primevue/tabpanel';
|
||||||
import TabPanels from 'primevue/tabpanels';
|
import TabPanels from 'primevue/tabpanels';
|
||||||
import ScheduleBid from '../calendar/bids/ScheduleBid.vue';
|
import ScheduleBid from '../calendar/bids/ScheduleBid.vue';
|
||||||
import JobsCalendar from '../calendar/jobs/JobsCalendar.vue';
|
import 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';
|
import { useNotificationStore } from '../../stores/notifications-primevue';
|
||||||
|
|
||||||
const notifications = useNotificationStore();
|
const notifications = useNotificationStore();
|
||||||
|
|
@ -65,6 +47,24 @@ const notifications = useNotificationStore();
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.calendar-navigation {
|
.calendar-navigation {
|
||||||
width: 100%;
|
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 {
|
.coming-soon {
|
||||||
|
|
|
||||||
|
|
@ -226,12 +226,14 @@ import BidMeetingModal from "../../modals/BidMeetingModal.vue";
|
||||||
import MeetingDetailsModal from "../../modals/MeetingDetailsModal.vue";
|
import MeetingDetailsModal from "../../modals/MeetingDetailsModal.vue";
|
||||||
import { useLoadingStore } from "../../../stores/loading";
|
import { useLoadingStore } from "../../../stores/loading";
|
||||||
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
||||||
|
import { useCompanyStore } from "../../../stores/company";
|
||||||
import Api from "../../../api";
|
import Api from "../../../api";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loadingStore = useLoadingStore();
|
const loadingStore = useLoadingStore();
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
|
const companyStore = useCompanyStore();
|
||||||
|
|
||||||
// Query parameters
|
// Query parameters
|
||||||
const isNewMode = computed(() => route.query.new === "true");
|
const isNewMode = computed(() => route.query.new === "true");
|
||||||
|
|
@ -816,7 +818,7 @@ const handleDropToUnscheduled = async (event) => {
|
||||||
const loadUnscheduledMeetings = async () => {
|
const loadUnscheduledMeetings = async () => {
|
||||||
loadingStore.setLoading(true);
|
loadingStore.setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await Api.getUnscheduledBidMeetings();
|
const result = await Api.getUnscheduledBidMeetings(companyStore.currentCompany);
|
||||||
// Ensure we always have an array
|
// Ensure we always have an array
|
||||||
unscheduledMeetings.value = Array.isArray(result) ? result : [];
|
unscheduledMeetings.value = Array.isArray(result) ? result : [];
|
||||||
console.log("Loaded unscheduled meetings:", unscheduledMeetings.value);
|
console.log("Loaded unscheduled meetings:", unscheduledMeetings.value);
|
||||||
|
|
@ -865,7 +867,7 @@ const loadWeekMeetings = async () => {
|
||||||
|
|
||||||
// Try to get meetings from API
|
// Try to get meetings from API
|
||||||
try {
|
try {
|
||||||
const apiResult = await Api.getWeekBidMeetings(weekStartStr, weekEndStr);
|
const apiResult = await Api.getWeekBidMeetings(weekStartStr, weekEndStr, companyStore.currentCompany);
|
||||||
if (Array.isArray(apiResult)) {
|
if (Array.isArray(apiResult)) {
|
||||||
// Transform the API data to match what the calendar expects
|
// Transform the API data to match what the calendar expects
|
||||||
meetings.value = apiResult
|
meetings.value = apiResult
|
||||||
|
|
@ -1085,6 +1087,15 @@ watch(currentWeekStart, () => {
|
||||||
loadWeekMeetings();
|
loadWeekMeetings();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch for company changes
|
||||||
|
watch(
|
||||||
|
() => companyStore.currentCompany,
|
||||||
|
async () => {
|
||||||
|
await loadWeekMeetings();
|
||||||
|
await loadUnscheduledMeetings();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.new,
|
() => route.query.new,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
|
|
@ -1098,9 +1109,10 @@ watch(
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.schedule-bid-container {
|
.schedule-bid-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|
@ -1110,6 +1122,7 @@ watch(
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-controls {
|
.header-controls {
|
||||||
|
|
@ -1150,9 +1163,9 @@ watch(
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
max-height: calc(100vh - 150px);
|
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.collapsed {
|
.sidebar.collapsed {
|
||||||
|
|
@ -1182,11 +1195,13 @@ watch(
|
||||||
.sidebar-header h4 {
|
.sidebar-header h4 {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unscheduled-meetings-list {
|
.unscheduled-meetings-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="calendar-container">
|
<div class="calendar-container">
|
||||||
<div class="calendar-header">
|
<div class="calendar-header">
|
||||||
<h2>Daily Schedule - Installs</h2>
|
<h2>Daily Schedule - {{ companyStore.currentCompany }}</h2>
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="previousDay"
|
@click="previousDay"
|
||||||
|
|
@ -26,6 +26,67 @@
|
||||||
<v-btn @click="goToToday" variant="outlined" size="small" class="ml-4"
|
<v-btn @click="goToToday" variant="outlined" size="small" class="ml-4"
|
||||||
>Today</v-btn
|
>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-menu
|
||||||
v-model="showForemenMenu"
|
v-model="showForemenMenu"
|
||||||
:close-on-content-click="false"
|
:close-on-content-click="false"
|
||||||
|
|
@ -303,14 +364,41 @@
|
||||||
import { ref, onMounted, computed, watch } from "vue";
|
import { ref, onMounted, computed, watch } from "vue";
|
||||||
import Api from "../../../api";
|
import Api from "../../../api";
|
||||||
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
||||||
|
import { useCompanyStore } from "../../../stores/company";
|
||||||
|
|
||||||
const notifications = useNotificationStore();
|
const notifications = useNotificationStore();
|
||||||
|
|
||||||
|
const companyStore = useCompanyStore()
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const services = ref([]);
|
const services = ref([]);
|
||||||
const currentDate = ref(new Date().toISOString().split("T")[0]);
|
const currentDate = ref(new Date().toISOString().split("T")[0]);
|
||||||
const eventDialog = ref(false);
|
const eventDialog = ref(false);
|
||||||
const selectedEvent = ref(null);
|
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
|
// Drag and drop state
|
||||||
const draggedService = ref(null);
|
const draggedService = ref(null);
|
||||||
|
|
@ -340,14 +428,10 @@ const showForemenMenu = ref(false);
|
||||||
const showDatePicker = ref(false);
|
const showDatePicker = ref(false);
|
||||||
const selectedDate = ref(null);
|
const selectedDate = ref(null);
|
||||||
|
|
||||||
// Computed properties
|
// Project template filter
|
||||||
const scheduledServices = computed(() =>
|
const selectedProjectTemplates = ref([]);
|
||||||
services.value.filter((service) => service.status === "scheduled" && service.foreman),
|
const showTemplateMenu = ref(false);
|
||||||
);
|
|
||||||
|
|
||||||
const unscheduledServices = computed(() =>
|
|
||||||
services.value.filter((service) => service.status === "unscheduled" || !service.foreman),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Daily calendar computed properties
|
// Daily calendar computed properties
|
||||||
const dayDisplayText = computed(() => {
|
const dayDisplayText = computed(() => {
|
||||||
|
|
@ -394,6 +478,15 @@ const timeSlots = computed(() => {
|
||||||
return slots;
|
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
|
// Methods
|
||||||
const getUnscheduledCount = () => unscheduledServices.value.length;
|
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
|
// Date picker methods
|
||||||
const onDateSelected = (date) => {
|
const onDateSelected = (date) => {
|
||||||
if (date) {
|
if (date) {
|
||||||
|
|
@ -742,10 +860,12 @@ const handleDrop = async (event, foremanId, time) => {
|
||||||
try {
|
try {
|
||||||
await Api.upsertJob({
|
await Api.upsertJob({
|
||||||
id: draggedService.value.id,
|
id: draggedService.value.id,
|
||||||
scheduledDate: currentDate.value,
|
expectedStartDate: currentDate.value,
|
||||||
foreman: foreman.name
|
expectedStartTime: time,
|
||||||
|
customForeman: foreman.name // This should ideally be the foreman employee ID
|
||||||
});
|
});
|
||||||
notifications.addSuccess("Job scheduled successfully");
|
notifications.addSuccess("Job scheduled successfully");
|
||||||
|
notifications.addWarning("This feature is currently still in development. This job has not actually been scheduled yet.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error scheduling job:", error);
|
console.error("Error scheduling job:", error);
|
||||||
notifications.addError("Failed to schedule job");
|
notifications.addError("Failed to schedule job");
|
||||||
|
|
@ -793,8 +913,9 @@ const handleUnscheduledDrop = async (event) => {
|
||||||
try {
|
try {
|
||||||
await Api.upsertJob({
|
await Api.upsertJob({
|
||||||
id: draggedService.value.id,
|
id: draggedService.value.id,
|
||||||
scheduledDate: null,
|
expectedStartDate: null,
|
||||||
foreman: null
|
expectedStartTime: null,
|
||||||
|
customForeman: null
|
||||||
});
|
});
|
||||||
notifications.addSuccess("Job unscheduled successfully");
|
notifications.addSuccess("Job unscheduled successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -812,31 +933,116 @@ const handleUnscheduledDrop = async (event) => {
|
||||||
|
|
||||||
const fetchProjects = async (date) => {
|
const fetchProjects = async (date) => {
|
||||||
try {
|
try {
|
||||||
const data = await Api.getInstallProjects(date, date);
|
const data = await Api.getJobsForCalendar(date, companyStore.currentCompany, selectedProjectTemplates.value);
|
||||||
services.value = data;
|
|
||||||
console.log("Loaded install projects:", data);
|
// 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) {
|
} catch (error) {
|
||||||
console.error("Error loading install projects:", error);
|
console.error("Error loading install projects:", error);
|
||||||
notifications.addError("Failed to load install projects");
|
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) => {
|
watch(currentDate, async (newDate) => {
|
||||||
await fetchProjects(newDate);
|
await fetchProjects(newDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(companyStore, async (newCompany) => {
|
||||||
|
await fetchForemen();
|
||||||
|
await fetchProjectTemplates();
|
||||||
|
await fetchProjects(currentDate.value);
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchProjects(currentDate.value);
|
await fetchProjects(currentDate.value);
|
||||||
|
await fetchForemen();
|
||||||
|
await fetchProjectTemplates();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.calendar-container {
|
.calendar-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-header {
|
.calendar-header {
|
||||||
|
|
@ -846,6 +1052,7 @@ onMounted(async () => {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
border-bottom: 1px solid var(--surface-border);
|
border-bottom: 1px solid var(--surface-border);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-controls {
|
.header-controls {
|
||||||
|
|
@ -1071,6 +1278,8 @@ onMounted(async () => {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unscheduled-header {
|
.unscheduled-header {
|
||||||
|
|
@ -1078,6 +1287,7 @@ onMounted(async () => {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unscheduled-header h4 {
|
.unscheduled-header h4 {
|
||||||
|
|
@ -1088,7 +1298,7 @@ onMounted(async () => {
|
||||||
.unscheduled-list {
|
.unscheduled-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: calc(100vh - 200px);
|
overflow-x: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- SNW Installation Status -->
|
<!-- SNW Installation Status -->
|
||||||
<div v-if="!isNew && !editMode" class="install-status-section">
|
<div v-if="!isNew && !editMode && isSNWClient" class="install-status-section">
|
||||||
<InstallStatus
|
<InstallStatus
|
||||||
:onsite-meeting-status="snwInstallData.onsiteMeetingStatus"
|
:onsite-meeting-status="snwInstallData.onsiteMeetingStatus"
|
||||||
:estimate-sent-status="snwInstallData.estimateSentStatus"
|
:estimate-sent-status="snwInstallData.estimateSentStatus"
|
||||||
|
|
@ -438,6 +438,13 @@ const fullAddress = computed(() => {
|
||||||
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
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
|
// Computed data for SNW Install status
|
||||||
const snwInstallData = computed(() => {
|
const snwInstallData = computed(() => {
|
||||||
if (!selectedAddressData.value) {
|
if (!selectedAddressData.value) {
|
||||||
|
|
|
||||||
|
|
@ -783,7 +783,9 @@ const hasExactlyOneRowSelected = computed(() => {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
||||||
filterableColumns.value.forEach((col) => {
|
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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="showOverlay"
|
v-if="showOverlay"
|
||||||
class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-[9999] transition-opacity duration-200"
|
class="global-loading-overlay"
|
||||||
:class="{ 'opacity-100': showOverlay, 'opacity-0 pointer-events-none': !showOverlay }"
|
|
||||||
>
|
>
|
||||||
<div class="bg-white rounded-lg p-6 shadow-xl max-w-sm w-full mx-4 text-center">
|
<div class="loading-content">
|
||||||
<div class="mb-4">
|
<div class="spinner-container">
|
||||||
<i class="pi pi-spin pi-spinner text-4xl text-blue-500"></i>
|
<i class="pi pi-spin pi-spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">Loading</h3>
|
<h3 class="loading-title">Loading</h3>
|
||||||
<p class="text-gray-600">{{ loadingMessage }}</p>
|
<p class="loading-message">{{ loadingMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -46,15 +45,51 @@ const loadingMessage = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Additional styling for better visual appearance */
|
.global-loading-overlay {
|
||||||
.bg-opacity-30 {
|
position: fixed;
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
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 */
|
.loading-content {
|
||||||
@supports (backdrop-filter: blur(4px)) {
|
background: white;
|
||||||
.fixed.inset-0 {
|
border-radius: 12px;
|
||||||
backdrop-filter: blur(4px);
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,7 @@ import CalendarNavigation from '@/components/calendar/CalendarNavigation.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
:deep(.calendar-navigation) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ import { useFiltersStore } from "../../stores/filters";
|
||||||
import { useModalStore } from "../../stores/modal";
|
import { useModalStore } from "../../stores/modal";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||||
|
import { useCompanyStore } from "../../stores/company";
|
||||||
import TodoChart from "../common/TodoChart.vue";
|
import TodoChart from "../common/TodoChart.vue";
|
||||||
import { Calendar, Community, Hammer, PathArrowSolid, Clock, Shield, ShieldSearch,
|
import { Calendar, Community, Hammer, PathArrowSolid, Clock, Shield, ShieldSearch,
|
||||||
ClipboardCheck, DoubleCheck, CreditCard, CardNoAccess, ChatBubbleQuestion, Edit,
|
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 notifications = useNotificationStore();
|
||||||
const loadingStore = useLoadingStore();
|
const loadingStore = useLoadingStore();
|
||||||
|
|
@ -41,6 +43,7 @@ const filtersStore = useFiltersStore();
|
||||||
const modalStore = useModalStore();
|
const modalStore = useModalStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const companyStore = useCompanyStore();
|
||||||
|
|
||||||
const tableData = ref([]);
|
const tableData = ref([]);
|
||||||
const totalRecords = ref(0);
|
const totalRecords = ref(0);
|
||||||
|
|
@ -50,10 +53,26 @@ const currentWeekParams = ref({});
|
||||||
const chartLoading = ref(true); // Start with loading state
|
const chartLoading = ref(true); // Start with loading state
|
||||||
|
|
||||||
const lookup = route.query.lookup;
|
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
|
// Computed property to get current filters for the chart
|
||||||
const currentFilters = computed(() => {
|
const currentFilters = computed(() => {
|
||||||
return filtersStore.getTableFilters("clients");
|
filters = { ...filtersStore.getTableFilters("clients"),
|
||||||
|
company: { value: companyStore.currentCompany, matchMode: FilterMatchMode.CONTAINS}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle week change from chart
|
// Handle week change from chart
|
||||||
|
|
@ -220,6 +239,7 @@ const tableActions = [
|
||||||
// Handle lazy loading events from DataTable
|
// Handle lazy loading events from DataTable
|
||||||
const handleLazyLoad = async (event) => {
|
const handleLazyLoad = async (event) => {
|
||||||
console.log("Clients page - handling lazy load:", event);
|
console.log("Clients page - handling lazy load:", event);
|
||||||
|
lastLazyLoadEvent.value = event;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
@ -263,8 +283,9 @@ const handleLazyLoad = async (event) => {
|
||||||
filters,
|
filters,
|
||||||
sortingArray,
|
sortingArray,
|
||||||
});
|
});
|
||||||
|
filters["company"] = { value: companyStore.currentCompany, matchMode: FilterMatchMode.CONTAINS};
|
||||||
|
|
||||||
const result = await Api.getPaginatedClientDetails(
|
const result = await Api.getPaginatedClientDetailsV2(
|
||||||
paginationParams,
|
paginationParams,
|
||||||
filters,
|
filters,
|
||||||
sortingArray,
|
sortingArray,
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ const notifications = useNotificationStore();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const jobIdQuery = computed(() => route.query.jobId || "");
|
const jobIdQuery = computed(() => route.query.name || "");
|
||||||
const isNew = computed(() => route.query.new === "true");
|
const isNew = computed(() => route.query.new === "true");
|
||||||
|
|
||||||
const tableData = ref([]);
|
const tableData = ref([]);
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ const handleLazyLoad = async (event) => {
|
||||||
|
|
||||||
const handleRowClick = (event) => {
|
const handleRowClick = (event) => {
|
||||||
const rowData = event.data;
|
const rowData = event.data;
|
||||||
router.push(`/job?jobId=${rowData.name}`);
|
router.push(`/job?name=${rowData.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ const currentFilters = computed(() => {
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ label: "Task", fieldName: "subject", type: "text", sortable: true, filterable: true,
|
{ 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,
|
{ label: "Job", fieldName: "project", type: "link", sortable: true,
|
||||||
onLinkClick: (link, rowData) => handleProjectClick(link, rowData)
|
onLinkClick: (link, rowData) => handleProjectClick(link, rowData)
|
||||||
},
|
},
|
||||||
|
|
@ -220,11 +220,6 @@ watch(showCompleted, () => {
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (subject) {
|
|
||||||
const inputElement = document.getElementById(`filter-subject`);
|
|
||||||
inputElement.text = subject;
|
|
||||||
}
|
|
||||||
notifications.addWarning("Tasks page coming soon");
|
|
||||||
// Initialize pagination and filters
|
// Initialize pagination and filters
|
||||||
paginationStore.initializeTablePagination("tasks", { rows: 10 });
|
paginationStore.initializeTablePagination("tasks", { rows: 10 });
|
||||||
filtersStore.initializeTableFilters("tasks", columns);
|
filtersStore.initializeTableFilters("tasks", columns);
|
||||||
|
|
@ -236,11 +231,8 @@ onMounted(async () => {
|
||||||
const initialSorting = filtersStore.getTableSorting("tasks");
|
const initialSorting = filtersStore.getTableSorting("tasks");
|
||||||
|
|
||||||
if (subject) {
|
if (subject) {
|
||||||
console.log(subject);
|
console.log("Setting subject filter from query param:", subject);
|
||||||
console.log(initialFilters);
|
|
||||||
console.log(initialFilters.subject);
|
|
||||||
initialFilters.subject.value = subject;
|
initialFilters.subject.value = subject;
|
||||||
//initialFilters = {...initialFilters, subject: {value: subject, match_mode: "contains"}};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsResult = await Api.getTaskStatusOptions();
|
const optionsResult = await Api.getTaskStatusOptions();
|
||||||
|
|
@ -255,6 +247,8 @@ onMounted(async () => {
|
||||||
sortOrder: initialSorting.order || initialPagination.sortOrder,
|
sortOrder: initialSorting.order || initialPagination.sortOrder,
|
||||||
filters: initialFilters,
|
filters: initialFilters,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
notifications.addWarning("Tasks page coming soon");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue