Compare commits
No commits in common. "28c57c4ad02d0dbedb62b94e296f0a3356ee6f95" and "5e192a61e18b8d89b41e216e69844d4b432a929a" have entirely different histories.
28c57c4ad0
...
5e192a61e1
31 changed files with 148 additions and 68974 deletions
|
|
@ -26,18 +26,6 @@ def get_week_bid_meetings(week_start, week_end, company):
|
|||
except Exception as e:
|
||||
frappe.log_error(message=str(e), title="Get Week On-Site Meetings Failed")
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bid_meeting_note_form(project_template):
|
||||
bid_meeting_note_form_name = frappe.db.get_value("Project Template", project_template, "bid_meeting_note_form")
|
||||
if not bid_meeting_note_form_name:
|
||||
return build_error_response(f"No Bid Meeting Note Form configured for Project Template '{project_template}'", 404)
|
||||
try:
|
||||
note_form = frappe.get_doc("Bid Meeting Note Form", bid_meeting_note_form_name)
|
||||
return build_success_response(note_form.as_dict())
|
||||
except Exception as e:
|
||||
frappe.log_error(message=str(e), title="Get Bid Meeting Note Form Failed")
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bid_meetings(fields=["*"], filters={}, company=None):
|
||||
|
|
@ -87,62 +75,6 @@ def get_unscheduled_bid_meetings(company):
|
|||
frappe.log_error(message=str(e), title="Get Unscheduled On-Site Meetings Failed")
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def submit_bid_meeting_note_form(bid_meeting, project_template, fields, form_template):
|
||||
"""Submit Bid Meeting Note Form data for a specific On-Site Meeting."""
|
||||
if isinstance(fields, str):
|
||||
fields = json.loads(fields)
|
||||
try:
|
||||
print(f"DEBUG: Submitting Bid Meeting Note Form for meeting='{bid_meeting}' from template='{form_template}' with fields='{fields}'")
|
||||
|
||||
meeting = DbService.get_or_throw("On-Site Meeting", bid_meeting)
|
||||
|
||||
# Update fields on the meeting
|
||||
meeting_note_field_docs = [{
|
||||
"label": field.get("label"),
|
||||
"type": field.get("type"),
|
||||
"value": json.dumps(field.get("value")) if isinstance(field.get("value"), (list, dict)) else field.get("value"),
|
||||
"row": field.get("row"),
|
||||
"column": field.get("column"),
|
||||
"value_doctype": field.get("doctype_for_select"),
|
||||
"available_options": field.get("options"),
|
||||
"include_available_options": field.get("include_available_options", False),
|
||||
"conditional_on_field": field.get("conditional_on_field"),
|
||||
"conditional_on_value": field.get("conditional_on_value"),
|
||||
"doctype_label_field": field.get("doctype_label_field")
|
||||
} for field in fields]
|
||||
new_bid_meeting_note_doc = frappe.get_doc({
|
||||
"doctype": "Bid Meeting Note",
|
||||
"bid_meeting": bid_meeting,
|
||||
"project_template": project_template,
|
||||
"form_template": form_template,
|
||||
"fields": meeting_note_field_docs
|
||||
})
|
||||
new_bid_meeting_note_doc.insert(ignore_permissions=True)
|
||||
for field_row, field in zip(new_bid_meeting_note_doc.fields, fields):
|
||||
print(f"DEBUG: {field_row.label} - {field.get("label")}")
|
||||
if not isinstance(field.get("value"), list):
|
||||
continue
|
||||
for item in field["value"]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
new_bid_meeting_note_doc.append("quantities", {
|
||||
"meeting_note_field": field_row.name,
|
||||
"item": item.get("item"),
|
||||
"quantity": item.get("quantity")
|
||||
})
|
||||
new_bid_meeting_note_doc.save(ignore_permissions=True)
|
||||
meeting.bid_notes = new_bid_meeting_note_doc.name
|
||||
meeting.status = "Completed"
|
||||
meeting.save()
|
||||
frappe.db.commit()
|
||||
|
||||
return build_success_response(meeting.as_dict())
|
||||
except frappe.DoesNotExistError:
|
||||
return build_error_response(f"On-Site Meeting '{bid_meeting}' does not exist.", 404)
|
||||
except Exception as e:
|
||||
frappe.log_error(message=str(e), title="Submit Bid Meeting Note Form Failed")
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bid_meeting(name):
|
||||
|
|
|
|||
|
|
@ -14,34 +14,8 @@ from custom_ui.services import DbService, ClientService, AddressService, Contact
|
|||
def get_estimate_table_data_v2(filters={}, sortings=[], page=1, page_size=10):
|
||||
"""Get paginated estimate table data with filtering and sorting."""
|
||||
print("DEBUG: Raw estimate options received:", filters, sortings, page, page_size)
|
||||
filters, sortings, page, page_size = DbUtils.process_datatable_request(filters, sortings, page, page_size)
|
||||
sortings = "modified desc" if not sortings else sortings
|
||||
count = frappe.db.count("Quotation", filters=filters)
|
||||
print(f"DEBUG: Number of estimates returned: {count}")
|
||||
estimate_names = frappe.db.get_all(
|
||||
"Quotation",
|
||||
filters=filters,
|
||||
pluck="name",
|
||||
limit=page_size,
|
||||
start=(page) * page_size,
|
||||
order_by=sortings
|
||||
)
|
||||
|
||||
estimates = [frappe.get_doc("Quotation", name).as_dict() for name in estimate_names]
|
||||
tableRows = []
|
||||
for estimate in estimates:
|
||||
tableRow = {
|
||||
"id": estimate["name"],
|
||||
"address": frappe.db.get_value("Address", estimate.get("custom_job_address"), "full_address"),
|
||||
# strip "-#-" from actual_customer_name and anything that comes after it
|
||||
"customer": estimate.get("actual_customer_name").split("-#-")[0] if estimate.get("actual_customer_name") else estimate.get("customer_name") if estimate.get("customer_name") else "",
|
||||
"status": estimate.get("custom_current_status", ""),
|
||||
"order_type": estimate.get("order_type", ""),
|
||||
}
|
||||
tableRows.append(tableRow)
|
||||
table_data_dict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size)
|
||||
return build_success_response(table_data_dict)
|
||||
|
||||
filters, sortings, page, page_size = DbUtils.process_query_conditions(filters, sortings, page, page_size)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import frappe
|
||||
from custom_ui.db_utils import build_history_entries, build_success_response, build_error_response
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
def get_doc_history(doctype, docname):
|
||||
"""Get the history of changes for a specific document."""
|
||||
|
|
@ -77,24 +76,4 @@ def get_week_holidays(week_start_date: str):
|
|||
)
|
||||
|
||||
print(f"DEBUG: Retrieved holidays from {start_date} to {end_date}: {holidays}")
|
||||
return build_success_response(holidays)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doc_list(doctype, fields=["*"], filters={}, pluck=None):
|
||||
"""Get list of documents for a given doctype with specified fields and filters."""
|
||||
if isinstance(fields, str):
|
||||
fields = json.loads(fields)
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
try:
|
||||
docs = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
order_by="creation desc",
|
||||
pluck=pluck
|
||||
)
|
||||
print(f"DEBUG: Retrieved documents for {doctype} with filters {filters}: {docs}")
|
||||
return build_success_response(docs)
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
return build_success_response(holidays)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import frappe, json
|
||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService
|
||||
from custom_ui.services import AddressService, ClientService
|
||||
from frappe.utils import getdate
|
||||
|
||||
# ===============================================================================
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import frappe, json
|
||||
from custom_ui.db_utils import build_success_response, build_error_response
|
||||
from custom_ui.services import ServiceAppointmentService
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_service_appointments(companies, filters={}):
|
||||
"""Get Service Appointments for given companies."""
|
||||
try:
|
||||
if isinstance(companies, str):
|
||||
companies = json.loads(companies)
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
filters["company"] = ["in", companies]
|
||||
service_appointment_names = frappe.get_all(
|
||||
"Service Appointment",
|
||||
filters=filters,
|
||||
pluck="name"
|
||||
)
|
||||
service_appointments = [
|
||||
ServiceAppointmentService.get_full_dict(name)
|
||||
for name in service_appointment_names
|
||||
]
|
||||
return build_success_response(service_appointments)
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_service_appointment_scheduled_dates(service_appointment_name: str, start_date, end_date, crew_lead_name, start_time=None, end_time=None):
|
||||
"""Update scheduled dates for a Service Appointment."""
|
||||
try:
|
||||
updated_service_appointment = ServiceAppointmentService.update_scheduled_dates(
|
||||
service_appointment_name,
|
||||
start_date,
|
||||
end_date,
|
||||
start_time,
|
||||
end_time
|
||||
)
|
||||
return build_success_response(updated_service_appointment.as_dict())
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import frappe
|
||||
|
||||
def attach_bid_note_form_to_project_template(doc, method):
|
||||
"""Attatch Bid Meeting Note Form to Project Template on insert."""
|
||||
print("DEBUG: Attaching Bid Meeting Note Form to Project Template")
|
||||
frappe.set_value("Project Template", doc.project_template, "bid_meeting_note_form", doc.name)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import frappe
|
||||
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService, TaskService
|
||||
from custom_ui.services import AddressService, ClientService
|
||||
from datetime import timedelta
|
||||
|
||||
def after_insert(doc, method):
|
||||
|
|
@ -21,21 +21,6 @@ def after_insert(doc, method):
|
|||
"job_status",
|
||||
"In Progress"
|
||||
)
|
||||
service_apt = ServiceAppointmentService.create({
|
||||
"project": doc.name,
|
||||
"customer": doc.customer,
|
||||
"address": doc.job_address,
|
||||
"company": doc.company,
|
||||
"project_template": doc.project_template
|
||||
})
|
||||
frappe.db.set_value("Project", doc.name, "service_appointment", service_apt.name)
|
||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.name)]
|
||||
for task_name in task_names:
|
||||
doc.append("tasks", {
|
||||
"task": task_name
|
||||
})
|
||||
TaskService.calculate_and_set_due_dates(task_names, "Created")
|
||||
|
||||
|
||||
|
||||
def before_insert(doc, method):
|
||||
|
|
@ -56,12 +41,6 @@ def before_save(doc, method):
|
|||
|
||||
def after_save(doc, method):
|
||||
print("DEBUG: After Save Triggered for Project:", doc.name)
|
||||
event = TaskService.determine_event(doc)
|
||||
if event:
|
||||
TaskService.calculate_and_set_due_dates(
|
||||
[task.task for task in doc.tasks],
|
||||
event
|
||||
)
|
||||
if doc.project_template == "SNW Install":
|
||||
print("DEBUG: Project template is SNW Install, updating Address Job Status based on Project status")
|
||||
status_mapping = {
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import frappe
|
||||
|
||||
from custom_ui.services import TaskService
|
||||
|
||||
def on_update(doc, method):
|
||||
print("DEBUG: On Update Triggered for Service Appointment")
|
||||
event = TaskService.determine_event(doc)
|
||||
if event:
|
||||
tasks = TaskService.get_tasks_by_project(doc.project)
|
||||
task_names = [task.name for task in tasks]
|
||||
TaskService.calculate_and_set_due_dates(task_names=task_names, event=event)
|
||||
|
||||
def after_insert(doc, method):
|
||||
print("DEBUG: After Insert Triggered for Service Appointment")
|
||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
|
||||
TaskService.calculate_and_set_due_dates(task_names=task_names, event="Created")
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import frappe
|
||||
from custom_ui.services import AddressService, ClientService, TaskService
|
||||
from custom_ui.services import AddressService, ClientService
|
||||
|
||||
def before_insert(doc, method):
|
||||
"""Set values before inserting a Task."""
|
||||
|
|
@ -18,16 +18,4 @@ def after_insert(doc, method):
|
|||
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 }
|
||||
)
|
||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
|
||||
TaskService.calculate_and_set_due_dates(task_names, "Created")
|
||||
|
||||
def after_save(doc, method):
|
||||
print("DEBUG: After Save Triggered for Task:", doc.name)
|
||||
event = TaskService.determine_event(doc)
|
||||
if event:
|
||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
|
||||
TaskService.calculate_and_set_due_dates(task_names, event)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,519 +0,0 @@
|
|||
[
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Opportunity",
|
||||
"enabled": 1,
|
||||
"modified": "2024-06-28 04:58:27.527685",
|
||||
"module": null,
|
||||
"name": "Set Item Price",
|
||||
"script": "frappe.ui.form.on('Opportunity Item', {\r\n item_code: function(frm, cdt, cdn) {\r\n var row = locals[cdt][cdn];\r\n if (row.item_code) {\r\n frappe.call({\r\n method: 'frappe.client.get_value',\r\n args: {\r\n 'doctype': 'Item Price',\r\n 'filters': {\r\n 'item_code': row.item_code,\r\n 'selling': 1\r\n },\r\n 'fieldname': ['price_list_rate']\r\n },\r\n callback: function(r) {\r\n if (r.message) {\r\n frappe.model.set_value(cdt, cdn, 'rate', r.message.price_list_rate);\r\n frappe.model.set_value(cdt, cdn, 'amount', row.qty * r.message.price_list_rate);\r\n }\r\n }\r\n });\r\n }\r\n },\r\n qty: function(frm, cdt, cdn) {\r\n var row = locals[cdt][cdn];\r\n if (row.qty && row.rate) {\r\n frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.rate);\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "SNW Jobs",
|
||||
"enabled": 1,
|
||||
"modified": "2024-07-18 02:56:08.345504",
|
||||
"module": null,
|
||||
"name": "SNW Jobs - auto populate days",
|
||||
"script": "frappe.ui.form.on('SNW Jobs', {\n refresh(frm) {\n // your code here (if needed)\n },\n start_date(frm) {\n calculate_total_days(frm);\n },\n end_date(frm) {\n calculate_total_days(frm);\n }\n});\n\nfunction calculate_total_days(frm) {\n if (frm.doc.start_date && frm.doc.end_date) {\n const startDate = new Date(frm.doc.start_date);\n const endDate = new Date(frm.doc.end_date);\n const timeDiff = endDate - startDate;\n const dayDiff = timeDiff / (1000 * 3600 * 24);\n frm.set_value('number_of_days', dayDiff);\n }\n}",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "SNW Jobs",
|
||||
"enabled": 1,
|
||||
"modified": "2024-07-18 03:04:59.760299",
|
||||
"module": null,
|
||||
"name": "SNW Jobs - Calculate Balance",
|
||||
"script": "frappe.ui.form.on('SNW Jobs', {\r\n refresh(frm) {\r\n // Your code here (if needed)\r\n },\r\n total_expected_price(frm) {\r\n calculate_balance(frm);\r\n },\r\n paid(frm) {\r\n calculate_balance(frm);\r\n }\r\n});\r\n\r\nfunction calculate_balance(frm) {\r\n if (frm.doc.total_expected_price != null && frm.doc.paid != null) {\r\n const balance = frm.doc.total_expected_price - frm.doc.paid;\r\n frm.set_value('balance', balance);\r\n }\r\n}\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "SNW Jobs",
|
||||
"enabled": 1,
|
||||
"modified": "2024-07-18 03:12:21.825901",
|
||||
"module": null,
|
||||
"name": "SNW Jobs - pull crew lead employees",
|
||||
"script": "frappe.ui.form.on('SNW Jobs', {\n\trefresh(frm) {\n\t\t// your code here\n\t},\n\t onload(frm) {\n frm.set_query('crew_leader', function() {\n return {\n filters: {\n designation: 'Crew Lead'\n }\n };\n });\n }\n})",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Follow Up Checklist",
|
||||
"enabled": 1,
|
||||
"modified": "2024-09-16 05:58:34.623817",
|
||||
"module": null,
|
||||
"name": "auto populate follow up form",
|
||||
"script": "frappe.ui.form.on('Follow Up Checklist', {\n customer:function(frm) {\n\t\t // Fetch customer details like phone and email\n frappe.db.get_value(\"Address\", {\"customer\": frm.doc.customer, \"customer_primary_address\": 1}, \"address_line1\", function(r) {\n if(r && r.address_line1) {\n frm.set_value(\"address\", r.address_line1); // Set the address field\n }\n });\n }\n});",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Quotation",
|
||||
"enabled": 1,
|
||||
"modified": "2025-04-24 05:36:48.042696",
|
||||
"module": null,
|
||||
"name": "Item Markup Script",
|
||||
"script": "frappe.ui.form.on('Quotation Item', {\r\n item_code: function(frm, cdt, cdn) {\r\n let row = locals[cdt][cdn];\r\n if (row.item_code) {\r\n // Fetch the item price from the Item Price List\r\n frappe.call({\r\n method: 'frappe.client.get_list',\r\n args: {\r\n doctype: 'Item Price',\r\n filters: {\r\n item_code: row.item_code,\r\n price_list: frm.doc.selling_price_list // Assuming the price list is set in the Quotation\r\n },\r\n fields: ['price_list_rate']\r\n },\r\n callback: function(response) {\r\n if (response.message && response.message.length > 0) {\r\n // Get the price from the Item Price List\r\n let base_rate = response.message[0].price_list_rate || 0;\r\n\r\n // Fetch the markup percentage from the Item master\r\n frappe.call({\r\n method: 'frappe.client.get',\r\n args: {\r\n doctype: 'Item',\r\n name: row.item_code\r\n },\r\n callback: function(r) {\r\n if (r.message) {\r\n // Fetch the markup percentage from the Item master\r\n let markup = r.message.custom_markup_percentage || 0; // Default to 0% if not set\r\n \r\n // Calculate the new rate with markup\r\n let new_rate = base_rate + (base_rate * (markup / 100));\r\n frappe.model.set_value(cdt, cdn, 'rate', new_rate);\r\n \r\n // Refresh the items table to show the updated rate\r\n frm.refresh_field('items');\r\n }\r\n }\r\n });\r\n }\r\n }\r\n });\r\n }\r\n }\r\n});\r\n\r\n// Optional: Recalculate all items on form refresh or load\r\nfrappe.ui.form.on('Quotation', {\r\n refresh: function(frm) {\r\n frm.doc.items.forEach(function(item) {\r\n //frappe.model.trigger('item_code', item.name);\r\n });\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Quotation",
|
||||
"enabled": 1,
|
||||
"modified": "2025-01-08 05:04:26.743210",
|
||||
"module": null,
|
||||
"name": "Quotation - Set Same Valid Until Date",
|
||||
"script": "frappe.ui.form.on(\"Quotation\", {\n onload: function(frm) {\n frm.set_value('valid_till', '2025-12-31');\n }\n});",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Note",
|
||||
"enabled": 1,
|
||||
"modified": "2024-10-24 23:43:27.548340",
|
||||
"module": null,
|
||||
"name": "Open Note in Edit Mode",
|
||||
"script": "frappe.ui.form.on('Note', {\r\n onload_post_render: function(frm) {\r\n // Check if this is a new document or in read mode, then switch to edit mode\r\n if (frm.is_new() || frm.doc.__unsaved) {\r\n frm.page.set_primary_action(__('Save'), () => frm.save());\r\n frm.page.wrapper.find('.btn-primary').removeClass('hidden');\r\n frm.enable_save();\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-05 11:55:14.179233",
|
||||
"module": null,
|
||||
"name": "Address Doctype Linked City Auto Populate",
|
||||
"script": "frappe.ui.form.on('Address', {\r\n custom_linked_city: function (frm) {\r\n if (frm.doc.custom_linked_city) {\r\n frappe.db.get_doc('City', frm.doc.custom_linked_city).then(city_doc => {\r\n frm.set_value('city', city_doc.city_name); // Sync to mandatory City field\r\n frm.set_value('state', city_doc.state); // Populate State\r\n if (city_doc.zip_code) {\r\n frm.set_value('zip_code', city_doc.zip_code); // Populate Zip Code\r\n }\r\n });\r\n }\r\n },\r\n city: function (frm) {\r\n // Optionally, sync back to custom_linked_city when the mandatory City field is used\r\n if (!frm.doc.custom_linked_city && frm.doc.city) {\r\n frappe.msgprint(__('Consider selecting a linked city for better accuracy.'));\r\n }\r\n }\r\n});",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-06 05:15:31.487671",
|
||||
"module": null,
|
||||
"name": "Address - Irrigation District Visibility Condition",
|
||||
"script": "frappe.ui.form.on('Address', {\r\n refresh: function(frm) {\r\n let show_irrigation_district = false;\r\n\r\n // Check if 'is_shipping_address' is checked\r\n if (frm.doc.is_shipping_address) {\r\n // Required companies list\r\n const required_companies = ['Sprinklers Northwest', 'Nuco Yard Care'];\r\n\r\n // Check child table rows\r\n if (frm.doc.custom_linked_companies) {\r\n show_irrigation_district = frm.doc.custom_linked_companies.some(row =>\r\n required_companies.includes(row.company)\r\n );\r\n }\r\n }\r\n\r\n // Show or hide the custom_irrigation_district field\r\n frm.set_df_property('custom_irrigation_district', 'hidden', !show_irrigation_district);\r\n },\r\n\r\n is_service_address: function(frm) {\r\n // Re-run visibility logic when 'is_service_address' changes\r\n frm.trigger('refresh');\r\n },\r\n\r\n custom_linked_companies: function(frm) {\r\n // Re-run visibility logic when the child table is updated\r\n frm.trigger('refresh');\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-06 05:27:19.840296",
|
||||
"module": null,
|
||||
"name": "Address - Installed By Snw Visibility Condition",
|
||||
"script": "frappe.ui.form.on('Address', {\r\n refresh: function(frm) {\r\n frm.trigger('toggle_irrigation_fields');\r\n },\r\n\r\n custom_installed_by_sprinklers_nw: function(frm) {\r\n frm.trigger('toggle_irrigation_fields');\r\n },\r\n\r\n toggle_irrigation_fields: function(frm) {\r\n const is_installed = frm.doc.custom_installed_by_sprinklers_nw;\r\n\r\n // Toggle visibility for irrigation-related fields\r\n frm.set_df_property('custom_install_month', 'hidden', !is_installed);\r\n frm.set_df_property('custom_install_year', 'hidden', !is_installed);\r\n frm.set_df_property('custom_backflow_test_report', 'hidden', !is_installed);\r\n frm.set_df_property('custom_photo_attachment', 'hidden', !is_installed);\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 0,
|
||||
"modified": "2024-12-13 07:14:26.487456",
|
||||
"module": null,
|
||||
"name": "Address - Google Map Display",
|
||||
"script": "frappe.ui.form.on('Address', {\r\n refresh: function(frm) {\r\n // Render Google Map if address fields are available and saved\r\n if (!frm.is_new() && frm.doc.address_line1 && frm.doc.city && frm.doc.country) {\r\n frm.trigger('render_google_map');\r\n }\r\n },\r\n\r\n render_google_map: function(frm) {\r\n // Construct the full address string\r\n const address = [\r\n frm.doc.address_line1,\r\n frm.doc.address_line2 || '', // Optional\r\n frm.doc.city,\r\n frm.doc.state || '', // Optional\r\n frm.doc.pincode || '', // Optional\r\n frm.doc.country\r\n ].filter(Boolean).join(', '); // Remove empty fields\r\n\r\n // Replace with your Google Maps API Key\r\n const apiKey = 'AIzaSyB2uNXSQpMp-lGJHIWFpzloWxs76zjkU8Y';\r\n\r\n // Generate the embed URL\r\n const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodeURIComponent(address)}`;\r\n\r\n // Render the iframe in the HTML field\r\n frm.fields_dict.custom_google_map.$wrapper.html(`\r\n <iframe \r\n width=\"100%\" \r\n height=\"400\" \r\n frameborder=\"0\" \r\n style=\"border:0\" \r\n src=\"${mapUrl}\" \r\n allowfullscreen>\r\n </iframe>\r\n `);\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 0,
|
||||
"modified": "2025-05-15 09:00:39.086280",
|
||||
"module": null,
|
||||
"name": "Address - Auto-Filling Mutiple Contact Email and Phone",
|
||||
"script": "frappe.ui.form.on('Address Contact Role', {\r\n contact: function(frm, cdt, cdn) {\r\n let row = frappe.get_doc(cdt, cdn);\r\n\r\n if (row.contact) {\r\n // Fetch email and phone from the selected Contact\r\n frappe.db.get_doc('Contact', row.contact).then(contact => {\r\n if (contact) {\r\n // Pull primary email and phone from the Contact\r\n row.email = contact.email_id || '';\r\n row.phone = contact.phone || contact.mobile_no || ''; // Prefer phone, fallback to mobile_no\r\n frm.refresh_field('custom_linked_contacts');\r\n }\r\n });\r\n } else {\r\n // Clear fields if no Contact selected\r\n row.email = '';\r\n row.phone = '';\r\n frm.refresh_field('custom_linked_contacts');\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Locate Log",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-11 12:11:35.789512",
|
||||
"module": null,
|
||||
"name": "Locate Button",
|
||||
"script": "frappe.ui.form.on('Locate Log', {\r\n refresh: function(frm) {\r\n // Add the custom button \"Mark as Located\"\r\n if (frm.doc.status !== 'Completed') {\r\n frm.add_custom_button(__('Mark as Located'), function() {\r\n frm.trigger('mark_as_located');\r\n });\r\n }\r\n },\r\n mark_as_located: function(frm) {\r\n // Check if Dig Ticket # is provided\r\n if (!frm.doc.dig_ticket_number) {\r\n frappe.msgprint(__('Please enter a Dig Ticket #.'));\r\n return;\r\n }\r\n\r\n // Trigger the backend method to update the Job Queue and Locate Log\r\n frappe.call({\r\n method: 'my_app.my_module.api.update_locate_log_and_job_queue',\r\n args: {\r\n locate_log: frm.doc.name,\r\n dig_ticket_number: frm.doc.dig_ticket_number\r\n },\r\n callback: function(r) {\r\n if (r.message) {\r\n frappe.msgprint(__('Locate marked as completed.'));\r\n frm.reload_doc(); // Reload the form to reflect changes\r\n }\r\n }\r\n });\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Pre-Built Routes",
|
||||
"enabled": 0,
|
||||
"modified": "2024-12-11 13:24:02.894613",
|
||||
"module": null,
|
||||
"name": "Real-Time Updates for Address -Route Connection",
|
||||
"script": "frappe.ui.form.on('Assigned Address', {\r\n address_name: function (frm, cdt, cdn) {\r\n const row = locals[cdt][cdn];\r\n\r\n // Ensure the address_name field is set\r\n if (row.address_name) {\r\n // Fetch the custom_confirmation_status from the Address Doctype\r\n frappe.db.get_value('Address', row.address_name, 'custom_confirmation_status', (value) => {\r\n if (value && value.custom_confirmation_status) {\r\n // Set the status in the child table to match the Address confirmation status\r\n frappe.model.set_value(cdt, cdn, 'status', value.custom_confirmation_status);\r\n } else {\r\n // Default to No Response if Address confirmation status is not set\r\n frappe.model.set_value(cdt, cdn, 'status', 'No Response');\r\n }\r\n });\r\n } else {\r\n // If no address_name is set, default status to No Response\r\n frappe.model.set_value(cdt, cdn, 'status', 'No Response');\r\n }\r\n }\r\n});\r\n\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 0,
|
||||
"modified": "2024-12-11 13:09:36.908372",
|
||||
"module": null,
|
||||
"name": "Real Time Updates for Address Doctype Connection to Pre-Built",
|
||||
"script": "frappe.ui.form.on('Address', {\r\n custom_confirmation_status: function (frm) {\r\n // Check if the Service Route is set\r\n if (!frm.doc.custom_service_route) {\r\n frappe.msgprint(__('Service Route is not assigned for this address.'));\r\n return;\r\n }\r\n\r\n // Retrieve the Assigned Address row where this address is linked\r\n frappe.call({\r\n method: 'frappe.client.get_value',\r\n args: {\r\n doctype: 'Assigned Address',\r\n filters: { address_name: frm.doc.name },\r\n fieldname: 'name'\r\n },\r\n callback: function (response) {\r\n if (response.message) {\r\n const assigned_row_name = response.message.name;\r\n // Update the Status in the Assigned Address row\r\n frappe.call({\r\n method: 'frappe.client.set_value',\r\n args: {\r\n doctype: 'Assigned Address',\r\n name: assigned_row_name,\r\n fieldname: 'status',\r\n value: frm.doc.custom_confirmation_status\r\n },\r\n callback: function (response) {\r\n if (!response.exc) {\r\n frappe.msgprint(__('Status updated in Pre-Built Route.'));\r\n }\r\n }\r\n });\r\n } else {\r\n frappe.msgprint(__('This address is not linked to any Pre-Built Route.'));\r\n }\r\n }\r\n });\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Pre-Built Routes",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-13 09:14:52.635425",
|
||||
"module": null,
|
||||
"name": "Color Code Route Child Table",
|
||||
"script": "frappe.ui.form.on('Pre-Built Routes', {\r\n refresh: function (frm) {\r\n // Loop through the rows in the Assigned Addresses table\r\n frm.fields_dict['assigned_addresses'].grid.wrapper.find('.grid-row').each(function () {\r\n const row = $(this);\r\n const doc = row.data('doc'); // Get the child table row data\r\n\r\n if (doc && doc.status) {\r\n // Apply color based on the Status field\r\n switch (doc.status) {\r\n case 'Confirmed':\r\n row.css('background-color', '#D4EDDA'); // Green for Scheduled\r\n break;\r\n case 'Reschedule':\r\n row.css('background-color', '#FFF3CD'); // Yellow for Completed\r\n break;\r\n case 'Declined':\r\n row.css('background-color', '#F8D7DA'); // Red for Pending Reschedule\r\n break;\r\n default:\r\n row.css('background-color', '#E2E0E0',); // Reset to default if no match\r\n break;\r\n }\r\n }\r\n });\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Pre-Built Routes",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-11 13:25:17.161414",
|
||||
"module": null,
|
||||
"name": "Dynamically Update Confirmation Status on Pre-Built Route",
|
||||
"script": "frappe.ui.form.on('Assigned Address', {\r\n address_name: function (frm, cdt, cdn) {\r\n const row = locals[cdt][cdn];\r\n\r\n // Ensure the address_name field is set\r\n if (row.address_name) {\r\n // Fetch the custom_confirmation_status from the Address Doctype\r\n frappe.db.get_value('Address', row.address_name, 'custom_confirmation_status', (value) => {\r\n if (value && value.custom_confirmation_status) {\r\n // Set the status in the child table to match the Address confirmation status\r\n frappe.model.set_value(cdt, cdn, 'status', value.custom_confirmation_status);\r\n } else {\r\n // Default to No Response if Address confirmation status is not set\r\n frappe.model.set_value(cdt, cdn, 'status', 'No Response');\r\n }\r\n });\r\n } else {\r\n // If no address_name is set, default status to No Response\r\n frappe.model.set_value(cdt, cdn, 'status', 'No Response');\r\n }\r\n }\r\n});",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Pre-Built Routes",
|
||||
"enabled": 0,
|
||||
"modified": "2024-12-13 09:03:06.173211",
|
||||
"module": null,
|
||||
"name": "Route Map",
|
||||
"script": "frappe.ui.form.on('Pre-Built Routes', {\r\n refresh: function (frm) {\r\n // Check if Google Maps API is loaded\r\n if (!window.google || !window.google.maps) {\r\n frappe.msgprint(__('Google Maps API is not loaded. Attempting to load...'));\r\n\r\n // Dynamically load the Google Maps API\r\n const script = document.createElement('script');\r\n script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyB2uNXSQpMp-lGJHIWFpzloWxs76zjkU8Y`;\r\n script.async = true;\r\n script.defer = true;\r\n\r\n // Call renderMap after the API is loaded\r\n script.onload = function () {\r\n console.log(\"Google Maps API loaded successfully.\");\r\n renderMap(frm); // Render the map after API loads\r\n };\r\n\r\n // Handle API load failure\r\n script.onerror = function () {\r\n frappe.msgprint(__('Failed to load Google Maps API. Please check your API key.'));\r\n };\r\n\r\n document.head.appendChild(script);\r\n } else {\r\n // If API is already loaded, render the map immediately\r\n renderMap(frm);\r\n }\r\n }\r\n});\r\n\r\nfunction renderMap(frm) {\r\n // Ensure the form has assigned addresses\r\n const addresses = frm.doc.assigned_addresses || [];\r\n if (!addresses.length) {\r\n frappe.msgprint(__('No addresses found for this route.'));\r\n return;\r\n }\r\n\r\n // Prepare points for the map\r\n const points = addresses.map((row) => ({\r\n lat: parseFloat(row.latitude || 0),\r\n lng: parseFloat(row.longitude || 0),\r\n status: row.status || 'No Response',\r\n title: row.address_name || 'Unknown Address'\r\n }));\r\n\r\n console.log(\"Map points:\", points); // Debug the points array\r\n\r\n // Define marker colors based on status\r\n const statusColors = {\r\n 'Confirmed': 'green',\r\n 'Reschedule': 'yellow',\r\n 'Decline': 'red',\r\n 'No Response': 'white'\r\n };\r\n\r\n // Initialize map container\r\n const mapContainer = frm.fields_dict['route_map'].wrapper;\r\n mapContainer.innerHTML = '<div id=\"route-map\" style=\"height: 400px;\"></div>';\r\n const map = new google.maps.Map(document.getElementById('route-map'), {\r\n zoom: 10,\r\n mapTypeId: 'roadmap'\r\n });\r\n\r\n // Fit map bounds to all points\r\n const bounds = new google.maps.LatLngBounds();\r\n\r\n // Add markers for each point\r\n points.forEach((point) => {\r\n if (point.lat && point.lng) {\r\n const marker = new google.maps.Marker({\r\n position: { lat: point.lat, lng: point.lng },\r\n map: map,\r\n title: point.title,\r\n icon: {\r\n path: google.maps.SymbolPath.CIRCLE,\r\n scale: 8,\r\n fillColor: statusColors[point.status],\r\n fillOpacity: 1,\r\n strokeWeight: 0\r\n }\r\n });\r\n bounds.extend(marker.getPosition());\r\n }\r\n });\r\n\r\n // Adjust map view to fit all markers\r\n if (!bounds.isEmpty()) {\r\n map.fitBounds(bounds);\r\n } else {\r\n frappe.msgprint(__('No valid points to display on the map.'));\r\n }\r\n}\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 1,
|
||||
"modified": "2025-05-15 12:34:32.006687",
|
||||
"module": null,
|
||||
"name": "Test - Update Address Map Display with lat/long",
|
||||
"script": "frappe.ui.form.on('Address', {\r\n refresh: function(frm) {\r\n frm.trigger('render_google_map');\r\n },\r\n\r\n render_google_map: function(frm) {\r\n if (!frm.is_new() && frm.doc.address_line1 && frm.doc.city && frm.doc.country) {\r\n // Render Google Map if address fields are available and saved\r\n\r\n const address = [\r\n frm.doc.address_line1,\r\n frm.doc.address_line2 || '',\r\n frm.doc.city,\r\n frm.doc.state || '',\r\n frm.doc.pincode || '',\r\n frm.doc.country\r\n ].filter(Boolean).join(', ');\r\n \r\n const apiKey = 'AIzaSyCd3ALZe6wjt3xnc7X_rRItfKAEJugfuZ4';\r\n const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodeURIComponent(address)}`;\r\n \r\n frm.fields_dict.custom_google_map.$wrapper.html(`\r\n <iframe \r\n width=\"100%\" \r\n height=\"400\" \r\n frameborder=\"0\" \r\n style=\"border:0\" \r\n src=\"${mapUrl}\" \r\n allowfullscreen>\r\n </iframe>\r\n `);\r\n } else {\r\n frm.fields_dict.custom_google_map.$wrapper.html('');\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Pre-Built Routes",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-13 08:47:49.392360",
|
||||
"module": null,
|
||||
"name": "Test - Pull The lat/long into Assigned Address",
|
||||
"script": "frappe.ui.form.on('Assigned Address', {\r\n address_name: function(frm, cdt, cdn) {\r\n let row = locals[cdt][cdn];\r\n if (row.address_name) {\r\n frappe.db.get_value('Address', row.address_name, ['custom_latitude', 'custom_longitude'], (value) => {\r\n if (value) {\r\n frappe.model.set_value(cdt, cdn, 'latitude', value.custom_latitude);\r\n frappe.model.set_value(cdt, cdn, 'longitude', value.custom_longitude);\r\n } else {\r\n frappe.msgprint(__('Could not fetch coordinates for the selected address.'));\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Pre-Built Routes",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-13 09:07:36.356251",
|
||||
"module": null,
|
||||
"name": "Test - Render color coded map",
|
||||
"script": "frappe.ui.form.on('Pre-Built Routes', {\r\n refresh: function(frm) {\r\n // Dynamically load Google Maps API if not already loaded\r\n if (!window.google || !window.google.maps) {\r\n const apiKey = 'AIzaSyCd3ALZe6wjt3xnc7X_rRItfKAEJugfuZ4'; // Replace with your actual API key\r\n const script = document.createElement('script');\r\n script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}`;\r\n script.async = true;\r\n script.defer = true;\r\n script.onload = function () {\r\n console.log(\"Google Maps API loaded successfully.\");\r\n renderMap(frm); // Render map after API is loaded\r\n };\r\n script.onerror = function () {\r\n frappe.msgprint(__('Failed to load Google Maps API. Please check your API key.'));\r\n };\r\n document.head.appendChild(script);\r\n return;\r\n }\r\n\r\n // If API is already loaded, render the map immediately\r\n renderMap(frm);\r\n }\r\n});\r\n\r\nfunction renderMap(frm) {\r\n const addresses = frm.doc.assigned_addresses || [];\r\n if (!addresses.length) {\r\n frappe.msgprint(__('No addresses to display on the map.'));\r\n return;\r\n }\r\n\r\n // Prepare points for the map\r\n const points = addresses.map(row => ({\r\n lat: parseFloat(row.latitude || 0),\r\n lng: parseFloat(row.longitude || 0),\r\n status: row.status || 'No Response',\r\n title: row.address_name || 'Unknown Address'\r\n }));\r\n\r\n // Define marker colors based on status\r\n const statusColors = {\r\n 'Confirmed': 'green',\r\n 'Reschedule': 'yellow',\r\n 'Declined': 'red',\r\n 'No Response': 'gray'\r\n };\r\n\r\n const mapContainer = frm.fields_dict['route_map'].wrapper;\r\n mapContainer.innerHTML = '<div id=\"route-map\" style=\"height: 400px;\"></div>';\r\n const map = new google.maps.Map(document.getElementById('route-map'), {\r\n zoom: 10,\r\n mapTypeId: 'roadmap'\r\n });\r\n\r\n const bounds = new google.maps.LatLngBounds();\r\n\r\n // Add markers for each point\r\n points.forEach(point => {\r\n if (point.lat && point.lng) {\r\n const marker = new google.maps.Marker({\r\n position: { lat: point.lat, lng: point.lng },\r\n map: map,\r\n title: point.title,\r\n icon: {\r\n path: google.maps.SymbolPath.CIRCLE,\r\n scale: 8,\r\n fillColor: statusColors[point.status],\r\n fillOpacity: 1,\r\n strokeWeight: 0\r\n }\r\n });\r\n bounds.extend(marker.getPosition());\r\n }\r\n });\r\n\r\n if (!bounds.isEmpty()) {\r\n map.fitBounds(bounds);\r\n }\r\n}\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Locate Log",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-16 14:47:43.738700",
|
||||
"module": null,
|
||||
"name": "calculated start date",
|
||||
"script": "frappe.ui.form.on('Your DocType', {\r\n date_located: function(frm) {\r\n if (frm.doc.date_located) {\r\n var input_date = frm.doc.date_located;\r\n var calculated_date = addBusinessDays(input_date, 2);\r\n frm.set_value('legal_start_date', calculated_date);\r\n }\r\n }\r\n});\r\n\r\n// Function to calculate date after a specified number of business days\r\nfunction addBusinessDays(startDate, numBusinessDays) {\r\n var date = new Date(startDate);\r\n var daysAdded = 0;\r\n \r\n while (daysAdded < numBusinessDays) {\r\n date.setDate(date.getDate() + 1);\r\n // Check if the new date is a weekend (Saturday = 6, Sunday = 0)\r\n if (date.getDay() !== 0 && date.getDay() !== 6) {\r\n daysAdded++;\r\n }\r\n }\r\n \r\n // Return the calculated date in the correct format for ERPNext\r\n return frappe.datetime.add_days(date, 0);\r\n}\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Locate Log",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-16 14:47:43.716572",
|
||||
"module": null,
|
||||
"name": "calculated start date-list",
|
||||
"script": "frappe.ui.form.on('Your DocType', {\r\n date_located: function(frm) {\r\n if (frm.doc.date_located) {\r\n var input_date = frm.doc.date_located;\r\n var calculated_date = addBusinessDays(input_date, 2);\r\n frm.set_value('legal_start_date', calculated_date);\r\n }\r\n }\r\n});\r\n\r\n// Function to calculate date after a specified number of business days\r\nfunction addBusinessDays(startDate, numBusinessDays) {\r\n var date = new Date(startDate);\r\n var daysAdded = 0;\r\n \r\n while (daysAdded < numBusinessDays) {\r\n date.setDate(date.getDate() + 1);\r\n // Check if the new date is a weekend (Saturday = 6, Sunday = 0)\r\n if (date.getDay() !== 0 && date.getDay() !== 6) {\r\n daysAdded++;\r\n }\r\n }\r\n \r\n // Return the calculated date in the correct format for ERPNext\r\n return frappe.datetime.add_days(date, 0);\r\n}\r\n",
|
||||
"view": "List"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Locate Log",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-16 14:47:18.813864",
|
||||
"module": null,
|
||||
"name": "expiration date",
|
||||
"script": "frappe.ui.form.on('Your DocType', {\r\n // Trigger when the \"Marked as Located\" button is clicked\r\n refresh: function(frm) {\r\n frm.add_custom_button(__('Marked as Located'), function() {\r\n update_expiration_date(frm);\r\n });\r\n }\r\n});\r\n\r\n// Function to update the expiration_date based on legal_start_date and state\r\nfunction update_expiration_date(frm) {\r\n if (frm.doc.legal_start_date && frm.doc.state) {\r\n var legalStartDate = frm.doc.legal_start_date;\r\n var expirationDate = null;\r\n \r\n // Check the state and calculate expiration date accordingly\r\n if (frm.doc.state === \"ID\") {\r\n expirationDate = frappe.datetime.add_days(legalStartDate, 28); // 28 days for ID\r\n } else if (frm.doc.state === \"WA\") {\r\n expirationDate = frappe.datetime.add_days(legalStartDate, 45); // 45 days for WA\r\n }\r\n\r\n // Set the calculated expiration date if a valid state is selected\r\n if (expirationDate) {\r\n frm.set_value('expiration_date', expirationDate);\r\n frappe.msgprint(__('Expiration Date has been calculated and updated.'));\r\n } else {\r\n frappe.msgprint(__('Please make sure both legal start date and state are filled.'));\r\n }\r\n } else {\r\n frappe.msgprint(__('Please make sure both legal start date and state are filled.'));\r\n }\r\n}\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Locate Log",
|
||||
"enabled": 1,
|
||||
"modified": "2024-12-16 14:53:13.214793",
|
||||
"module": null,
|
||||
"name": "expiration date -list",
|
||||
"script": "frappe.ui.form.on('Your DocType', {\r\n date_located: function(frm) {\r\n if (frm.doc.date_located) {\r\n var input_date = frm.doc.date_located;\r\n var calculated_date = addBusinessDays(input_date, 30);\r\n frm.set_value('expiration_date', calculated_date);\r\n }\r\n }\r\n});\r\n\r\n// Function to calculate date after a specified number of business days\r\nfunction addBusinessDays(startDate, numBusinessDays) {\r\n var date = new Date(startDate);\r\n var daysAdded = 0;\r\n \r\n while (daysAdded < numBusinessDays) {\r\n date.setDate(date.getDate() + 1);\r\n // Check if the new date is a weekend (Saturday = 6, Sunday = 0)\r\n if (date.getDay() !== 0 && date.getDay() !== 6) {\r\n daysAdded++;\r\n }\r\n }\r\n \r\n // Return the calculated date in the correct format for ERPNext\r\n return frappe.datetime.add_days(date, 0);\r\n}\r\n",
|
||||
"view": "List"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Quotation",
|
||||
"enabled": 0,
|
||||
"modified": "2025-01-24 13:24:25.882893",
|
||||
"module": null,
|
||||
"name": "Auto populate address from customer - test",
|
||||
"script": "frappe.ui.form.on('Quotation', {\r\n customer: function(frm) {\r\n // Trigger when the customer field is set or changed\r\n if (frm.doc.customer) {\r\n frappe.call({\r\n method: 'frappe.client.get_value',\r\n args: {\r\n doctype: 'Customer',\r\n filters: { name: frm.doc.customer },\r\n fieldname: 'address_html'\r\n },\r\n callback: function(response) {\r\n if (response.message && response.message.address_html) {\r\n // Set the customer_address field in the Quotation\r\n frm.set_value('customer_address', response.message.address_html);\r\n } else {\r\n frappe.msgprint(__('No address found for the selected customer.'));\r\n }\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 0,
|
||||
"modified": "2025-01-24 14:05:52.607856",
|
||||
"module": "Brotherton SOP",
|
||||
"name": "address concatenate",
|
||||
"script": "frappe.ui.form.on('<Address>', {\n validate: function(frm) {\n // Concatenate the fields\n frm.set_value('address_line1', \n (frm.doc.custom_street_number || '') + ', ' + \n (frm.doc.custom_directional || '') + ', ' + \n (frm.doc.custom_street_name || '') + ', ' + \n (frm.doc.custom_street_suffix || '')\n );\n }\n});",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 1,
|
||||
"modified": "2025-05-15 12:30:18.280633",
|
||||
"module": null,
|
||||
"name": "Address Validation",
|
||||
"script": "const renameInProgress = new Set();\r\nfunction rename(frm) {\r\n const newName = frm.doc.address_line1;\r\n if (!renameInProgress.has(newName)) {\r\n renameInProgress.add(newName);\r\n return frappe.call({\r\n method: 'frappe.rename_doc',\r\n freeze: true,\r\n \t\tfreeze_message: \"Updating name...\",\r\n args: {\r\n doctype: 'Address',\r\n old: frm.doc.name,\r\n new: newName,\r\n merge: 0\r\n },\r\n \r\n // From https://github.com/frappe/frappe/blob/f708acb59e3cdc9ec1a91bcdfc0f36d6d012cbf5/frappe/public/js/frappe/model/model.js#L787\r\n callback: function (r, rt) {\r\n \t\t\tif (!r.exc) {\r\n \t\t\t\t$(document).trigger(\"rename\", [\r\n \t\t\t\t\t'Address',\r\n \t\t\t\t\tfrm.doc.name,\r\n \t\t\t\t\tr.message || newName,\r\n \t\t\t\t]);\r\n \t\t\t}\r\n \t\t\trenameInProgress.delete(newName);\r\n \t\t},\r\n })\r\n }\r\n}\r\n\r\nfrappe.ui.form.on('Address', {\r\n // Trigger validation and formatting on refresh\r\n refresh: function (frm) {\r\n // Trigger field validation when the form is refreshed\r\n frm.trigger('format_address_fields');\r\n \r\n // Rename to remove appended type, if needed\r\n if (!frm.is_new() && frm.doc.name !== frm.doc.address_line1 && frm.doc.address_type === \"Other\") {\r\n // Trial and error, seems like 1 second is needed to not cause issues\r\n setTimeout(() => rename(frm), 1000);\r\n }\r\n },\r\n\r\n // Watch for changes in address_line1 and address_line2 fields\r\n address_line1: function (frm) {\r\n frm.trigger('format_address_fields');\r\n if (frm.doc.address_line1 && frm.doc.address_title !== frm.doc.address_line1) {\r\n frm.set_value('address_title', frm.doc.address_line1);\r\n }\r\n },\r\n address_line2: function (frm) {\r\n frm.trigger('format_address_fields');\r\n },\r\n\r\n // Format and validate address fields\r\n format_address_fields: function (frm) {\r\n // Helper function to capitalize text and remove punctuation\r\n function format_text(field) {\r\n let value = frm.doc[field] || '';\r\n // Remove punctuation and capitalize the text\r\n let formatted_value = value\r\n .replace(/[.,!?;:']/g, '') // Remove punctuation\r\n .toUpperCase(); // Capitalize text\r\n return formatted_value;\r\n }\r\n\r\n // Format address_line1 and address_line2\r\n const formatted_line1 = format_text('address_line1');\r\n const formatted_line2 = format_text('address_line2');\r\n\r\n // Set the formatted values back to the form\r\n if (formatted_line1 !== frm.doc.address_line1) {\r\n frm.set_value('address_line1', formatted_line1);\r\n }\r\n if (formatted_line2 !== frm.doc.address_line2) {\r\n frm.set_value('address_line2', formatted_line2);\r\n }\r\n\r\n // Validate if punctuation exists (after formatting)\r\n if (/[.,!?;:']/.test(frm.doc.address_line1) || /[.,!?;:']/.test(frm.doc.address_line2)) {\r\n frappe.msgprint(__('Punctuation is not allowed in address fields.'));\r\n }\r\n },\r\n\r\n // Before saving the document, validate the fields\r\n validate: function (frm) {\r\n const invalidFields = [];\r\n\r\n // Check if punctuation still exists in address fields\r\n ['address_line1', 'address_line2'].forEach(field => {\r\n if (/[.,!?;:']/.test(frm.doc[field])) {\r\n invalidFields.push(field);\r\n }\r\n });\r\n\r\n // If invalid fields exist, stop the save process and alert the user\r\n if (invalidFields.length > 0) {\r\n frappe.msgprint(__('Punctuation is not allowed in address fields: ') + invalidFields.join(', '));\r\n frappe.validated = false; // Prevent saving\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "QB Export",
|
||||
"enabled": 1,
|
||||
"modified": "2025-02-04 03:12:39.473331",
|
||||
"module": null,
|
||||
"name": "QB Export",
|
||||
"script": "frappe.ui.form.on('QB Export', {\n\tonload(frm) {\n\t if (!frm.doc.start_date) frm.doc.start_date = new Date(new Date().getFullYear(), new Date().getMonth()-1)\n if (!frm.doc.end_date) frm.doc.end_date = new Date(new Date().getFullYear(), new Date().getMonth(), 0)\n }\n})",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Customer",
|
||||
"enabled": 1,
|
||||
"modified": "2025-02-05 05:28:54.588303",
|
||||
"module": null,
|
||||
"name": "Customer - Allow Multiple Addresses To Be Asssigned",
|
||||
"script": "frappe.ui.form.on('Custom Customer Address Link', {\r\n address_name: function(frm, cdt, cdn) {\r\n let row = locals[cdt][cdn];\r\n \r\n if (row.address_name) {\r\n frappe.call({\r\n method: \"frappe.client.get\",\r\n args: {\r\n doctype: \"Address\",\r\n name: row.address_name\r\n },\r\n callback: function(r) {\r\n if (r.message) {\r\n frappe.model.set_value(cdt, cdn, \"address_line1\", r.message.address_line1);\r\n frappe.model.set_value(cdt, cdn, \"city\", r.message.city);\r\n frappe.model.set_value(cdt, cdn, \"state\", r.message.state);\r\n frappe.model.set_value(cdt, cdn, \"pincode\", r.message.pincode);\r\n frappe.model.set_value(cdt, cdn, \"address_type\", r.message.address_type);\r\n }\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Sales Order",
|
||||
"enabled": 1,
|
||||
"modified": "2025-03-05 05:13:03.150519",
|
||||
"module": null,
|
||||
"name": "Carry Over Installation Address",
|
||||
"script": "frappe.ui.form.on(\"Sales Order\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.quotation) {\r\n frappe.db.get_value(\"Quotation\", frm.doc.quotation, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n \r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Sales Invoice",
|
||||
"enabled": 1,
|
||||
"modified": "2025-03-05 05:13:42.768423",
|
||||
"module": null,
|
||||
"name": "Quotation to Sales Invoice Carry Over",
|
||||
"script": "frappe.ui.form.on(\"Sales Invoice\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.quotation) {\r\n frappe.db.get_value(\"Quotation\", frm.doc.quotation, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Work Order",
|
||||
"enabled": 1,
|
||||
"modified": "2025-03-05 07:09:42.680637",
|
||||
"module": null,
|
||||
"name": "Sales Order to Work Order Carry Over",
|
||||
"script": "frappe.ui.form.on(\"Work Order\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.sales_order) {\r\n frappe.db.get_value(\"Sales Order\", frm.doc.sales_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Delivery Note",
|
||||
"enabled": 1,
|
||||
"modified": "2025-03-05 05:15:06.408083",
|
||||
"module": null,
|
||||
"name": "Sales Order to Delivery Note Carry Over",
|
||||
"script": "frappe.ui.form.on(\"Delivery Note\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.against_sales_order) {\r\n frappe.db.get_value(\"Sales Order\", frm.doc.against_sales_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Payment Entry",
|
||||
"enabled": 1,
|
||||
"modified": "2025-04-25 04:17:25.626671",
|
||||
"module": null,
|
||||
"name": "Sales Invoice to Payment Entry Carry Over",
|
||||
"script": "frappe.ui.form.on(\"Payment Entry\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.reference_doctype && frm.doc.reference_name) {\r\n let source_doctype = frm.doc.reference_doctype;\r\n let source_name = frm.doc.reference_name;\r\n\r\n frappe.db.get_value(\"Sales Invoice\", frm.doc.sales_invoice, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Job Card",
|
||||
"enabled": 1,
|
||||
"modified": "2025-03-05 05:22:37.566028",
|
||||
"module": null,
|
||||
"name": "Work Order to Job Card Carry Over",
|
||||
"script": "frappe.ui.form.on(\"Job Card\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.work_order) {\r\n frappe.db.get_value(\"Work Order\", frm.doc.work_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_jobsite\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Project",
|
||||
"enabled": 1,
|
||||
"modified": "2025-03-05 05:19:29.828616",
|
||||
"module": null,
|
||||
"name": "Sales Order to Project Carry Over",
|
||||
"script": "frappe.ui.form.on(\"Project\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.sales_order) {\r\n frappe.db.get_value(\"Sales Order\", frm.doc.sales_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 1,
|
||||
"modified": "2025-09-02 11:38:09.302073",
|
||||
"module": null,
|
||||
"name": "Buttons on Address Doctype",
|
||||
"script": "frappe.ui.form.on(\"Address\", {\r\n refresh: function (frm) {\r\n if (!frm.is_new()) {\r\n frm.add_custom_button(__('Schedule On-Site Meeting'), function () {\r\n sessionStorage.setItem('on-site-meeting-address', frm.doc.name);\r\n frappe.set_route(\"List\", \"On-Site Meeting\", \"Calendar\", \"On-Site Meeting Calendar\");\r\n }, __(\"Create\"));\r\n \r\n // Add button to create a Sales Order\r\n frm.add_custom_button(__('Create Sales Order'), function () {\r\n frappe.new_doc('Sales Order', {\r\n custom_installation_address: frm.doc.name,\r\n customer: frm.doc.custom_customer_to_bill\r\n });\r\n }, __(\"Create\"));\r\n \r\n // Add button to create a Quotation\r\n frm.add_custom_button(__('Create Quotation'), function () {\r\n frappe.new_doc('Quotation', {\r\n custom_installation_address: frm.doc.name,\r\n quotation_to: 'Customer',\r\n party_name: frm.doc.custom_customer_to_bill\r\n });\r\n }, __(\"Create\"));\r\n // Add button to create a new Service Appointment\r\n frm.add_custom_button(__('Create Service Appointment'), function() {\r\n frappe.new_doc('Service Appointment', {\r\n //custom_location_of_meeting: frm.doc.name,\r\n contact: frm.doc.custom_customer_to_bill\r\n });\r\n }, __(\"Create\"));\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "On-Site Meeting",
|
||||
"enabled": 1,
|
||||
"modified": "2025-04-17 12:19:42.335880",
|
||||
"module": null,
|
||||
"name": "On-Site Meeting - autofill address from session Enabled",
|
||||
"script": "frappe.ui.form.on('On-Site Meeting', {\n\tonload(frm) {\n\t if (sessionStorage.getItem('on-site-meeting-address')) {\n\t frm.set_value('address', sessionStorage.getItem('on-site-meeting-address'));\n\t console.log('start_time', frm.doc.start_time)\n\t if (frm.doc.start_time) {\n\t frm.save();\n\t }\n\t }\n\t},\n\tvalidate(frm) {\n\t let end = new Date(frm.doc.start_time + \" GMT\");\n\t end.setHours(end.getHours() + 1)\n\t frm.set_value('end_time', end.toISOString().replace('T', ' ').split('.')[0]);\n\t},\n\tafter_save(frm) {\n\t console.log('on submit')\n\t if (frm.doc.address === sessionStorage.getItem('on-site-meeting-address')) {\n\t frappe.set_route('Form', 'Address', frm.doc.address);\n\t }\n\t sessionStorage.removeItem('on-site-meeting-address');\n\t},\n\tafter_cancel(frm) {\n\t sessionStorage.removeItem('on-site-meeting-address');\n\t},\n\tafter_discard(frm) {\n\t sessionStorage.removeItem('on-site-meeting-address');\n\t},\n})",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Quotation",
|
||||
"enabled": 1,
|
||||
"modified": "2025-05-08 13:56:46.345762",
|
||||
"module": null,
|
||||
"name": "Quick-send quote button (SNW)",
|
||||
"script": "frappe.ui.form.on('Quotation', {\n\trefresh(frm) {\n\t if (frm.doc.status == \"Open\" && frm.doc.company == \"Sprinklers Northwest\") {\n \t\tfrm.add_custom_button(__('Send Email'), async function () {\n \t\t const party = frm.doc.quotation_to && frm.doc.party_name ? await frappe.db.get_doc(frm.doc.quotation_to, frm.doc.party_name) : null;\n \t\t const address = frm.doc.custom_installation_address ? await frappe.db.get_doc('Address', frm.doc.custom_installation_address) : null;\n \t\t \n \t\t let email = null;\n \t\t if (party && party.email_id) {\n \t\t email = party.email_id;\n \t\t } else if (party && party.email_ids && party.email_ids.length) {\n \t\t const primary = party.email_ids.find(email => email.is_primary);\n \t\t if (primary) email = primary.email_id;\n \t\t else email = party.email_ids[0].email_id;\n \t\t } else if (address && address.email_id) {\n \t\t email = address.email_id;\n \t\t }\n \t\t \n \t\t if (!email) {\n \t\t frappe.msgprint(\"No email on customer or address\");\n \t\t return;\n \t\t }\n \t\t \n frappe.confirm(`Send quote to ${frm.doc.party_name} (${email})?`,\n async () => {\n const { message: { subject, message } } = await frappe.call({\n method: \"frappe.email.doctype.email_template.email_template.get_email_template\",\n type: \"POST\",\n args: {\n template_name: 'Quote with Actions - SNW',\n doc: frm.doc\n }\n });\n \n await frappe.call({\n method: \"frappe.core.doctype.communication.email.make\",\n type: 'POST',\n args: {\n recipients: email, \n subject,\n content: message,\n doctype: 'Quotation',\n name: frm.doc.name,\n send_email: 1,\n send_me_a_copy: 0,\n print_format: 'SNW Quotations',\n email_template: 'Quote with Actions - SNW',\n read_receipt: 1,\n print_letterhead: 1\n }\n });\n \n frappe.msgprint(\"Email Queued\");\n },\n null\n );\n });\n\t }\n\t}\n})",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Payment Entry",
|
||||
"enabled": 1,
|
||||
"modified": "2025-05-02 11:03:34.086145",
|
||||
"module": null,
|
||||
"name": "Installation Address Carry Over from Sales Order to Payment Address",
|
||||
"script": "frappe.ui.form.on(\"Payment Entry\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.against_sales_order) {\r\n frappe.db.get_value(\"Sales Order\", frm.doc.against_sales_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n\r\n\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Payment Entry",
|
||||
"enabled": 1,
|
||||
"modified": "2025-05-08 13:20:53.255294",
|
||||
"module": null,
|
||||
"name": "Fetch billing customer from installation address",
|
||||
"script": "frappe.ui.form.on('Payment Entry', {\n\tcustom_installation_address(frm) {\n\t if (frm.doc.custom_installation_address) {\n\t frappe.db.get_doc('Address', frm.doc.custom_installation_address).then(doc => {\n frm.set_value('party_type', 'Customer')\n frm.set_value('party', doc.custom_customer_to_bill)\n })\n\t }\n\t}\n})",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Pre-Built Routes",
|
||||
"enabled": 1,
|
||||
"modified": "2025-06-17 02:12:53.095227",
|
||||
"module": null,
|
||||
"name": "Filter Route Technicians by Role",
|
||||
"script": "frappe.ui.form.on('Pre-Built Routes', {\r\n onload: function(frm) {\r\n\r\n // Filter Crew Leader: Only show employees with designation = \"Crew Lead\"\r\n frm.set_query('crew_leader', () => {\r\n return {\r\n filters: {\r\n designation: 'Crew Lead',\r\n status: 'Active'\r\n }\r\n };\r\n });\r\n\r\n // Filter Technicians: Only show active employees who are NOT crew leads\r\n frm.fields_dict['assigned_technicians'].grid.get_field('employee').get_query = function() {\r\n return {\r\n filters: [\r\n ['designation', '!=', 'Crew Lead'],\r\n ['status', '=', 'Active']\r\n ]\r\n };\r\n };\r\n\r\n },\r\n\r\n // Optional: Prevent assigning crew leader as a technician too\r\n validate: function(frm) {\r\n let crew_leader = frm.doc.crew_leader;\r\n let techs = frm.doc.assigned_technicians || [];\r\n\r\n let duplicate = techs.find(row => row.employee === crew_leader);\r\n if (duplicate) {\r\n frappe.msgprint(__('Crew Leader cannot be listed as a Technician.'));\r\n frappe.validated = false;\r\n }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Pre-Built Routes",
|
||||
"enabled": 1,
|
||||
"modified": "2025-06-17 02:20:56.937981",
|
||||
"module": null,
|
||||
"name": "Auto-Fetch Relevant Sales Order",
|
||||
"script": "// Trigger this logic whenever the address_name field is changed\nfrappe.ui.form.on('Assigned Address', {\n address_name: function(frm, cdt, cdn) {\n let row = locals[cdt][cdn];\n// Exit early if no address is selected\n if (!row.address_name) return;\n // Call the server to fetch all Sales Orders with this installation address\n frappe.call({\n method: \"frappe.client.get_list\",\n args: {\n doctype: \"Sales Order\",\n filters: {\n custom_installation_address: row.address_name\n },\n fields: [\"name\", \"customer\", \"transaction_date\", \"status\", \"grand_total\"]\n },\n callback: function(response) {\n const orders = response.message;\n // Case: No Sales Orders found\n if (!orders || orders.length === 0) {\n frappe.msgprint(\"No Sales Orders found for this address.\");\n frappe.model.set_value(cdt, cdn, \"linked_sales_order\", \"\");\n return;\n }\n // Case: Exactly one Sales Order found — auto-select it\n if (orders.length === 1) {\n frappe.model.set_value(cdt, cdn, \"linked_sales_order\", orders[0].name);\n return;\n }\n\n // Case: Multiple Sales Orders found — show a dialog to select one\n\n // Create a user-friendly list of options for the select field\n const options = orders.map(order => {\n return {\n label: `${order.name} | ${order.customer} | ${frappe.datetime.str_to_user(order.transaction_date)} | ${order.status} | $${order.grand_total}`,\n value: order.name\n };\n });\n // Define and show a custom dialog for selecting the appropriate Sales Order\n const dialog = new frappe.ui.Dialog({\n title: \"Select Sales Order for \" + row.address_name,\n fields: [\n {\n fieldname: 'selected_so',\n label: 'Sales Order',\n fieldtype: 'Select',\n options: options,\n reqd: 1\n }\n ],\n primary_action_label: 'Select',\n primary_action(values) {\n frappe.model.set_value(cdt, cdn, \"linked_sales_order\", values.selected_so);\n dialog.hide();\n }\n });\n\n dialog.show();\n }\n });\n }\n});\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Project",
|
||||
"enabled": 1,
|
||||
"modified": "2025-08-29 17:31:31.663751",
|
||||
"module": null,
|
||||
"name": "Project Warranty Countdown",
|
||||
"script": "frappe.ui.form.on('Project', {\n\trefresh(frm) {\n\t\tif (frm.doc.status == \"Completed\" && frm.doc.project_template == \"SNW Install\") {\n\t\t let message;\n\t\t const days = Math.abs(frappe.datetime.get_day_diff(frm.doc.custom_warranty_expiration_date, frm.doc.custom_completion_date));\n\t\t let dayMessage = days == 1 ? \"day\" : \"days\";\n\t\t if (frappe.datetime.get_today() <= frm.doc.custom_warranty_expiration_date) {\n\t\t message = `Warranty is valid for ${days} more ${dayMessage}`;\n\t\t } else {\n\t\t message = `Warranty has expired ${days} ${dayMessage} ago.`;\n\t\t }\n\t\t frm.set_value(\"custom_warranty_information\", message);\n\t\t}\n\t}\n});\n",
|
||||
"view": "Form"
|
||||
},
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Client Script",
|
||||
"dt": "Address",
|
||||
"enabled": 1,
|
||||
"modified": "2025-05-15 09:21:20.813661",
|
||||
"module": null,
|
||||
"name": "Address Error Fix",
|
||||
"script": "frappe.ui.form.on('Address', {\r\n validate: function (frm) {\r\n // Default value for the custom field (if it doesn't exist in Address DocType)\r\n if (!frm.doc.hasOwnProperty('custom_is_your_company_address')) {\r\n frm.doc.custom_is_your_company_address = false;\r\n }\r\n\r\n // Custom validation logic\r\n // if (frm.doc.custom_is_your_company_address && !frm.doc.some_other_field) {\r\n // frappe.throw(__('Please ensure that the required fields are filled out for Company Address.'));\r\n // }\r\n }\r\n});\r\n",
|
||||
"view": "Form"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -192,9 +192,6 @@ doc_events = {
|
|||
"Task": {
|
||||
"before_insert": "custom_ui.events.task.before_insert",
|
||||
"after_insert": "custom_ui.events.task.after_insert"
|
||||
},
|
||||
"Bid Meeting Note Form": {
|
||||
"after_insert": "custom_ui.events.general.attach_bid_note_form_to_project_template"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -208,26 +205,79 @@ fixtures = [
|
|||
{
|
||||
"dt": "DocType",
|
||||
"filters": [
|
||||
["custom", "=", 1]
|
||||
["name", "in", [
|
||||
"Quotation Template",
|
||||
"Quotation Template Item",
|
||||
"Customer Company Link",
|
||||
"Customer Address Link",
|
||||
"Customer Contact Link",
|
||||
|
||||
# New link doctypes
|
||||
"Customer Project Link",
|
||||
"Customer Quotation Link",
|
||||
"Customer Sales Order Link",
|
||||
"Customer On-Site Meeting Link",
|
||||
"Lead Address Link",
|
||||
"Lead Contact Link",
|
||||
"Lead Companies Link",
|
||||
"Lead Quotation Link",
|
||||
"Lead On-Site Meeting Link",
|
||||
"Address Project Link",
|
||||
"Address Quotation Link",
|
||||
"Address On-Site Meeting Link",
|
||||
"Address Sales Order Link",
|
||||
"Address Contact Link",
|
||||
"Address Company Link",
|
||||
"Contact Address Link",
|
||||
"Address Task Link",
|
||||
"Customer Task Link"
|
||||
]]
|
||||
]
|
||||
},
|
||||
{
|
||||
"dt": "Custom Field",
|
||||
"filters": [
|
||||
["dt", "=", "Quotation"],
|
||||
["fieldname", "in", [
|
||||
"custom_quotation_template",
|
||||
"custom_project_template"
|
||||
]]
|
||||
]
|
||||
},
|
||||
{
|
||||
"dt": "Custom Field",
|
||||
"filters": [
|
||||
["dt", "=", "Sales Order"],
|
||||
["fieldname", "=", "custom_project_template"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"dt": "Custom Field",
|
||||
"filters": [
|
||||
["dt", "=", "Lead"],
|
||||
["fieldname", "=", "custom_customer_name"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"dt": "Custom Field",
|
||||
"filters": [
|
||||
["dt", "=", "Project Template"],
|
||||
["fieldname", "=", "company"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"dt": "Property Setter",
|
||||
"filters": [
|
||||
["doc_type", "=", "Lead"],
|
||||
["doc_type", "=", "Project"],
|
||||
["doc_type", "=", "Address"]
|
||||
]
|
||||
}
|
||||
|
||||
# These don't have reliable flags → export all
|
||||
{"dt": "Custom Field"},
|
||||
{"dt": "Property Setter"},
|
||||
{"dt": "Client Script"},
|
||||
{"dt": "Server Script"},
|
||||
{"dt": "Report"},
|
||||
{"dt": "Print Format"},
|
||||
{"dt": "Dashboard"},
|
||||
{"dt": "Workspace"},
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Scheduled Tasks
|
||||
# ---------------
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,6 @@ def after_install():
|
|||
frappe.reload_doctype("On-Site Meeting")
|
||||
update_onsite_meeting_fields()
|
||||
update_address_fields()
|
||||
check_and_create_holiday_list()
|
||||
create_project_templates()
|
||||
create_task_types()
|
||||
create_tasks()
|
||||
create_bid_meeting_note_form_templates()
|
||||
build_frontend()
|
||||
|
||||
def after_migrate():
|
||||
|
|
@ -38,10 +33,6 @@ def after_migrate():
|
|||
frappe.reload_doctype(doctype)
|
||||
|
||||
check_and_create_holiday_list()
|
||||
# create_project_templates()
|
||||
create_task_types()
|
||||
# create_tasks
|
||||
# create_bid_meeting_note_form_templates()
|
||||
|
||||
# update_address_fields()
|
||||
# build_frontend()
|
||||
|
|
@ -1029,293 +1020,3 @@ def get_all_sundays(year):
|
|||
d += timedelta(days=7)
|
||||
|
||||
return sundays
|
||||
|
||||
def create_task_types():
|
||||
task_types = [
|
||||
{
|
||||
"title": "811/Locate",
|
||||
"name": "811/Locate",
|
||||
"description": "Utility locate request prior to installation start.",
|
||||
"base_date": "Start",
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Scheduled",
|
||||
},
|
||||
{
|
||||
"title": "Permit",
|
||||
"name": "Permit",
|
||||
"description": "Permits required prior to installation start.",
|
||||
"base_date": "Start",
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Scheduled",
|
||||
},
|
||||
{
|
||||
"title": "1/2 Down Payment",
|
||||
"name": "1/2 Down Payment",
|
||||
"description": "Collect half down payment on project creation.",
|
||||
"calculate_from": "Project",
|
||||
"trigger": "Created",
|
||||
"no_due_date": 1,
|
||||
},
|
||||
{
|
||||
"title": "Machine Staging",
|
||||
"name": "Machine Staging",
|
||||
"description": "Stage machinery one day before installation start.",
|
||||
"base_date": "Start",
|
||||
"offset_days": 1,
|
||||
"offset_direction": "Before",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Scheduled",
|
||||
},
|
||||
{
|
||||
"title": "Final Invoice",
|
||||
"name": "Final Invoice",
|
||||
"description": "Send final invoice within 5 days of job completion.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 5,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
{
|
||||
"title": "Backflow Test",
|
||||
"name": "Backflow Test",
|
||||
"description": "Backflow test after job completion if quoted.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 0,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
{
|
||||
"title": "Schedule Permit Inspection",
|
||||
"name": "Schedule Permit Inspection",
|
||||
"description": "Schedule permit inspection 5 days after completion.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 5,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
{
|
||||
"title": "15 Day Warranty Follow-Up",
|
||||
"name": "15 Day Warranty Follow-Up",
|
||||
"description": "15-day warranty follow-up after completion.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 15,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
{
|
||||
"title": "30 Day Warranty Check",
|
||||
"name": "30 Day Warranty Check",
|
||||
"description": "30-day warranty check after completion.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 30,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
{
|
||||
"title": "30 Day Payment Reminder",
|
||||
"name": "30 Day Payment Reminder",
|
||||
"description": "Payment reminder sent 30 days after completion.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 30,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
{
|
||||
"title": "60 Day Late Payment Notice",
|
||||
"name": "60 Day Late Payment Notice",
|
||||
"description": "Late payment notification at 60 days post completion.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 60,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
{
|
||||
"title": "80 Day Lien Notice",
|
||||
"name": "80 Day Lien Notice",
|
||||
"description": "Lien notice if payment is still late after 80 days.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 80,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
{
|
||||
"title": "365 Day Warranty Call / Walk",
|
||||
"name": "365 Day Warranty Call / Walk",
|
||||
"description": "One-year warranty call or walk-through.",
|
||||
"base_date": "Completion",
|
||||
"offset_days": 365,
|
||||
"offset_direction": "After",
|
||||
"calculate_from": "Service Appointment",
|
||||
"trigger": "Completed",
|
||||
},
|
||||
]
|
||||
|
||||
for task_type in task_types:
|
||||
# Idempotency check
|
||||
if frappe.db.exists("Task Type", task_type["name"]):
|
||||
continue
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Task Type",
|
||||
"title": task_type["title"],
|
||||
"name": task_type["name"],
|
||||
"description": task_type["description"],
|
||||
"base_date": task_type.get("base_date"),
|
||||
"offset_days": task_type.get("offset_days", 0),
|
||||
"offset_direction": task_type.get("offset_direction"),
|
||||
"calculate_from": task_type.get("calculate_from", "Service Appointment"),
|
||||
"trigger": task_type["trigger"],
|
||||
"no_due_date": task_type.get("no_due_date", 0),
|
||||
})
|
||||
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
def create_tasks():
|
||||
print("\n🔧 Creating default Tasks if they do not exist...")
|
||||
default_tasks = [
|
||||
{
|
||||
"task_name": "Initial Consultation",
|
||||
"description": "Conduct an initial consultation with the client to discuss project requirements.",
|
||||
"status": "Open",
|
||||
"priority": "High",
|
||||
"type": "Consultation"
|
||||
},
|
||||
{
|
||||
"task_name": "Site Survey",
|
||||
"description": "Perform a site survey to assess conditions and gather necessary data.",
|
||||
"status": "Open",
|
||||
"priority": "Medium"
|
||||
},
|
||||
{
|
||||
"task_name": "Design Proposal",
|
||||
"description": "Prepare and present a design proposal based on client needs and site survey findings.",
|
||||
"status": "Open",
|
||||
"priority": "High"
|
||||
}
|
||||
]
|
||||
|
||||
for task in default_tasks:
|
||||
if frappe.db.exists("Task", task["task_name"]):
|
||||
continue
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Task",
|
||||
"is_template": 1,
|
||||
"task_name": task["task_name"],
|
||||
"description": task["description"],
|
||||
"status": task["status"],
|
||||
"priority": task["priority"],
|
||||
"type": task["type"]
|
||||
})
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
def create_project_templates():
|
||||
"""Create default Project Templates if they do not exist."""
|
||||
print("\n🔧 Checking for default Project Templates...")
|
||||
templates = {
|
||||
"snw_templates": [
|
||||
{
|
||||
"name": "SNW Install",
|
||||
"project_type": "Service",
|
||||
"company": "Sprinklers Northwest",
|
||||
"calendar_color": "#FF5733" # Example color
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def create_bid_meeting_note_form_templates():
|
||||
"""Create Bid Meeting Note Forms if they do not exist."""
|
||||
print("\n🔧 Checking for Bid Meeting Note Forms...")
|
||||
forms = {
|
||||
"Sprinklers Northwest": [
|
||||
{
|
||||
"name": "SNW Install Bid Meeting Notes",
|
||||
"title": "SNW Install Bid Meeting Notes",
|
||||
"description": "Notes form for SNW Install bid meetings.",
|
||||
"project_template": "SNW Install",
|
||||
"fields": [{
|
||||
"label": "Locate Needed",
|
||||
"type": "Check",
|
||||
"required": 1,
|
||||
"help_text": "Indicate if a locate is needed for this project.",
|
||||
"row": 1,
|
||||
"column": 1
|
||||
},
|
||||
{
|
||||
"label": "Permit Needed",
|
||||
"type": "Check",
|
||||
"required": 1,
|
||||
"help_text": "Indicate if a permit is needed for this project.",
|
||||
"row": 1,
|
||||
"column": 2
|
||||
},
|
||||
{
|
||||
"label": "Back Flow Test Required",
|
||||
"type": "Check",
|
||||
"required": 1,
|
||||
"help_text": "Indicate if a backflow test is required after installation.",
|
||||
"row": 1,
|
||||
"column": 3
|
||||
},
|
||||
{
|
||||
"label": "Machine Access",
|
||||
"type": "Check",
|
||||
"required": 1,
|
||||
"row": 2,
|
||||
"column": 1
|
||||
},
|
||||
{
|
||||
"label": "Machines",
|
||||
"type": "Multi-Select",
|
||||
"options": "MT, Skip Steer, Excavator-E-50, Link Belt, Tre?, Forks, Auger, Backhoe, Loader, Duzer",
|
||||
"required": 0,
|
||||
"include_options": 1,
|
||||
"conditional_on_field": "Machine Access",
|
||||
"row": 2,
|
||||
"column": 2
|
||||
},
|
||||
{
|
||||
"label": "Materials Required",
|
||||
"type": "Check",
|
||||
"required": 1,
|
||||
"row": 3,
|
||||
"column": 0
|
||||
},
|
||||
{
|
||||
"label": "Materials",
|
||||
"type": "Multi-Select w/ Quantity",
|
||||
"required": 0,
|
||||
"doctype_for_select": "Item",
|
||||
"conditional_on_field": "Materials Required",
|
||||
"doctype_label_field": "itemName",
|
||||
"row": 4,
|
||||
"column": 0
|
||||
}]
|
||||
}]}
|
||||
for company, form_list in forms.items():
|
||||
for form in form_list:
|
||||
if frappe.db.exists("Bid Meeting Note Form", form["name"]):
|
||||
continue
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Bid Meeting Note Form",
|
||||
"company": company,
|
||||
"title": form["title"],
|
||||
"description": form["description"],
|
||||
"project_template": form["project_template"],
|
||||
"fields": form["fields"]
|
||||
})
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,4 @@ from .contact_service import ContactService
|
|||
from .db_service import DbService
|
||||
from .client_service import ClientService
|
||||
from .estimate_service import EstimateService
|
||||
from .onsite_meeting_service import OnSiteMeetingService
|
||||
from .task_service import TaskService
|
||||
from .service_appointment_service import ServiceAppointmentService
|
||||
from .onsite_meeting_service import OnSiteMeetingService
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import frappe
|
||||
from custom_ui.services import ContactService, AddressService, ClientService, DbService
|
||||
|
||||
class ServiceAppointmentService:
|
||||
|
||||
@staticmethod
|
||||
def create(data):
|
||||
"""Create a new Service Appointment document."""
|
||||
print("DEBUG: Creating Service Appointment with data:", data)
|
||||
service_appointment_doc = frappe.get_doc({
|
||||
"doctype": "Service Appointment",
|
||||
**data
|
||||
})
|
||||
service_appointment_doc.insert()
|
||||
print("DEBUG: Created Service Appointment with name:", service_appointment_doc.name)
|
||||
return service_appointment_doc
|
||||
|
||||
@staticmethod
|
||||
def get_full_dict(service_appointment_name: str) -> dict:
|
||||
"""Retrieve a Service Appointment document as a full dictionary."""
|
||||
print(f"DEBUG: Retrieving Service Appointment document with name: {service_appointment_name}")
|
||||
service_appointment = frappe.get_doc("Service Appointment", service_appointment_name).as_dict()
|
||||
service_appointment["service_address"] = AddressService.get_or_throw(service_appointment["service_address"]).as_dict()
|
||||
service_appointment["customer"] = ClientService.get_client_or_throw(service_appointment["customer"]).as_dict()
|
||||
service_appointment["project"] = DbService.get_doc_or_throw("Project", service_appointment["project"]).as_dict()
|
||||
|
||||
return service_appointment
|
||||
|
||||
@staticmethod
|
||||
def update_scheduled_dates(service_appointment_name: str, start_date, end_date, start_time=None, end_time=None):
|
||||
"""Update the scheduled start and end dates of a Service Appointment."""
|
||||
print(f"DEBUG: Updating scheduled dates for Service Appointment {service_appointment_name} to start: {start_date}, end: {end_date}")
|
||||
service_appointment = DbService.get_or_throw("Service Appointment", service_appointment_name)
|
||||
service_appointment.expected_start_date = start_date
|
||||
service_appointment.expected_end_date = end_date
|
||||
if start_time:
|
||||
service_appointment.expected_start_time = start_time
|
||||
if end_time:
|
||||
service_appointment.expected_end_time = end_time
|
||||
service_appointment.save()
|
||||
print(f"DEBUG: Updated scheduled dates for Service Appointment {service_appointment_name}")
|
||||
return service_appointment
|
||||
|
||||
@staticmethod
|
||||
def update_field(service_appointment_name: str, updates: list[tuple[str, any]]):
|
||||
"""Update specific fields of a Service Appointment."""
|
||||
print(f"DEBUG: Updating fields for Service Appointment {service_appointment_name} with updates: {updates}")
|
||||
service_appointment = DbService.get_or_throw("Service Appointment", service_appointment_name)
|
||||
for field, value in updates:
|
||||
setattr(service_appointment, field, value)
|
||||
service_appointment.save()
|
||||
print(f"DEBUG: Updated fields for Service Appointment {service_appointment_name}")
|
||||
return service_appointment
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
import frappe
|
||||
from frappe.utils.safe_exec import safe_eval
|
||||
from datetime import timedelta, datetime, date
|
||||
class TaskService:
|
||||
|
||||
|
||||
@staticmethod
|
||||
def calculate_and_set_due_dates(task_names: list[str], event: str):
|
||||
"""Calculate the due date for a list of tasks based on their expected end dates."""
|
||||
for task_name in task_names:
|
||||
TaskService.check_and_update_task_due_date(task_name, event)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_tasks_by_project(project_name: str):
|
||||
"""Retrieve all tasks associated with a given project."""
|
||||
task_names = frappe.get_all("Task", filters={"project": project_name}, pluck="name")
|
||||
tasks = [frappe.get_doc("Task", task_name) for task_name in task_names]
|
||||
return tasks
|
||||
|
||||
@staticmethod
|
||||
def check_and_update_task_due_date(task_name: str, event: str):
|
||||
"""Determine the triggering configuration for a given task."""
|
||||
task_type_doc = TaskService.get_task_type_doc(task_name)
|
||||
if task_type_doc.trigger != event:
|
||||
print(f"DEBUG: Task {task_name} trigger {task_type_doc.trigger} does not match event {event}, skipping calculation.")
|
||||
return
|
||||
if task_type_doc.logic_key:
|
||||
print(f"DEBUG: Task {task_name} has a logic key set, skipping calculations and running logic.")
|
||||
safe_eval(task_type_doc.logic_key, {"task_name": task_name, "task_type_doc": task_type_doc})
|
||||
if task_type_doc.no_due_date:
|
||||
print(f"DEBUG: Task {task_name} is marked as no due date, skipping calculation.")
|
||||
return
|
||||
calculated_from = task_type_doc.calculated_from
|
||||
trigger = task_type_doc.trigger
|
||||
print(f"DEBUG: Calculating triggering data for Task {task_name} from {calculated_from} on trigger {trigger}")
|
||||
|
||||
|
||||
triggering_doc_dict = TaskService.get_triggering_doc_dict(task_name=task_name, task_type_doc=task_type_doc)
|
||||
|
||||
|
||||
calculated_due_date, calculated_start_date = TaskService.calculate_dates(
|
||||
task_name=task_name,
|
||||
triggering_doc_dict=triggering_doc_dict,
|
||||
task_type_doc=task_type_doc
|
||||
)
|
||||
|
||||
update_required = TaskService.determine_update_required(
|
||||
task_name=task_name,
|
||||
calculated_due_date=calculated_due_date,
|
||||
calculated_start_date=calculated_start_date
|
||||
)
|
||||
if update_required:
|
||||
TaskService.update_task_dates(
|
||||
task_name=task_name,
|
||||
calculated_due_date=calculated_due_date,
|
||||
calculated_start_date=calculated_start_date
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_task_type_doc(task_name: str):
|
||||
task_type_name = frappe.get_value("Task", task_name, "task_type")
|
||||
return frappe.get_doc("Task Type", task_type_name)
|
||||
|
||||
@staticmethod
|
||||
def calculate_dates(task_name: str, triggering_doc_dict: dict, task_type_doc) -> tuple[date | None, date | None]:
|
||||
offset_direction = task_type_doc.offset_direction
|
||||
offset_days = task_type_doc.offset_days
|
||||
base_date_field = TaskService.map_base_date_to_field(task_type_doc.base_date)
|
||||
|
||||
if offset_direction == "Before":
|
||||
offset_days = -offset_days
|
||||
|
||||
base_date_field_value = triggering_doc_dict.get(base_date_field)
|
||||
|
||||
if isinstance(base_date_field_value, datetime):
|
||||
base_date_field_value = base_date_field_value.date()
|
||||
|
||||
calculated_due_date = base_date_field_value + timedelta(days=offset_days)
|
||||
calculated_start_date = None
|
||||
if task_type_doc.days > 1:
|
||||
calculated_start_date = calculated_due_date - timedelta(days=task_type_doc.days)
|
||||
print(f"DEBUG: Calculated dates for Task {task_name} - Due Date: {calculated_due_date}, Start Date: {calculated_start_date}")
|
||||
return calculated_due_date, calculated_start_date
|
||||
|
||||
@staticmethod
|
||||
def determine_update_required(task_name: str, calculated_due_date: date | None, calculated_start_date: date | None) -> bool:
|
||||
current_due_date = frappe.get_value("Task", task_name, "expected_end_date")
|
||||
current_start_date = frappe.get_value("Task", task_name, "expected_start_date")
|
||||
if current_due_date != calculated_due_date or current_start_date != calculated_start_date:
|
||||
print(f"DEBUG: Update required for Task {task_name}. Current due date: {current_due_date}, Calculated due date: {calculated_due_date}. Current start date: {current_start_date}, Calculated start date: {calculated_start_date}")
|
||||
return True
|
||||
else:
|
||||
print(f"DEBUG: No update required for Task {task_name}. Dates are up to date.")
|
||||
return False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_triggering_doc_dict(task_name: str, task_type_doc) -> dict | None:
|
||||
project_name = frappe.get_value("Task", task_name, "project")
|
||||
dict = None
|
||||
if task_type_doc.calculated_from == "Project":
|
||||
dict = frappe.get_doc("Project", project_name).to_dict()
|
||||
if task_type_doc.calculated_from == "Service Appointment":
|
||||
service_name = frappe.get_value("Project", project_name, "service_appointment")
|
||||
dict = frappe.get_doc("Service Appointment", service_name).to_dict()
|
||||
if task_type_doc.calculated_from == "Task":
|
||||
project_doc = frappe.get_doc("Project", project_name)
|
||||
for task in project_doc.tasks:
|
||||
if task.task_type == task_type_doc.task_type_calculate_from:
|
||||
dict = frappe.get_doc("Task", task.task).to_dict()
|
||||
print(f"DEBUG: Triggering doc dict for Task {task_name}: {dict}")
|
||||
return dict
|
||||
|
||||
@staticmethod
|
||||
def update_task_dates(task_name: str, calculated_due_date: date | None, calculated_start_date: date | None):
|
||||
task_doc = frappe.get_doc("Task", task_name)
|
||||
task_doc.expected_end_date = calculated_due_date
|
||||
task_doc.expected_start_date = calculated_start_date
|
||||
task_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Updated Task {task_name} with new dates - Start: {calculated_start_date}, End: {calculated_due_date}")
|
||||
|
||||
@staticmethod
|
||||
def map_base_date_to_field(base_date: str) -> str:
|
||||
"""Map a base date configuration to a corresponding field name."""
|
||||
base_date_field_map = {
|
||||
"Start": "expected_start_date",
|
||||
"End": "expected_end_date",
|
||||
"Creation": "creation",
|
||||
"Completion": "actual_end_date"
|
||||
}
|
||||
return base_date_field_map.get(base_date, "expected_end_date")
|
||||
|
||||
@staticmethod
|
||||
def determine_event(triggering_doc) -> str | None:
|
||||
prev_doc = triggering_doc.get_doc_before_save()
|
||||
if prev_doc.expected_end_date != triggering_doc.expected_end_date or prev_doc.expected_start_date != triggering_doc.expected_start_date:
|
||||
return "Scheduled"
|
||||
elif prev_doc.status != triggering_doc.status and triggering_doc.status == "Completed":
|
||||
return "Completed"
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
@ -10,7 +10,6 @@ const FRAPPE_GET_INCOMPLETE_BIDS_METHOD = "custom_ui.api.db.on_site_meetings.get
|
|||
// Estimate methods
|
||||
const FRAPPE_UPSERT_ESTIMATE_METHOD = "custom_ui.api.db.estimates.upsert_estimate";
|
||||
const FRAPPE_GET_ESTIMATES_METHOD = "custom_ui.api.db.estimates.get_estimate_table_data";
|
||||
const FRAPPE_GET_ESTIMATES_TABLE_DATA_V2_METHOD = "custom_ui.api.db.estimates.get_estimate_table_data_v2";
|
||||
const FRAPPE_GET_ESTIMATE_BY_ADDRESS_METHOD = "custom_ui.api.db.estimates.get_estimate_from_address";
|
||||
const FRAPPE_SEND_ESTIMATE_EMAIL_METHOD = "custom_ui.api.db.estimates.send_estimate_email";
|
||||
const FRAPPE_LOCK_ESTIMATE_METHOD = "custom_ui.api.db.estimates.lock_estimate";
|
||||
|
|
@ -48,9 +47,7 @@ const FRAPPE_GET_WARRANTY_CLAIMS_METHOD = "custom_ui.api.db.warranties.get_warra
|
|||
// On-Site Meeting methods
|
||||
const FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD =
|
||||
"custom_ui.api.db.bid_meetings.get_week_bid_meetings";
|
||||
const FRAPPE_GET_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.get_bid_meeting_note_form";
|
||||
const FRAPPE_GET_ONSITE_MEETINGS_METHOD = "custom_ui.api.db.bid_meetings.get_bid_meetings";
|
||||
const FRAPPE_SUBMIT_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.submit_bid_meeting_note_form";
|
||||
// Address methods
|
||||
const FRAPPE_GET_ADDRESSES_METHOD = "custom_ui.api.db.addresses.get_addresses";
|
||||
// Client methods
|
||||
|
|
@ -65,10 +62,6 @@ const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees";
|
|||
const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized";
|
||||
// Other methods
|
||||
const FRAPPE_GET_WEEK_HOLIDAYS_METHOD = "custom_ui.api.db.general.get_week_holidays";
|
||||
const FRAPPE_GET_DOC_LIST_METHOD = "custom_ui.api.db.general.get_doc_list";
|
||||
// Service Appointment methods
|
||||
const FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD = "custom_ui.api.db.service_appointments.get_service_appointments";
|
||||
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_SCHEDULED_DATES_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_scheduled_dates";
|
||||
class Api {
|
||||
// ============================================================================
|
||||
// CORE REQUEST METHOPD
|
||||
|
|
@ -173,18 +166,6 @@ class Api {
|
|||
// ON-SITE MEETING METHODS
|
||||
// ============================================================================
|
||||
|
||||
static async getBidMeetingNoteForm(projectTemplate) {
|
||||
return await this.request(FRAPPE_GET_BID_MEETING_NOTE_FORM_METHOD, { projectTemplate });
|
||||
}
|
||||
|
||||
static async submitBidMeetingNoteForm(data) {
|
||||
return await this.request(FRAPPE_SUBMIT_BID_MEETING_NOTE_FORM_METHOD, {
|
||||
bidMeeting: data.bidMeeting,
|
||||
projectTemplate: data.projectTemplate,
|
||||
formTemplate: data.formTemplate,
|
||||
fields: data.fields});
|
||||
}
|
||||
|
||||
static async getUnscheduledBidMeetings(company) {
|
||||
return await this.request(
|
||||
"custom_ui.api.db.bid_meetings.get_unscheduled_bid_meetings",
|
||||
|
|
@ -219,12 +200,6 @@ class Api {
|
|||
});
|
||||
}
|
||||
|
||||
static async getBidMeetingNote(name) {
|
||||
return await this.request("custom_ui.api.db.bid_meetings.get_bid_meeting_note", {
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ESTIMATE / QUOTATION METHODS
|
||||
// ============================================================================
|
||||
|
|
@ -269,7 +244,7 @@ class Api {
|
|||
|
||||
console.log("DEBUG: API - Sending estimate options to backend:", page, pageSize, filters, sorting);
|
||||
|
||||
const result = await this.request(FRAPPE_GET_ESTIMATES_TABLE_DATA_V2_METHOD, { page, pageSize, filters, sorting});
|
||||
const result = await this.request(FRAPPE_GET_ESTIMATES_METHOD, { page, pageSize, filters, sorting});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -450,25 +425,6 @@ class Api {
|
|||
return result;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVICE APPOINTMENT METHODS
|
||||
// ============================================================================
|
||||
|
||||
static async getServiceAppointments(companies = [], filters = {}) {
|
||||
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD, { companies, filters });
|
||||
}
|
||||
|
||||
static async updateServiceAppointmentScheduledDates(serviceAppointmentName, startDate, endDate, crewLeadName, startTime = null, endTime = null) {
|
||||
return await this.request(FRAPPE_UPDATE_SERVICE_APPOINTMENT_SCHEDULED_DATES_METHOD, {
|
||||
serviceAppointmentName,
|
||||
startDate,
|
||||
endDate,
|
||||
crewLeadName,
|
||||
startTime,
|
||||
endTime
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TASK METHODS
|
||||
// ============================================================================
|
||||
|
|
@ -733,21 +689,20 @@ class Api {
|
|||
*/
|
||||
static async getDocsList(
|
||||
doctype,
|
||||
fields = ["*"],
|
||||
fields = [],
|
||||
filters = {},
|
||||
pluck = null,
|
||||
page = 0,
|
||||
start = 0,
|
||||
pageLength = 0,
|
||||
) {
|
||||
const docs = await this.request(
|
||||
FRAPPE_GET_DOC_LIST_METHOD,
|
||||
{
|
||||
doctype,
|
||||
fields,
|
||||
filters,
|
||||
pluck,
|
||||
}
|
||||
);
|
||||
const docs = await frappe.db.get_list(doctype, {
|
||||
fields,
|
||||
filters,
|
||||
start: start,
|
||||
limit: pageLength,
|
||||
});
|
||||
console.log(
|
||||
`DEBUG: API - Fetched ${doctype} list: `,
|
||||
`DEBUG: API - Fetched ${doctype} list (page ${page + 1}, start ${start}): `,
|
||||
docs,
|
||||
);
|
||||
return docs;
|
||||
|
|
|
|||
|
|
@ -26,37 +26,9 @@
|
|||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<Tabs v-else value="0">
|
||||
<TabList>
|
||||
<Tab value="0">Bids</Tab>
|
||||
<Tab value="1">Projects</Tab>
|
||||
<Tab value="2">Service</Tab>
|
||||
<Tab value="3">Warranties</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel header="Bids" value="0">
|
||||
<ScheduleBid />
|
||||
</TabPanel>
|
||||
<TabPanel header="Projects" value="1">
|
||||
<div class="coming-soon">
|
||||
<p>Calendar Coming Soon!</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="Service" value="2">
|
||||
<div class="coming-soon">
|
||||
<p>Calendar Coming Soon!</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="Service" value="2">
|
||||
<div class="coming-soon">
|
||||
<p>Calendar Coming Soon!</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="Warranties" value="3">
|
||||
<p>Calendar Coming Soon!</p>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<div v-else class="coming-soon">
|
||||
<p>Calendar feature coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -206,7 +206,6 @@
|
|||
:meeting="selectedMeeting"
|
||||
@close="closeMeetingModal"
|
||||
@meeting-updated="handleMeetingUpdated"
|
||||
@complete-meeting="openNoteForm"
|
||||
/>
|
||||
|
||||
<!-- New Meeting Modal -->
|
||||
|
|
@ -217,17 +216,6 @@
|
|||
@confirm="handleNewMeetingConfirm"
|
||||
@cancel="handleNewMeetingCancel"
|
||||
/>
|
||||
|
||||
<!-- Bid Meeting Note Form Modal -->
|
||||
<BidMeetingNoteForm
|
||||
v-if="selectedMeetingForNotes"
|
||||
:visible="showNoteFormModal"
|
||||
@update:visible="showNoteFormModal = $event"
|
||||
:bid-meeting-name="selectedMeetingForNotes.name"
|
||||
:project-template="selectedMeetingForNotes.projectTemplate"
|
||||
@submit="handleNoteFormSubmit"
|
||||
@cancel="handleNoteFormCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -236,7 +224,6 @@ import { ref, computed, onMounted, watch } from "vue";
|
|||
import { useRoute, useRouter } from "vue-router";
|
||||
import BidMeetingModal from "../../modals/BidMeetingModal.vue";
|
||||
import MeetingDetailsModal from "../../modals/MeetingDetailsModal.vue";
|
||||
import BidMeetingNoteForm from "../../modals/BidMeetingNoteForm.vue";
|
||||
import { useLoadingStore } from "../../../stores/loading";
|
||||
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
||||
import { useCompanyStore } from "../../../stores/company";
|
||||
|
|
@ -264,8 +251,6 @@ const unscheduledMeetings = ref([]);
|
|||
const selectedMeeting = ref(null);
|
||||
const showMeetingModal = ref(false);
|
||||
const showNewMeetingModal = ref(false);
|
||||
const showNoteFormModal = ref(false);
|
||||
const selectedMeetingForNotes = ref(null);
|
||||
|
||||
// Drag and drop state
|
||||
const isDragOver = ref(false);
|
||||
|
|
@ -491,63 +476,6 @@ const handleMeetingUpdated = async () => {
|
|||
await loadUnscheduledMeetings();
|
||||
};
|
||||
|
||||
const openNoteForm = (meeting) => {
|
||||
// Verify meeting has required data
|
||||
if (!meeting || !meeting.name) {
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: "Meeting information is incomplete",
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!meeting.projectTemplate) {
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Missing Project Template",
|
||||
message: "This meeting does not have a project template assigned. Cannot open note form.",
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
selectedMeetingForNotes.value = meeting;
|
||||
showNoteFormModal.value = true;
|
||||
};
|
||||
|
||||
const handleNoteFormSubmit = async () => {
|
||||
// After successful submission, mark the meeting as completed
|
||||
try {
|
||||
loadingStore.setLoading(true);
|
||||
await Api.updateBidMeeting(selectedMeetingForNotes.value.name, {
|
||||
status: "Completed",
|
||||
});
|
||||
|
||||
notificationStore.addNotification({
|
||||
type: "success",
|
||||
title: "Success",
|
||||
message: "Meeting marked as completed",
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
// Reload meetings
|
||||
await handleMeetingUpdated();
|
||||
} catch (error) {
|
||||
console.error("Error updating meeting status:", error);
|
||||
} finally {
|
||||
loadingStore.setLoading(false);
|
||||
showNoteFormModal.value = false;
|
||||
selectedMeetingForNotes.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleNoteFormCancel = () => {
|
||||
showNoteFormModal.value = false;
|
||||
selectedMeetingForNotes.value = null;
|
||||
};
|
||||
|
||||
const openNewMeetingModal = () => {
|
||||
showNewMeetingModal.value = true;
|
||||
};
|
||||
|
|
@ -563,16 +491,7 @@ const handleNewMeetingConfirm = async (meetingData) => {
|
|||
|
||||
showNewMeetingModal.value = false;
|
||||
|
||||
// Optimistically add the new meeting to the unscheduled list
|
||||
unscheduledMeetings.value.unshift({
|
||||
name: result.name,
|
||||
address: meetingData.address,
|
||||
projectTemplate: meetingData.projectTemplate,
|
||||
contact: meetingData.contact,
|
||||
status: "Unscheduled",
|
||||
});
|
||||
|
||||
// Reload unscheduled meetings to ensure consistency
|
||||
// Reload unscheduled meetings to show the new one
|
||||
await loadUnscheduledMeetings();
|
||||
|
||||
notificationStore.addNotification({
|
||||
|
|
@ -617,7 +536,6 @@ const handleDragStart = (event, meeting = null) => {
|
|||
notes: meeting.notes || "",
|
||||
assigned_employee: meeting.assigned_employee || "",
|
||||
status: meeting.status,
|
||||
projectTemplate: meeting.projectTemplate,
|
||||
};
|
||||
} else if (!draggedMeeting.value) {
|
||||
// If no meeting data is set, use query address
|
||||
|
|
@ -641,7 +559,6 @@ const handleMeetingDragStart = (event, meeting) => {
|
|||
assigned_employee: meeting.assigned_employee || "",
|
||||
status: meeting.status,
|
||||
isRescheduling: true, // Flag to indicate this is a reschedule
|
||||
projectTemplate: meeting.projectTemplate,
|
||||
};
|
||||
|
||||
// Store the original meeting data in case drag is cancelled
|
||||
|
|
@ -752,7 +669,6 @@ const handleDrop = async (event, date, time) => {
|
|||
notes: droppedMeeting.notes || "",
|
||||
assigned_employee: droppedMeeting.assigned_employee || "",
|
||||
status: "Scheduled",
|
||||
projectTemplate: droppedMeeting.projectTemplate,
|
||||
};
|
||||
|
||||
// If this is an existing meeting, update it in the backend
|
||||
|
|
@ -876,7 +792,6 @@ const handleDropToUnscheduled = async (event) => {
|
|||
notes: droppedMeeting.notes || "",
|
||||
status: "Unscheduled",
|
||||
assigned_employee: droppedMeeting.assigned_employee || "",
|
||||
projectTemplate: droppedMeeting.projectTemplate,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@
|
|||
|
||||
<!-- Event Details Modal -->
|
||||
<JobDetailsModal
|
||||
v-model="eventDialog"
|
||||
v-model:visible="eventDialog"
|
||||
:job="selectedEvent"
|
||||
:foremen="foremen"
|
||||
@close="eventDialog = false"
|
||||
|
|
@ -807,8 +807,7 @@ const toggleTemplate = (templateName) => {
|
|||
|
||||
const applyTemplateFilter = async () => {
|
||||
showTemplateMenu.value = false;
|
||||
// await fetchProjects(currentDate.value);
|
||||
await fetchServiceAppointments();
|
||||
await fetchProjects(currentDate.value);
|
||||
};
|
||||
|
||||
// Date picker methods
|
||||
|
|
@ -1239,20 +1238,14 @@ const stopResize = async () => {
|
|||
originalEndDate.value = null;
|
||||
};
|
||||
|
||||
const fetchServiceAppointments = async (currentDate) => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
// Calculate date range for the week
|
||||
const startDate = weekStartDate.value;
|
||||
const endDate = addDays(startDate, 6);
|
||||
|
||||
// const data = await Api.getJobsForCalendar(startDate, endDate, companyStore.currentCompany, selectedProjectTemplates.value);
|
||||
const data = await Api.getServiceAppointments(
|
||||
[companyStore.currentCompany],
|
||||
{
|
||||
"expectedStartDate": ["<=", endDate],
|
||||
"expectedEndDate": [">=", startDate]
|
||||
}
|
||||
);
|
||||
const data = await Api.getJobsForCalendar(startDate, endDate, companyStore.currentCompany, selectedProjectTemplates.value);
|
||||
|
||||
// Transform the API response into the format the component expects
|
||||
const transformedServices = [];
|
||||
|
||||
|
|
@ -1357,24 +1350,21 @@ const fetchHolidays = async () => {
|
|||
}
|
||||
|
||||
watch(weekStartDate, async () => {
|
||||
// await fetchProjects();
|
||||
await fetchServiceAppointments();
|
||||
await fetchProjects();
|
||||
await fetchHolidays();
|
||||
});
|
||||
|
||||
watch(companyStore, async () => {
|
||||
await fetchForemen();
|
||||
await fetchProjectTemplates();
|
||||
// await fetchProjects();
|
||||
await fetchServiceAppointments();
|
||||
await fetchProjects();
|
||||
}, { deep: true });
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await fetchForemen();
|
||||
await fetchProjectTemplates();
|
||||
// await fetchProjects();
|
||||
await fetchServiceAppointments();
|
||||
await fetchProjects();
|
||||
await fetchHolidays();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,978 +0,0 @@
|
|||
<template>
|
||||
<Modal
|
||||
:visible="showModal"
|
||||
@update:visible="showModal = $event"
|
||||
:options="modalOptions"
|
||||
@confirm="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<template #title>
|
||||
<div class="modal-header">
|
||||
<i class="pi pi-file-edit" style="color: var(--primary-color); font-size: 1.2rem; margin-right: 0.5rem;"></i>
|
||||
{{ formTitle }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<ProgressSpinner style="width: 50px; height: 50px" strokeWidth="4" />
|
||||
<p>Loading form...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="formConfig" class="form-container">
|
||||
<Button @click="debugLog" label="Debug" severity="secondary" size="small" class="debug-button" />
|
||||
<template v-for="row in groupedFields" :key="`row-${row.rowIndex}`">
|
||||
<div class="form-row">
|
||||
<div
|
||||
v-for="field in row.fields"
|
||||
:key="field.name"
|
||||
:class="`form-column-${Math.min(Math.max(field.columns || 12, 1), 12)}`"
|
||||
>
|
||||
<div class="form-field">
|
||||
<!-- Field Label -->
|
||||
<label :for="field.name" class="field-label">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="required-indicator">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Help Text -->
|
||||
<small v-if="field.helpText" class="field-help-text">
|
||||
{{ field.helpText }}
|
||||
</small>
|
||||
|
||||
<!-- Data/Text Field -->
|
||||
<template v-if="field.type === 'Data' || field.type === 'Text'">
|
||||
<InputText
|
||||
v-if="field.type === 'Data'"
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="field.label"
|
||||
class="w-full"
|
||||
/>
|
||||
<Textarea
|
||||
v-else
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="field.label"
|
||||
rows="3"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Check Field -->
|
||||
<template v-else-if="field.type === 'Check'">
|
||||
<div class="checkbox-container">
|
||||
<Checkbox
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:binary="true"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
/>
|
||||
<label :for="field.name" class="checkbox-label">
|
||||
{{ formData[field.name].value ? 'Yes' : 'No' }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Date Field -->
|
||||
<template v-else-if="field.type === 'Date'">
|
||||
<DatePicker
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
dateFormat="yy-mm-dd"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Datetime Field -->
|
||||
<template v-else-if="field.type === 'Datetime'">
|
||||
<DatePicker
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
showTime
|
||||
hourFormat="12"
|
||||
dateFormat="yy-mm-dd"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Time Field -->
|
||||
<template v-else-if="field.type === 'Time'">
|
||||
<DatePicker
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
timeOnly
|
||||
hourFormat="12"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Number Field -->
|
||||
<template v-else-if="field.type === 'Number'">
|
||||
<InputNumber
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Select Field -->
|
||||
<template v-else-if="field.type === 'Select'">
|
||||
<div @click="console.log('Select wrapper clicked:', field.name, 'disabled:', field.readOnly || !isFieldVisible(field), 'options:', optionsForFields[field.name])">
|
||||
<Select
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:options="optionsForFields[field.name]"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="`Select ${field.label}`"
|
||||
:optionLabel="'label'"
|
||||
:optionValue="'value'"
|
||||
:editable="false"
|
||||
:showClear="true"
|
||||
:baseZIndex="10000"
|
||||
@click.native="console.log('Select native click:', field.name)"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Multi-Select Field -->
|
||||
<template v-else-if="field.type === 'Multi-Select'">
|
||||
<MultiSelect
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:options="optionsForFields[field.name]"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="`Select ${field.label}`"
|
||||
:key="`${field.name}-${field.readOnly || !isFieldVisible(field)}`"
|
||||
:optionLabel="'label'"
|
||||
:optionValue="'value'"
|
||||
:showClear="true"
|
||||
:baseZIndex="9999"
|
||||
display="chip"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Multi-Select w/ Quantity Field -->
|
||||
<template v-else-if="field.type === 'Multi-Select w/ Quantity'">
|
||||
<div class="multi-select-quantity-container">
|
||||
<!-- Item Selector -->
|
||||
<div class="item-selector">
|
||||
<Select
|
||||
v-model="currentItemSelection[field.name]"
|
||||
:options="optionsForFields[field.name]"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="`Add ${field.label}`"
|
||||
:key="`${field.name}-${field.readOnly || !isFieldVisible(field)}`"
|
||||
:optionLabel="'label'"
|
||||
:showClear="true"
|
||||
:baseZIndex="9999"
|
||||
class="w-full"
|
||||
@change="addItemToQuantityList(field)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Selected Items with Quantities -->
|
||||
<div v-if="formData[field.name].value && formData[field.name].value.length > 0" class="selected-items-list">
|
||||
<div
|
||||
v-for="(item, index) in formData[field.name].value"
|
||||
:key="index"
|
||||
class="quantity-item"
|
||||
>
|
||||
<div class="item-name">{{ getOptionLabel(field, item) }}</div>
|
||||
<div class="quantity-controls">
|
||||
<InputNumber
|
||||
v-model="item.quantity"
|
||||
:min="1"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
showButtons
|
||||
buttonLayout="horizontal"
|
||||
:step="1"
|
||||
decrementButtonClass="p-button-secondary"
|
||||
incrementButtonClass="p-button-secondary"
|
||||
incrementButtonIcon="pi pi-plus"
|
||||
decrementButtonIcon="pi pi-minus"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
@click="removeItemFromQuantityList(field, index)"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else class="error-container">
|
||||
<i class="pi pi-exclamation-triangle" style="font-size: 2rem; color: var(--red-500);"></i>
|
||||
<p>Failed to load form configuration</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, reactive } from "vue";
|
||||
import Modal from "../common/Modal.vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Textarea from "primevue/textarea";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
import DatePicker from "primevue/datepicker";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import Select from "primevue/select";
|
||||
import MultiSelect from "primevue/multiselect";
|
||||
import Button from "primevue/button";
|
||||
import ProgressSpinner from "primevue/progressspinner";
|
||||
import Api from "../../api";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
|
||||
const docsForSelectFields = ref({});
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
bidMeetingName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectTemplate: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "submit", "cancel"]);
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const showModal = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit("update:visible", value),
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
const formConfig = ref(null);
|
||||
const formData = ref({}); // Will store fieldName: {fieldConfig, value}
|
||||
const currentItemSelection = ref({}); // For tracking current selection in Multi-Select w/ Quantity
|
||||
const doctypeOptions = ref({}); // Cache for doctype options
|
||||
|
||||
const formTitle = computed(() => {
|
||||
return formConfig.value?.title || "Bid Meeting Notes";
|
||||
});
|
||||
|
||||
// Include all fields from config plus a general notes field
|
||||
const allFields = computed(() => {
|
||||
if (!formConfig.value) return [];
|
||||
|
||||
const fields = [...(formConfig.value.fields || [])];
|
||||
|
||||
// Always add a general notes field at the end
|
||||
const generalNotesField = {
|
||||
name: 'general_notes',
|
||||
label: 'General Notes',
|
||||
type: 'Text',
|
||||
required: 0,
|
||||
readOnly: 0,
|
||||
helpText: 'Any additional notes or observations from the meeting',
|
||||
row: Math.max(...fields.map(f => f.row || 1), 0) + 1,
|
||||
columns: 12,
|
||||
};
|
||||
|
||||
fields.push(generalNotesField);
|
||||
return fields;
|
||||
});
|
||||
|
||||
// Group fields by row for grid layout
|
||||
const groupedFields = computed(() => {
|
||||
const groups = {};
|
||||
|
||||
allFields.value.forEach(field => {
|
||||
const rowNum = field.row || 1;
|
||||
if (!groups[rowNum]) {
|
||||
groups[rowNum] = { rowIndex: rowNum, fields: [] };
|
||||
}
|
||||
groups[rowNum].fields.push(field);
|
||||
});
|
||||
|
||||
// Sort fields by column and set columns span
|
||||
Object.values(groups).forEach(group => {
|
||||
group.fields.sort((a, b) => (a.column || 0) - (b.column || 0));
|
||||
const numFields = group.fields.length;
|
||||
const span = Math.floor(12 / numFields);
|
||||
group.fields.forEach(field => {
|
||||
field.columns = span;
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(groups).sort((a, b) => a.rowIndex - b.rowIndex);
|
||||
});
|
||||
|
||||
// Update field value in reactive form data
|
||||
const updateFieldValue = (fieldName, value) => {
|
||||
if (formData.value[fieldName]) {
|
||||
formData.value[fieldName].value = value;
|
||||
}
|
||||
};
|
||||
|
||||
// Get CSS class for field column span
|
||||
const getFieldColumnClass = (field) => {
|
||||
const columns = field.columns || 12; // Default to full width if not specified
|
||||
return `form-column-${Math.min(Math.max(columns, 1), 12)}`; // Ensure between 1-12
|
||||
};
|
||||
|
||||
const modalOptions = computed(() => ({
|
||||
maxWidth: "800px",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Submit",
|
||||
cancelButtonText: "Cancel",
|
||||
confirmButtonColor: "primary",
|
||||
zIndex: 1000, // Lower than select baseZIndex
|
||||
}));
|
||||
|
||||
// Helper to find field name by label
|
||||
const findFieldNameByLabel = (label) => {
|
||||
if (!formConfig.value || !formConfig.value.fields) return null;
|
||||
const field = formConfig.value.fields.find(f => f.label === label);
|
||||
return field ? field.name : null;
|
||||
};
|
||||
|
||||
const fetchDocsForSelectField = async (doctype, fieldName) => {
|
||||
const docs = await Api.getDocsList(doctype);
|
||||
docsForSelectFields[fieldName] = docs;
|
||||
}
|
||||
|
||||
// Check if a field should be visible based on conditional logic
|
||||
const isFieldVisible = (field) => {
|
||||
if (!field.conditionalOnField) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find the actual field name from the label (conditionalOnField contains the label)
|
||||
const dependentFieldName = findFieldNameByLabel(field.conditionalOnField);
|
||||
if (!dependentFieldName) {
|
||||
console.warn(`Could not find field with label: ${field.conditionalOnField}`);
|
||||
return true; // Show field if we can't find the dependency
|
||||
}
|
||||
|
||||
const dependentFieldValue = formData.value[dependentFieldName]?.value;
|
||||
|
||||
console.log(`Checking visibility for ${field.label}:`, {
|
||||
conditionalOnField: field.conditionalOnField,
|
||||
dependentFieldName,
|
||||
dependentFieldValue,
|
||||
conditionalOnValue: field.conditionalOnValue,
|
||||
});
|
||||
|
||||
// If the dependent field is a checkbox, it should be true
|
||||
if (typeof dependentFieldValue === "boolean") {
|
||||
return dependentFieldValue === true;
|
||||
}
|
||||
|
||||
// If conditional_on_value is specified, check for exact match
|
||||
if (field.conditionalOnValue !== null && field.conditionalOnValue !== undefined) {
|
||||
return dependentFieldValue === field.conditionalOnValue;
|
||||
}
|
||||
|
||||
// Otherwise, just check if the dependent field has any truthy value
|
||||
return !!dependentFieldValue;
|
||||
};
|
||||
|
||||
// Get options for select/multi-select fields
|
||||
const getFieldOptions = (field) => {
|
||||
// Access reactive data to ensure reactivity
|
||||
const optionsData = docsForSelectFields.value[field.name];
|
||||
|
||||
console.log(`getFieldOptions called for ${field.label}:`, {
|
||||
type: field.type,
|
||||
options: field.options,
|
||||
optionsType: typeof field.options,
|
||||
doctypeForSelect: field.doctypeForSelect,
|
||||
doctypeLabelField: field.doctypeLabelField,
|
||||
hasDoctypeOptions: !!optionsData,
|
||||
});
|
||||
|
||||
// If options should be fetched from a doctype
|
||||
if (field.doctypeForSelect && optionsData) {
|
||||
console.log(`Using doctype options for ${field.label}:`, optionsData);
|
||||
return [...optionsData]; // Return a copy to ensure reactivity
|
||||
}
|
||||
|
||||
// If options are provided as a string (comma-separated), parse them
|
||||
if (field.options && typeof field.options === "string" && field.options.trim() !== "") {
|
||||
const optionStrings = field.options.split(",").map((opt) => opt.trim()).filter(opt => opt !== "");
|
||||
// Convert to objects for consistency with PrimeVue MultiSelect
|
||||
const options = optionStrings.map((opt) => ({
|
||||
label: opt,
|
||||
value: opt,
|
||||
}));
|
||||
console.log(`Parsed options for ${field.label}:`, options);
|
||||
return options;
|
||||
}
|
||||
|
||||
console.warn(`No options found for ${field.label}`);
|
||||
return [];
|
||||
};
|
||||
|
||||
// Get options for select/multi-select fields
|
||||
const optionsForFields = computed(() => {
|
||||
// Ensure reactivity by accessing docsForSelectFields
|
||||
const docsData = docsForSelectFields.value;
|
||||
const opts = {};
|
||||
allFields.value.forEach(field => {
|
||||
const options = getFieldOptions(field);
|
||||
opts[field.name] = options;
|
||||
console.log(`Computed options for ${field.name}:`, options);
|
||||
});
|
||||
console.log('optionsForFields computed:', opts);
|
||||
return opts;
|
||||
});
|
||||
|
||||
// Add item to quantity list for Multi-Select w/ Quantity fields
|
||||
const addItemToQuantityList = (field) => {
|
||||
const selectedItem = currentItemSelection.value[field.name];
|
||||
if (!selectedItem) return;
|
||||
|
||||
// selectedItem is now an object with { label, value }
|
||||
const itemValue = selectedItem.value || selectedItem;
|
||||
const itemLabel = selectedItem.label || selectedItem;
|
||||
|
||||
// Initialize array if it doesn't exist
|
||||
const fieldData = formData.value[field.name];
|
||||
if (!fieldData.value) {
|
||||
fieldData.value = [];
|
||||
}
|
||||
|
||||
// Check if item already exists (compare by value)
|
||||
const existingItem = fieldData.value.find((item) => item.item === itemValue);
|
||||
if (existingItem) {
|
||||
// Increment quantity if item already exists
|
||||
existingItem.quantity += 1;
|
||||
} else {
|
||||
// Add new item with quantity 1
|
||||
fieldData.value.push({
|
||||
item: itemValue,
|
||||
label: itemLabel,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
currentItemSelection.value[field.name] = null;
|
||||
};
|
||||
|
||||
// Remove item from quantity list
|
||||
const removeItemFromQuantityList = (field, index) => {
|
||||
const fieldData = formData.value[field.name];
|
||||
if (fieldData && fieldData.value) {
|
||||
fieldData.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Get option label for display
|
||||
const getOptionLabel = (field, item) => {
|
||||
const options = optionsForFields.value[field.name];
|
||||
const option = options.find(o => o.value === item.item);
|
||||
return option ? option.label : item.label || item.item;
|
||||
};
|
||||
|
||||
// Initialize form data with default values
|
||||
const initializeFormData = () => {
|
||||
if (!formConfig.value) return;
|
||||
|
||||
const data = {};
|
||||
allFields.value.forEach((field) => {
|
||||
// Create reactive object with field config and value
|
||||
data[field.name] = {
|
||||
...field, // Include all field configuration
|
||||
value: null, // Initialize value
|
||||
};
|
||||
|
||||
// Set default value if provided
|
||||
if (field.defaultValue !== null && field.defaultValue !== undefined) {
|
||||
data[field.name].value = field.defaultValue;
|
||||
} else {
|
||||
// Initialize based on field type
|
||||
switch (field.type) {
|
||||
case "Check":
|
||||
data[field.name].value = false;
|
||||
break;
|
||||
case "Multi-Select":
|
||||
data[field.name].value = [];
|
||||
break;
|
||||
case "Multi-Select w/ Quantity":
|
||||
data[field.name].value = [];
|
||||
break;
|
||||
case "Number":
|
||||
data[field.name].value = null;
|
||||
break;
|
||||
case "Select":
|
||||
data[field.name].value = null;
|
||||
break;
|
||||
default:
|
||||
data[field.name].value = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
formData.value = data;
|
||||
};
|
||||
|
||||
// Load form configuration
|
||||
const loadFormConfig = async () => {
|
||||
if (!props.projectTemplate) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const config = await Api.getBidMeetingNoteForm(props.projectTemplate);
|
||||
formConfig.value = config;
|
||||
console.log("Loaded form config:", config);
|
||||
|
||||
// Load doctype options for fields that need them
|
||||
await loadDoctypeOptions();
|
||||
|
||||
// Initialize form data
|
||||
initializeFormData();
|
||||
} catch (error) {
|
||||
console.error("Error loading form config:", error);
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: "Failed to load form configuration",
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load options for fields that reference doctypes
|
||||
const loadDoctypeOptions = async () => {
|
||||
if (!formConfig.value || !formConfig.value.fields) return;
|
||||
|
||||
const fieldsWithDoctype = formConfig.value.fields.filter(
|
||||
(field) => field.doctypeForSelect && field.doctypeForSelect !== ""
|
||||
);
|
||||
|
||||
for (const field of fieldsWithDoctype) {
|
||||
try {
|
||||
// Use the new API method for fetching docs
|
||||
let docs = await Api.getEstimateItems();
|
||||
|
||||
// Deduplicate by value field
|
||||
const valueField = field.doctypeValueField || 'name';
|
||||
const seen = new Set();
|
||||
docs = docs.filter(doc => {
|
||||
const val = doc[valueField] || doc.name || doc;
|
||||
if (seen.has(val)) return false;
|
||||
seen.add(val);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Transform docs into options format
|
||||
// Use doctypeLabelField if specified, otherwise default to 'name'
|
||||
const labelField = field.doctypeLabelField || 'name';
|
||||
|
||||
const options = docs.map((doc) => ({
|
||||
label: doc[labelField] || doc.name || doc,
|
||||
value: doc[valueField] || doc.name || doc,
|
||||
}));
|
||||
|
||||
docsForSelectFields.value[field.name] = options;
|
||||
console.log(`Loaded ${options.length} options for ${field.label} from ${field.doctypeForSelect}`);
|
||||
} catch (error) {
|
||||
console.error(`Error loading options for ${field.doctypeForSelect}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Validate form
|
||||
const validateForm = () => {
|
||||
const errors = [];
|
||||
|
||||
if (!formConfig.value) return errors;
|
||||
|
||||
allFields.value.forEach((field) => {
|
||||
// Only validate if field is visible
|
||||
if (!isFieldVisible(field)) return;
|
||||
|
||||
// Skip required validation for checkboxes (they always have a value: true or false)
|
||||
if (field.type === 'Check') return;
|
||||
|
||||
if (field.required) {
|
||||
const value = formData.value[field.name]?.value;
|
||||
|
||||
if (value === null || value === undefined || value === "") {
|
||||
errors.push(`${field.label} is required`);
|
||||
} else if (Array.isArray(value) && value.length === 0) {
|
||||
errors.push(`${field.label} is required`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
// Format field data for submission
|
||||
const formatFieldData = (field) => {
|
||||
const value = formData.value[field.name]?.value;
|
||||
|
||||
// Include the entire field configuration
|
||||
const fieldData = {
|
||||
...field, // Include all field properties
|
||||
value: value, // Override with current value
|
||||
};
|
||||
|
||||
// Handle options: include unless fetched from doctype
|
||||
if (field.doctypeForSelect) {
|
||||
// Remove options if they were fetched from doctype
|
||||
delete fieldData.options;
|
||||
}
|
||||
|
||||
// For fields with include_options flag, include the selected options
|
||||
if (field.includeOptions) {
|
||||
if (field.type === "Multi-Select") {
|
||||
fieldData.selectedOptions = value || [];
|
||||
} else if (field.type === "Multi-Select w/ Quantity") {
|
||||
fieldData.items = value || [];
|
||||
} else if (field.type === "Select") {
|
||||
fieldData.selectedOption = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Format dates as strings
|
||||
if (field.type === "Date" || field.type === "Datetime" || field.type === "Time") {
|
||||
if (value instanceof Date) {
|
||||
if (field.type === "Date") {
|
||||
fieldData.value = value.toISOString().split("T")[0];
|
||||
} else if (field.type === "Datetime") {
|
||||
fieldData.value = value.toISOString();
|
||||
} else if (field.type === "Time") {
|
||||
fieldData.value = value.toTimeString().split(" ")[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldData;
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async () => {
|
||||
// Validate form
|
||||
const errors = validateForm();
|
||||
if (errors.length > 0) {
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Validation Error",
|
||||
message: errors.join(", "),
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loadingStore.setLoading(true);
|
||||
|
||||
// Format data for submission
|
||||
const submissionData = {
|
||||
bidMeeting: props.bidMeetingName,
|
||||
projectTemplate: props.projectTemplate,
|
||||
formName: formConfig.value?.name || formConfig.value?.title,
|
||||
formTemplate: formConfig.value?.name || formConfig.value?.title,
|
||||
fields: allFields.value
|
||||
.filter((field) => isFieldVisible(field))
|
||||
.map((field) => formatFieldData(field)),
|
||||
};
|
||||
|
||||
console.log("Submitting form data:", submissionData);
|
||||
|
||||
// Submit to API
|
||||
await Api.submitBidMeetingNoteForm(submissionData);
|
||||
|
||||
notificationStore.addNotification({
|
||||
type: "success",
|
||||
title: "Success",
|
||||
message: "Bid meeting notes submitted successfully",
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
emit("submit", submissionData);
|
||||
showModal.value = false;
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: "Failed to submit form. Please try again.",
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
loadingStore.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle cancel
|
||||
const handleCancel = () => {
|
||||
emit("cancel");
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
// Debug function to log current form data and select options
|
||||
const debugLog = () => {
|
||||
console.log("=== FORM DEBUG ===");
|
||||
|
||||
const debugData = {};
|
||||
allFields.value.forEach(field => {
|
||||
const fieldValue = formData.value[field.name]?.value;
|
||||
const fieldOptions = optionsForFields.value[field.name];
|
||||
const isVisible = isFieldVisible(field);
|
||||
const isDisabled = field.readOnly || !isVisible;
|
||||
|
||||
debugData[field.name] = {
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
value: fieldValue,
|
||||
isVisible,
|
||||
isDisabled,
|
||||
...(field.type.includes('Select') ? {
|
||||
options: fieldOptions,
|
||||
optionsCount: fieldOptions?.length || 0
|
||||
} : {}),
|
||||
};
|
||||
});
|
||||
|
||||
console.log("Current Form Data:", debugData);
|
||||
console.log("==================");
|
||||
};
|
||||
|
||||
// Watch for modal visibility changes
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
loadFormConfig();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Load form config on mount if modal is visible
|
||||
onMounted(() => {
|
||||
if (props.visible) {
|
||||
loadFormConfig();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
gap: 1rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.debug-button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Grid Layout Styles */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-column-1 { grid-column: span 1; }
|
||||
.form-column-2 { grid-column: span 2; }
|
||||
.form-column-3 { grid-column: span 3; }
|
||||
.form-column-4 { grid-column: span 4; }
|
||||
.form-column-5 { grid-column: span 5; }
|
||||
.form-column-6 { grid-column: span 6; }
|
||||
.form-column-7 { grid-column: span 7; }
|
||||
.form-column-8 { grid-column: span 8; }
|
||||
.form-column-9 { grid-column: span 9; }
|
||||
.form-column-10 { grid-column: span 10; }
|
||||
.form-column-11 { grid-column: span 11; }
|
||||
.form-column-12 { grid-column: span 12; }
|
||||
|
||||
.form-field-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.form-field :deep(.p-select) {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.form-field :deep(.p-multiselect) {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.required-indicator {
|
||||
color: var(--red-500);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.field-help-text {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.85rem;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.checkbox-container :deep(.p-checkbox) {
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.checkbox-container :deep(.p-checkbox .p-checkbox-box) {
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
border: 1px solid var(--surface-border) !important;
|
||||
border-radius: 3px !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.checkbox-container :deep(.p-checkbox .p-checkbox-input) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
opacity: 0 !important;
|
||||
cursor: pointer !important;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
.checkbox-container :deep(.p-checkbox .p-checkbox-box .p-checkbox-icon) {
|
||||
font-size: 0.875rem !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color-secondary);
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.multi-select-quantity-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--surface-50);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.quantity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--surface-0);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.item-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.form-container {
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.quantity-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,532 +0,0 @@
|
|||
<template>
|
||||
<div class="bid-meeting-notes">
|
||||
<div v-if="loading" class="loading-state">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<p>Loading bid notes...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="error-state">
|
||||
<i class="pi pi-exclamation-triangle"></i>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="bidNote" class="notes-content">
|
||||
<!-- Header Information -->
|
||||
<div class="notes-header">
|
||||
<div class="header-info">
|
||||
<div class="info-item">
|
||||
<span class="label">Meeting:</span>
|
||||
<span class="value">{{ bidNote.bidMeeting }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="bidNote.formTemplate">
|
||||
<span class="label">Template:</span>
|
||||
<span class="value">{{ bidNote.formTemplate }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Created By:</span>
|
||||
<span class="value">{{ bidNote.owner }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Last Modified:</span>
|
||||
<span class="value">{{ formatDate(bidNote.modified) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Notes (if exists) -->
|
||||
<div v-if="bidNote.notes" class="general-notes">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-file-edit"></i>
|
||||
<span>General Notes</span>
|
||||
</div>
|
||||
<div class="notes-text">{{ bidNote.notes }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Fields organized by rows -->
|
||||
<div v-if="fieldsByRow && Object.keys(fieldsByRow).length > 0" class="fields-section">
|
||||
<div v-for="(rowFields, rowIndex) in fieldsByRow" :key="rowIndex" class="field-row">
|
||||
<div v-for="field in rowFields" :key="field.name" class="field-item" :class="`field-type-${field.type.toLowerCase().replace(/\s+/g, '-')}`">
|
||||
<!-- Check if field should be displayed based on conditionals -->
|
||||
<template v-if="shouldShowField(field)">
|
||||
<!-- Check Type -->
|
||||
<div v-if="field.type === 'Check'" class="field-check">
|
||||
<i :class="field.value === '1' ? 'pi pi-check-square' : 'pi pi-square'" :style="{ color: field.value === '1' ? 'var(--primary-color)' : '#999' }"></i>
|
||||
<span class="field-label">{{ field.label }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Text Type -->
|
||||
<div v-else-if="field.type === 'Text'" class="field-text">
|
||||
<div class="field-label">{{ field.label }}</div>
|
||||
<div class="field-value">{{ field.value || 'N/A' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select Type -->
|
||||
<div v-else-if="field.type === 'Multi-Select'" class="field-multiselect">
|
||||
<div class="field-label">{{ field.label }}</div>
|
||||
<div class="field-value">
|
||||
<div v-if="getParsedMultiSelect(field.value).length > 0" class="selected-items">
|
||||
<span v-for="(item, idx) in getParsedMultiSelect(field.value)" :key="idx" class="selected-item">
|
||||
{{ item }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="no-selection">No items selected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Select w/ Quantity Type -->
|
||||
<div v-else-if="field.type === 'Multi-Select w/ Quantity'" class="field-multiselect-qty">
|
||||
<div class="field-label">{{ field.label }}</div>
|
||||
<div class="field-value">
|
||||
<div v-if="loading" class="loading-items">
|
||||
<v-progress-circular size="20" indeterminate></v-progress-circular>
|
||||
<span>Loading items...</span>
|
||||
</div>
|
||||
<div v-else-if="getParsedMultiSelectQty(field.value).length > 0" class="quantity-items">
|
||||
<div v-for="(item, idx) in getParsedMultiSelectQty(field.value)" :key="idx" class="quantity-item">
|
||||
<span class="item-label">{{ getItemLabel(field, item) }}</span>
|
||||
<span class="item-quantity">Qty: {{ item.quantity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-selection">No items selected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default/Unknown Type -->
|
||||
<div v-else class="field-unknown">
|
||||
<div class="field-label">{{ field.label }}</div>
|
||||
<div class="field-value">{{ field.value || 'N/A' }}</div>
|
||||
<div class="field-type-note">(Type: {{ field.type }})</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state">
|
||||
<i class="pi pi-inbox"></i>
|
||||
<p>No fields to display</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import Api from "../../api";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
bidNote: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Local state
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const doctypeCache = ref({});
|
||||
|
||||
// Organize fields by row
|
||||
const fieldsByRow = computed(() => {
|
||||
if (!props.bidNote?.fields) return {};
|
||||
|
||||
const rows = {};
|
||||
props.bidNote.fields.forEach(field => {
|
||||
const rowNum = field.row || 0;
|
||||
if (!rows[rowNum]) {
|
||||
rows[rowNum] = [];
|
||||
}
|
||||
rows[rowNum].push(field);
|
||||
});
|
||||
|
||||
// Sort fields within each row by column
|
||||
Object.keys(rows).forEach(rowNum => {
|
||||
rows[rowNum].sort((a, b) => (a.column || 0) - (b.column || 0));
|
||||
});
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const shouldShowField = (field) => {
|
||||
// If no conditional, always show
|
||||
if (!field.conditionalOnField) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find the field this one depends on
|
||||
const parentField = props.bidNote.fields.find(f => f.label === field.conditionalOnField);
|
||||
if (!parentField) {
|
||||
return true; // If parent not found, show anyway
|
||||
}
|
||||
|
||||
// For checkboxes, show if checked
|
||||
if (parentField.type === 'Check') {
|
||||
return parentField.value === '1';
|
||||
}
|
||||
|
||||
// If conditional value is specified, check against it
|
||||
if (field.conditionalOnValue) {
|
||||
return parentField.value === field.conditionalOnValue;
|
||||
}
|
||||
|
||||
// Otherwise, show if parent has any value
|
||||
return !!parentField.value;
|
||||
};
|
||||
|
||||
const getParsedMultiSelect = (value) => {
|
||||
if (!value) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (e) {
|
||||
console.error("Error parsing multi-select value:", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getParsedMultiSelectQty = (value) => {
|
||||
if (!value) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (e) {
|
||||
console.error("Error parsing multi-select with quantity value:", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getItemLabel = (field, item) => {
|
||||
// If we have a cached label from doctype lookup
|
||||
if (item.fetchedLabel) {
|
||||
return item.fetchedLabel;
|
||||
}
|
||||
|
||||
// If label is provided in the item itself
|
||||
if (item.label) {
|
||||
return item.label;
|
||||
}
|
||||
|
||||
// Otherwise use the item ID
|
||||
return item.item || 'Unknown Item';
|
||||
};
|
||||
|
||||
const fetchDoctypeData = async (field, itemId) => {
|
||||
if (!field.valueDoctype || !itemId) return null;
|
||||
|
||||
// Check cache first
|
||||
const cacheKey = `${field.valueDoctype}:${itemId}`;
|
||||
if (doctypeCache.value[cacheKey]) {
|
||||
return doctypeCache.value[cacheKey];
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the getDetailedDoc method from the API
|
||||
const data = await Api.getDetailedDoc(field.valueDoctype, itemId);
|
||||
|
||||
// Cache the result
|
||||
doctypeCache.value[cacheKey] = data;
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching doctype ${field.valueDoctype}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const loadDoctypeLabels = async () => {
|
||||
if (!props.bidNote?.fields) return;
|
||||
|
||||
// Find all Multi-Select w/ Quantity fields that have valueDoctype
|
||||
const quantityFields = props.bidNote.fields.filter(
|
||||
f => f.type === 'Multi-Select w/ Quantity' && f.valueDoctype
|
||||
);
|
||||
|
||||
for (const field of quantityFields) {
|
||||
const items = getParsedMultiSelectQty(field.value);
|
||||
for (const item of items) {
|
||||
if (item.item && !item.fetchedLabel) {
|
||||
const data = await fetchDoctypeData(field, item.item);
|
||||
if (data && field.doctypeLabelField) {
|
||||
// Add the fetched label to the item
|
||||
item.fetchedLabel = data[field.doctypeLabelField] || item.item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
return new Date(dateString).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await loadDoctypeLabels();
|
||||
} catch (err) {
|
||||
console.error("Error loading bid note details:", err);
|
||||
error.value = "Failed to load some field details";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bid-meeting-notes {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.error-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.notes-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.notes-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.9;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
font-weight: 600;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.general-notes {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #667eea;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.section-header i {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.notes-text {
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.fields-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.field-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.field-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-check i {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.field-check .field-label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.field-text,
|
||||
.field-multiselect,
|
||||
.field-multiselect-qty,
|
||||
.field-unknown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selected-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quantity-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quantity-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-quantity {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.no-selection {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.field-type-note {
|
||||
font-size: 0.75em;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.header-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -171,7 +171,7 @@ import { ref, computed } from "vue";
|
|||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
|
@ -186,15 +186,15 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["update:modelValue", "close"]);
|
||||
const emit = defineEmits(["update:visible", "close"]);
|
||||
|
||||
// Computed
|
||||
const showModal = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
return props.visible;
|
||||
},
|
||||
set(value) {
|
||||
emit("update:modelValue", value);
|
||||
emit("update:visible", value);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<Modal :visible="showModal" @update:visible="showModal = $event" :options="modalOptions" @confirm="handleClose">
|
||||
<template #title>
|
||||
<div class="modal-header">
|
||||
<i class="pi pi-calendar" style="color: var(--primary-color); font-size: 1.2rem; margin-right: 0.5rem;"></i>
|
||||
Meeting Details
|
||||
</div>
|
||||
</template>
|
||||
<Modal :visible="showModal" @update:visible="showModal = $event" :options="modalOptions" @confirm="handleClose">
|
||||
<template #title>
|
||||
<div class="modal-header">
|
||||
<i class="pi pi-calendar" style="color: var(--primary-color); font-size: 1.2rem; margin-right: 0.5rem;"></i>
|
||||
Meeting Details
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="meeting" class="meeting-details">
|
||||
<!-- Status Badge -->
|
||||
<div class="status-section">
|
||||
|
|
@ -136,19 +135,8 @@
|
|||
variant="elevated"
|
||||
:loading="isUpdating"
|
||||
>
|
||||
<v-icon left>mdi-file-edit</v-icon>
|
||||
Create Notes and Complete
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="meeting.status === 'Completed' && meeting.bidNotes"
|
||||
@click="handleViewBidNotes"
|
||||
color="info"
|
||||
variant="elevated"
|
||||
:loading="loadingBidNotes"
|
||||
>
|
||||
<v-icon left>mdi-note-text</v-icon>
|
||||
View Bid Notes
|
||||
<v-icon left>mdi-check</v-icon>
|
||||
Mark as Completed
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
|
|
@ -163,38 +151,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Bid Notes Modal -->
|
||||
<Modal
|
||||
:visible="showBidNotesModal"
|
||||
@update:visible="showBidNotesModal = $event"
|
||||
:options="bidNotesModalOptions"
|
||||
@confirm="handleCloseBidNotes"
|
||||
>
|
||||
<template #title>
|
||||
<div class="modal-header">
|
||||
<i class="pi pi-file-edit" style="color: var(--primary-color); font-size: 1.2rem; margin-right: 0.5rem;"></i>
|
||||
Bid Meeting Notes
|
||||
</div>
|
||||
</template>
|
||||
<BidMeetingNotes v-if="bidNoteData" :bid-note="bidNoteData" />
|
||||
<div v-else-if="bidNotesError" class="error-message">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
<span>{{ bidNotesError }}</span>
|
||||
</div>
|
||||
<div v-else class="loading-message">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<span>Loading bid notes...</span>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import Modal from "../common/Modal.vue";
|
||||
import BidMeetingNotes from "./BidMeetingNotes.vue";
|
||||
import Api from "../../api";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
|
||||
|
|
@ -214,14 +176,10 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["update:visible", "close", "meetingUpdated", "completeMeeting"]);
|
||||
const emit = defineEmits(["update:visible", "close", "meetingUpdated"]);
|
||||
|
||||
// Local state
|
||||
const isUpdating = ref(false);
|
||||
const showBidNotesModal = ref(false);
|
||||
const bidNoteData = ref(null);
|
||||
const loadingBidNotes = ref(false);
|
||||
const bidNotesError = ref(null);
|
||||
|
||||
const showModal = computed({
|
||||
get() {
|
||||
|
|
@ -240,13 +198,6 @@ const modalOptions = computed(() => ({
|
|||
confirmButtonColor: "primary",
|
||||
}));
|
||||
|
||||
const bidNotesModalOptions = computed(() => ({
|
||||
maxWidth: "1000px",
|
||||
showCancelButton: false,
|
||||
confirmButtonText: "Close",
|
||||
confirmButtonColor: "primary",
|
||||
}));
|
||||
|
||||
// Computed properties for data extraction
|
||||
const customerName = computed(() => {
|
||||
if (props.meeting?.address?.customerName) {
|
||||
|
|
@ -318,20 +269,34 @@ const handleClose = () => {
|
|||
const handleMarkComplete = async () => {
|
||||
if (!props.meeting?.name) return;
|
||||
|
||||
// Check if meeting has a project template
|
||||
if (!props.meeting.projectTemplate) {
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
|
||||
await Api.updateBidMeeting(props.meeting.name, {
|
||||
status: "Completed",
|
||||
});
|
||||
|
||||
notificationStore.addNotification({
|
||||
type: "warning",
|
||||
title: "Missing Project Template",
|
||||
message: "This meeting requires a project template to create notes.",
|
||||
type: "success",
|
||||
title: "Meeting Completed",
|
||||
message: "The meeting has been marked as completed.",
|
||||
duration: 4000,
|
||||
});
|
||||
|
||||
// Emit event to refresh the calendar
|
||||
emit("meetingUpdated");
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error("Error marking meeting as complete:", error);
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: "Failed to update meeting status.",
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
|
||||
// Open the note form modal
|
||||
emit("completeMeeting", props.meeting);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleCreateEstimate = () => {
|
||||
|
|
@ -371,43 +336,6 @@ const formatDateTime = (dateString) => {
|
|||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewBidNotes = async () => {
|
||||
if (!props.meeting?.bidNotes) return;
|
||||
|
||||
try {
|
||||
loadingBidNotes.value = true;
|
||||
bidNotesError.value = null;
|
||||
bidNoteData.value = null;
|
||||
|
||||
// Fetch the bid meeting note
|
||||
const noteData = await Api.getBidMeetingNote(props.meeting.bidNotes);
|
||||
|
||||
if (!noteData) {
|
||||
throw new Error("Failed to load bid notes");
|
||||
}
|
||||
|
||||
bidNoteData.value = noteData;
|
||||
showBidNotesModal.value = true;
|
||||
} catch (error) {
|
||||
console.error("Error loading bid notes:", error);
|
||||
bidNotesError.value = error.message || "Failed to load bid notes";
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: "Failed to load bid notes. Please try again.",
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
loadingBidNotes.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseBidNotes = () => {
|
||||
showBidNotesModal.value = false;
|
||||
bidNoteData.value = null;
|
||||
bidNotesError.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -449,24 +377,5 @@ const handleCloseBidNotes = () => {
|
|||
padding-top: 16px;
|
||||
border-top: 2px solid #e0e0e0;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.error-message,
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.error-message i {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -129,16 +129,13 @@ import DataTable from "../common/DataTable.vue";
|
|||
import TodoChart from "../common/TodoChart.vue";
|
||||
import { Edit, ChatBubbleQuestion, CreditCard, Hammer } from "@iconoir/vue";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import Api from "../../api";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
import { usePaginationStore } from "../../stores/pagination";
|
||||
import { useFiltersStore } from "../../stores/filters";
|
||||
import { useCompanyStore } from "../../stores/company.js";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCompanyStore } from "../../stores/company";
|
||||
|
||||
const companyStore = useCompanyStore();
|
||||
const loadingStore = useLoadingStore();
|
||||
const paginationStore = usePaginationStore();
|
||||
const filtersStore = useFiltersStore();
|
||||
|
|
@ -238,7 +235,7 @@ const handleLazyLoad = async (event) => {
|
|||
};
|
||||
|
||||
// Get filters (convert PrimeVue format to API format)
|
||||
const filters = {company: companyStore.currentCompany};
|
||||
const filters = {};
|
||||
if (event.filters) {
|
||||
Object.keys(event.filters).forEach((key) => {
|
||||
if (key !== "global" && event.filters[key] && event.filters[key].value) {
|
||||
|
|
@ -284,8 +281,6 @@ const handleLazyLoad = async (event) => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const loadChartData = async () => {
|
||||
chartData.value.bids.data = await Api.getIncompleteBidsCount(companyStore.currentCompany);
|
||||
chartData.value.estimates.data = await Api.getUnapprovedEstimatesCount(companyStore.currentCompany);
|
||||
|
|
@ -313,21 +308,14 @@ onMounted(async () => {
|
|||
sortOrder: initialSorting.order || initialPagination.sortOrder,
|
||||
filters: initialFilters,
|
||||
});
|
||||
|
||||
// Chart Data
|
||||
await loadChartData();
|
||||
});
|
||||
|
||||
// watch the company store and refetch data when it changes
|
||||
watch(
|
||||
() => companyStore.currentCompany,
|
||||
async (newCompany, oldCompany) => {
|
||||
console.log("Company changed from", oldCompany, "to", newCompany, "- refetching estimates data.");
|
||||
await handleLazyLoad({
|
||||
page: paginationStore.getTablePagination("estimates").page,
|
||||
rows: paginationStore.getTablePagination("estimates").rows,
|
||||
first: paginationStore.getTablePagination("estimates").first,
|
||||
filters: filtersStore.getTableFilters("estimates"),
|
||||
});
|
||||
}
|
||||
);
|
||||
watch(() => companyStore.currentCompany, async (newCompany, oldCompany) => {
|
||||
await loadChartData();
|
||||
});
|
||||
|
||||
</script>
|
||||
<style lang="css">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue