Compare commits
No commits in common. "4f06984a0d39585e518a3a3807fbe1c5f729db29" and "d53ebf9ecd45371d7287e65b577335eb9fe7986b" have entirely different histories.
4f06984a0d
...
d53ebf9ecd
26 changed files with 425 additions and 1892 deletions
|
|
@ -1,15 +1,31 @@
|
||||||
import frappe
|
import frappe
|
||||||
import json
|
import json
|
||||||
from custom_ui.db_utils import build_error_response, build_success_response
|
from custom_ui.db_utils import build_error_response, build_success_response
|
||||||
from custom_ui.services import ClientService, AddressService
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_address_by_full_address(full_address):
|
def get_address_by_full_address(full_address):
|
||||||
"""Get address by full_address, including associated contacts."""
|
"""Get address by full_address, including associated contacts."""
|
||||||
print(f"DEBUG: get_address_by_full_address called with full_address: {full_address}")
|
print(f"DEBUG: get_address_by_full_address called with full_address: {full_address}")
|
||||||
try:
|
try:
|
||||||
address = AddressService.get_address_by_full_address(full_address)
|
address = frappe.get_doc("Address", {"full_address": full_address}).as_dict()
|
||||||
return build_success_response(AddressService.build_full_dict(address))
|
customer_exists = frappe.db.exists("Customer", address.get("custom_customer_to_bill"))
|
||||||
|
doctype = "Customer" if customer_exists else "Lead"
|
||||||
|
name = ""
|
||||||
|
if doctype == "Customer":
|
||||||
|
name = address.get("custom_customer_to_bill")
|
||||||
|
else:
|
||||||
|
## filter through links for one with doctype Lead
|
||||||
|
lead_links = address.get("links", [])
|
||||||
|
print(f"DEBUG: lead_links: {lead_links}")
|
||||||
|
lead_name = [link.link_name for link in lead_links if link.link_doctype == "Lead"]
|
||||||
|
name = lead_name[0] if lead_name else ""
|
||||||
|
address["customer"] = frappe.get_doc(doctype, name).as_dict()
|
||||||
|
contacts = []
|
||||||
|
for contact_link in address.custom_linked_contacts:
|
||||||
|
contact_doc = frappe.get_doc("Contact", contact_link.contact)
|
||||||
|
contacts.append(contact_doc.as_dict())
|
||||||
|
address["contacts"] = contacts
|
||||||
|
return build_success_response(address)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
|
|
@ -17,23 +33,23 @@ def get_address_by_full_address(full_address):
|
||||||
def get_address(address_name):
|
def get_address(address_name):
|
||||||
"""Get a specific address by name."""
|
"""Get a specific address by name."""
|
||||||
try:
|
try:
|
||||||
address = AddressService.get_or_throw(address_name)
|
address = frappe.get_doc("Address", address_name)
|
||||||
return build_success_response(address.as_dict())
|
return build_success_response(address.as_dict())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
# @frappe.whitelist() #### DEPRECATED FUNCTION
|
@frappe.whitelist()
|
||||||
# def get_contacts_for_address(address_name):
|
def get_contacts_for_address(address_name):
|
||||||
# """Get contacts linked to a specific address."""
|
"""Get contacts linked to a specific address."""
|
||||||
# try:
|
try:
|
||||||
# address = AddressService.get_or_throw(address_name)
|
address = frappe.get_doc("Address", address_name)
|
||||||
# contacts = []
|
contacts = []
|
||||||
# for contact_link in address.custom_linked_contacts:
|
for contact_link in address.custom_linked_contacts:
|
||||||
# contact = frappe.get_doc("Contact", contact_link.contact)
|
contact = frappe.get_doc("Contact", contact_link.contact)
|
||||||
# contacts.append(contact.as_dict())
|
contacts.append(contact.as_dict())
|
||||||
# return build_success_response(contacts)
|
return build_success_response(contacts)
|
||||||
# except Exception as e:
|
except Exception as e:
|
||||||
# return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_addresses(fields=["*"], filters={}):
|
def get_addresses(fields=["*"], filters={}):
|
||||||
|
|
@ -58,6 +74,16 @@ def get_addresses(fields=["*"], filters={}):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.log_error(message=str(e), title="Get Addresses Failed")
|
frappe.log_error(message=str(e), title="Get Addresses Failed")
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
|
|
||||||
|
def create_address(address_data):
|
||||||
|
"""Create a new address."""
|
||||||
|
address = frappe.get_doc({
|
||||||
|
"doctype": "Address",
|
||||||
|
**address_data
|
||||||
|
})
|
||||||
|
address.insert(ignore_permissions=True)
|
||||||
|
return address
|
||||||
|
|
||||||
def update_address(address_data):
|
def update_address(address_data):
|
||||||
"""Update an existing address."""
|
"""Update an existing address."""
|
||||||
|
|
@ -80,10 +106,19 @@ def address_exists(address_line1, address_line2, city, state, pincode):
|
||||||
}
|
}
|
||||||
return frappe.db.exists("Address", filters) is not None
|
return frappe.db.exists("Address", filters) is not None
|
||||||
|
|
||||||
|
def check_and_get_address_by_name(address_name):
|
||||||
|
"""Check if an address exists by name and return the address document if found."""
|
||||||
|
if frappe.db.exists("Address", address_name):
|
||||||
|
return frappe.get_doc("Address", address_name)
|
||||||
|
raise ValueError(f"Address with name {address_name} does not exist.")
|
||||||
|
|
||||||
|
def address_exists_by_name(address_name):
|
||||||
|
"""Check if an address with the given name exists."""
|
||||||
|
return frappe.db.exists("Address", address_name) is not None
|
||||||
|
|
||||||
def calculate_address_title(customer_name, address_data):
|
def calculate_address_title(customer_name, address_data):
|
||||||
return f"{customer_name} - {address_data.get('address_line1', '')}, {address_data.get('city', '')} - {address_data.get('type', '')}"
|
return f"{customer_name} - {address_data.get('address_line1', '')}, {address_data.get('city', '')} - {address_data.get('type', '')}"
|
||||||
|
|
||||||
|
|
||||||
def create_address_links(address_doc, client_doc, contact_docs):
|
def create_address_links(address_doc, client_doc, contact_docs):
|
||||||
print("#####DEBUG: Linking customer to address.")
|
print("#####DEBUG: Linking customer to address.")
|
||||||
print("#####DEBUG: Client Doc:", client_doc.as_dict(), "Address Doc:", address_doc.as_dict(), "Contact Docs:", [c.as_dict() for c in contact_docs])
|
print("#####DEBUG: Client Doc:", client_doc.as_dict(), "Address Doc:", address_doc.as_dict(), "Contact Docs:", [c.as_dict() for c in contact_docs])
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import frappe
|
import frappe
|
||||||
import json
|
import json
|
||||||
from custom_ui.db_utils import build_error_response, build_success_response, process_filters, process_sorting
|
from custom_ui.db_utils import build_error_response, build_success_response, process_filters, process_sorting
|
||||||
from custom_ui.services import DbService, ClientService, AddressService
|
from custom_ui.services import DbService
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_week_bid_meetings(week_start, week_end):
|
def get_week_bid_meetings(week_start, week_end):
|
||||||
|
|
@ -67,51 +67,38 @@ def get_unscheduled_bid_meetings():
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_bid_meeting(name):
|
def create_bid_meeting(address, notes="", company=None, contact=None):
|
||||||
"""Get a specific On-Site Meeting by name."""
|
|
||||||
try:
|
|
||||||
meeting = frappe.get_doc("On-Site Meeting", name)
|
|
||||||
meeting_dict = meeting.as_dict()
|
|
||||||
|
|
||||||
# Get the full address data
|
|
||||||
if meeting_dict.get("address"):
|
|
||||||
address_doc = frappe.get_doc("Address", meeting_dict["address"])
|
|
||||||
meeting_dict["address"] = address_doc.as_dict()
|
|
||||||
|
|
||||||
return build_success_response(meeting_dict)
|
|
||||||
except frappe.DoesNotExistError:
|
|
||||||
return build_error_response(f"On-Site Meeting '{name}' does not exist.", 404)
|
|
||||||
except Exception as e:
|
|
||||||
frappe.log_error(message=str(e), title="Get On-Site Meeting Failed")
|
|
||||||
return build_error_response(str(e), 500)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def create_bid_meeting(data):
|
|
||||||
"""Create a new On-Site Meeting with Unscheduled status."""
|
"""Create a new On-Site Meeting with Unscheduled status."""
|
||||||
if isinstance(data, str):
|
|
||||||
data = json.loads(data)
|
|
||||||
try:
|
try:
|
||||||
print(f"DEBUG: Creating meeting with data='{data}'")
|
print(f"DEBUG: Creating meeting with address='{address}', notes='{notes}', company='{company}'")
|
||||||
|
|
||||||
|
# Validate address parameter
|
||||||
|
if not address or address == "None" or not address.strip():
|
||||||
|
return build_error_response("Address is required and cannot be empty.", 400)
|
||||||
|
|
||||||
|
# Get the address document name from the full address string
|
||||||
|
address_name = frappe.db.get_value("Address", filters={"full_address": address}, fieldname="name")
|
||||||
|
|
||||||
address_doc = DbService.get_or_throw("Address", data.get("address"))
|
|
||||||
|
print(f"DEBUG: Address lookup result: address_name='{address_name}'")
|
||||||
|
|
||||||
|
if not address_name:
|
||||||
|
return build_error_response(f"Address '{address}' not found in the system.", 404)
|
||||||
|
address_doc = DbService.get("Address", address_name)
|
||||||
|
|
||||||
# Create the meeting with Unscheduled status
|
# Create the meeting with Unscheduled status
|
||||||
meeting = frappe.get_doc({
|
meeting = frappe.get_doc({
|
||||||
"doctype": "On-Site Meeting",
|
"doctype": "On-Site Meeting",
|
||||||
"address": address_doc.name,
|
"address": address_doc.name,
|
||||||
"notes": data.get("notes") or "",
|
"notes": notes or "",
|
||||||
"status": "Unscheduled",
|
"status": "Unscheduled",
|
||||||
"company": data.get("company"),
|
"company": company,
|
||||||
"contact": data.get("contact"),
|
"contact": contact,
|
||||||
"party_type": address_doc.customer_type,
|
"party_type": address_doc.customer_type,
|
||||||
"party_name": address_doc.customer_name,
|
"party_name": address_doc.customer_name
|
||||||
"project_template": data.get("project_template")
|
|
||||||
})
|
})
|
||||||
meeting.insert(ignore_permissions=True)
|
|
||||||
# ClientService.append_link(address_doc.customer_name, "onsite_meetings", "onsite_meeting", meeting.name)
|
|
||||||
# AddressService.append_link(address_doc.name, "onsite_meetings", "onsite_meeting", meeting.name)
|
|
||||||
meeting.flags.ignore_permissions = True
|
meeting.flags.ignore_permissions = True
|
||||||
|
meeting.insert(ignore_permissions=True)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
# Clear any auto-generated messages from Frappe
|
# Clear any auto-generated messages from Frappe
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import frappe, json
|
import frappe, json
|
||||||
from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, map_lead_client, build_address_title
|
from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, map_lead_client, build_address_title
|
||||||
from erpnext.crm.doctype.lead.lead import make_customer
|
from erpnext.crm.doctype.lead.lead import make_customer
|
||||||
from custom_ui.api.db.addresses import address_exists
|
from custom_ui.api.db.addresses import address_exists, create_address, create_address_links
|
||||||
from custom_ui.api.db.contacts import check_and_get_contact, create_contact, create_contact_links
|
from custom_ui.api.db.contacts import check_and_get_contact, create_contact, create_contact_links
|
||||||
from custom_ui.services import AddressService, ContactService, ClientService
|
|
||||||
|
|
||||||
# ===============================================================================
|
# ===============================================================================
|
||||||
# CLIENT MANAGEMENT API METHODS
|
# CLIENT MANAGEMENT API METHODS
|
||||||
|
|
@ -97,7 +96,7 @@ def get_client(client_name):
|
||||||
"""Get detailed information for a specific client including address, customer, and projects."""
|
"""Get detailed information for a specific client including address, customer, and projects."""
|
||||||
print("DEBUG: get_client called with client_name:", client_name)
|
print("DEBUG: get_client called with client_name:", client_name)
|
||||||
try:
|
try:
|
||||||
clientData = {"addresses": [], "contacts": [], "jobs": [], "sales_invoices": [], "payment_entries": [], "tasks": []}
|
clientData = {"addresses": [], "contacts": [], "jobs": [], "sales_invoices": [], "payment_entries": [], "sales_orders": [], "tasks": []}
|
||||||
customer = check_and_get_client_doc(client_name)
|
customer = check_and_get_client_doc(client_name)
|
||||||
if not customer:
|
if not customer:
|
||||||
return build_error_response(f"Client with name '{client_name}' does not exist.", 404)
|
return build_error_response(f"Client with name '{client_name}' does not exist.", 404)
|
||||||
|
|
@ -143,30 +142,6 @@ def get_client(client_name):
|
||||||
return build_error_response(str(ve), 400)
|
return build_error_response(str(ve), 400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_client_v2(client_name):
|
|
||||||
"""Get detailed information for a specific client including address, customer, and projects."""
|
|
||||||
print("DEBUG: get_client_v2 called with client_name:", client_name)
|
|
||||||
try:
|
|
||||||
clientData = {"addresses": [], "jobs": [], "payment_entries": [], "tasks": []}
|
|
||||||
customer = check_and_get_client_doc(client_name)
|
|
||||||
if not customer:
|
|
||||||
return build_error_response(f"Client with name '{client_name}' does not exist.", 404)
|
|
||||||
print("DEBUG: Retrieved customer/lead document:", customer.as_dict())
|
|
||||||
clientData = {**clientData, **customer.as_dict()}
|
|
||||||
clientData["contacts"] = [ContactService.get_or_throw(link.contact) for link in clientData["contacts"]]
|
|
||||||
clientData["addresses"] = [AddressService.get_or_throw(link.address) for link in clientData["properties"]]
|
|
||||||
if clientData["doctype"] == "Lead":
|
|
||||||
clientData["customer_name"] = customer.custom_customer_name
|
|
||||||
|
|
||||||
# TODO: Continue getting other linked docs like jobs, invoices, etc.
|
|
||||||
print("DEBUG: Final client data prepared:", clientData)
|
|
||||||
return build_success_response(clientData)
|
|
||||||
except frappe.ValidationError as ve:
|
|
||||||
return build_error_response(str(ve), 400)
|
|
||||||
except Exception as e:
|
|
||||||
return build_error_response(str(e), 500)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
@ -214,7 +189,8 @@ def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||||
customer_links = [link for link in links if link.link_doctype == "Lead"] if links else None
|
customer_links = [link for link in links if link.link_doctype == "Lead"] if links else None
|
||||||
is_lead = True if customer_links else False
|
is_lead = True if customer_links else False
|
||||||
if not customer_name and not customer_links:
|
if not customer_name and not customer_links:
|
||||||
customer_name = frappe.get_value("Lead", address.get("customer_name"), "custom_customer_name")
|
print("DEBUG: No customer links found and no customer to bill.")
|
||||||
|
customer_name = "N/A"
|
||||||
elif not customer_name and customer_links:
|
elif not customer_name and customer_links:
|
||||||
print("DEBUG: No customer to bill. Customer links found:", customer_links)
|
print("DEBUG: No customer to bill. Customer links found:", customer_links)
|
||||||
customer_name = frappe.get_value("Lead", customer_links[0].link_name, "custom_customer_name") if is_lead else customer_links[0].link_name
|
customer_name = frappe.get_value("Lead", customer_links[0].link_name, "custom_customer_name") if is_lead else customer_links[0].link_name
|
||||||
|
|
@ -226,9 +202,9 @@ def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||||
f"{address['city']}, {address['state']} {address['pincode']}"
|
f"{address['city']}, {address['state']} {address['pincode']}"
|
||||||
)
|
)
|
||||||
tableRow["client_type"] = "Lead" if is_lead else "Customer"
|
tableRow["client_type"] = "Lead" if is_lead else "Customer"
|
||||||
# tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled
|
tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled
|
||||||
# tableRow["estimate_sent_status"] = address.custom_estimate_sent_status
|
tableRow["estimate_sent_status"] = address.custom_estimate_sent_status
|
||||||
# tableRow["job_status"] = address.custom_job_status
|
tableRow["job_status"] = address.custom_job_status
|
||||||
tableRow["payment_received_status"] = address.custom_payment_received_status
|
tableRow["payment_received_status"] = address.custom_payment_received_status
|
||||||
tableRows.append(tableRow)
|
tableRows.append(tableRow)
|
||||||
tableDataDict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size)
|
tableDataDict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size)
|
||||||
|
|
@ -280,21 +256,19 @@ def upsert_client(data):
|
||||||
|
|
||||||
customer_name = data.get("customer_name")
|
customer_name = data.get("customer_name")
|
||||||
contacts = data.get("contacts", [])
|
contacts = data.get("contacts", [])
|
||||||
addresses = data.get("addresses", [])
|
|
||||||
|
|
||||||
# Check for existing address
|
# Check for existing address
|
||||||
client_doc = check_and_get_client_doc(customer_name)
|
client_doc = check_and_get_client_doc(customer_name)
|
||||||
if client_doc:
|
if client_doc:
|
||||||
return build_error_response(f"Client with name '{customer_name}' already exists.", 400)
|
return build_error_response(f"Client with name '{customer_name}' already exists.", 400)
|
||||||
for address in addresses:
|
if address_exists(
|
||||||
if address_exists(
|
data.get("address_line1"),
|
||||||
address.get("address_line1"),
|
data.get("address_line2"),
|
||||||
address.get("address_line2"),
|
data.get("city"),
|
||||||
address.get("city"),
|
data.get("state"),
|
||||||
address.get("state"),
|
data.get("pincode")
|
||||||
address.get("pincode")
|
):
|
||||||
):
|
return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400)
|
||||||
return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400)
|
|
||||||
|
|
||||||
# Handle customer creation/update
|
# Handle customer creation/update
|
||||||
|
|
||||||
|
|
@ -306,16 +280,28 @@ def upsert_client(data):
|
||||||
"last_name": primary_contact.get("last_name"),
|
"last_name": primary_contact.get("last_name"),
|
||||||
"email_id": primary_contact.get("email"),
|
"email_id": primary_contact.get("email"),
|
||||||
"phone": primary_contact.get("phone_number"),
|
"phone": primary_contact.get("phone_number"),
|
||||||
|
"company": data.get("company"),
|
||||||
"custom_customer_name": customer_name,
|
"custom_customer_name": customer_name,
|
||||||
"customer_type": customer_type,
|
"customer_type": customer_type
|
||||||
"companies": [{ "company": data.get("company_name")
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
if customer_type == "Company":
|
if customer_type == "Company":
|
||||||
lead_data["company_name"] = data.get("customer_name")
|
lead_data["company_name"] = data.get("customer_name")
|
||||||
client_doc = create_lead(lead_data)
|
client_doc = create_lead(lead_data)
|
||||||
print(f"#####DEBUG: {client_doc.doctype}:", client_doc.as_dict())
|
print(f"#####DEBUG: {client_doc.doctype}:", client_doc.as_dict())
|
||||||
|
|
||||||
|
# Handle address creation
|
||||||
|
address_doc = create_address({
|
||||||
|
"address_title": build_address_title(customer_name, data),
|
||||||
|
"address_line1": data.get("address_line1"),
|
||||||
|
"address_line2": data.get("address_line2"),
|
||||||
|
"city": data.get("city"),
|
||||||
|
"state": data.get("state"),
|
||||||
|
"country": "United States",
|
||||||
|
"pincode": data.get("pincode"),
|
||||||
|
"customer_type": "Lead",
|
||||||
|
"customer_name": client_doc.name
|
||||||
|
})
|
||||||
|
|
||||||
#Handle contact creation
|
#Handle contact creation
|
||||||
contact_docs = []
|
contact_docs = []
|
||||||
for contact_data in contacts:
|
for contact_data in contacts:
|
||||||
|
|
@ -330,14 +316,12 @@ def upsert_client(data):
|
||||||
)
|
)
|
||||||
if not contact_doc:
|
if not contact_doc:
|
||||||
print("#####DEBUG: No existing contact found. Creating new contact.")
|
print("#####DEBUG: No existing contact found. Creating new contact.")
|
||||||
contact_doc = ContactService.create({
|
contact_doc = create_contact({
|
||||||
"first_name": contact_data.get("first_name"),
|
"first_name": contact_data.get("first_name"),
|
||||||
"last_name": contact_data.get("last_name"),
|
"last_name": contact_data.get("last_name"),
|
||||||
"role": contact_data.get("contact_role", "Other"),
|
"role": contact_data.get("contact_role", "Other"),
|
||||||
"custom_email": contact_data.get("email"),
|
"custom_email": contact_data.get("email"),
|
||||||
"is_primary_contact":1 if contact_data.get("is_primary", False) else 0,
|
"is_primary_contact":1 if contact_data.get("is_primary", False) else 0,
|
||||||
"customer_type": "Lead",
|
|
||||||
"customer_name": client_doc.name,
|
|
||||||
"email_ids": [{
|
"email_ids": [{
|
||||||
"email_id": contact_data.get("email"),
|
"email_id": contact_data.get("email"),
|
||||||
"is_primary": 1
|
"is_primary": 1
|
||||||
|
|
@ -348,61 +332,37 @@ def upsert_client(data):
|
||||||
"is_primary_phone": 1
|
"is_primary_phone": 1
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
ContactService.link_contact_to_customer(contact_doc, "Lead", client_doc.name)
|
|
||||||
contact_docs.append(contact_doc)
|
contact_docs.append(contact_doc)
|
||||||
|
|
||||||
# Link all contacts to client after creating them
|
|
||||||
client_doc.reload()
|
|
||||||
for idx, contact_data in enumerate(contacts):
|
|
||||||
if isinstance(contact_data, str):
|
|
||||||
contact_data = json.loads(contact_data)
|
|
||||||
contact_doc = contact_docs[idx]
|
|
||||||
client_doc.append("contacts", {
|
|
||||||
"contact": contact_doc.name
|
|
||||||
})
|
|
||||||
if contact_data.get("is_primary", False):
|
|
||||||
client_doc.primary_contact = contact_doc.name
|
|
||||||
client_doc.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
# Handle address creation
|
# ##### Create links
|
||||||
address_docs = []
|
# # Customer -> Address
|
||||||
for address in addresses:
|
# if client_doc.doctype == "Customer":
|
||||||
print("#####DEBUG: Creating address with data:", address)
|
# print("#####DEBUG: Linking address to customer.")
|
||||||
address_doc = AddressService.create_address({
|
# client_doc.append("custom_select_address", {
|
||||||
"address_title": build_address_title(customer_name, address),
|
# "address_name": address_doc.name,
|
||||||
"address_line1": address.get("address_line1"),
|
# })
|
||||||
"address_line2": address.get("address_line2"),
|
|
||||||
"city": address.get("city"),
|
# # Customer -> Contact
|
||||||
"state": address.get("state"),
|
# print("#####DEBUG: Linking contacts to customer.")
|
||||||
"country": "United States",
|
# for contact_doc in contact_docs:
|
||||||
"pincode": address.get("pincode"),
|
# client_doc.append("custom_add_contacts", {
|
||||||
"customer_type": "Lead",
|
# "contact": contact_doc.name,
|
||||||
"customer_name": client_doc.name,
|
# "email": contact_doc.custom_email,
|
||||||
"companies": [{ "company": data.get("company_name") }]
|
# "phone": contact_doc.phone,
|
||||||
})
|
# "role": contact_doc.role
|
||||||
AddressService.link_address_to_customer(address_doc, "Lead", client_doc.name)
|
# })
|
||||||
address_doc.reload()
|
# client_doc.save(ignore_permissions=True)
|
||||||
for contact_to_link_idx in address.get("contacts", []):
|
|
||||||
contact_doc = contact_docs[contact_to_link_idx]
|
# Address -> Customer/Lead
|
||||||
AddressService.link_address_to_contact(address_doc, contact_doc.name)
|
create_address_links(address_doc, client_doc, contact_docs)
|
||||||
address_doc.reload()
|
|
||||||
ContactService.link_contact_to_address(contact_doc, address_doc.name)
|
# Contact -> Customer/Lead & Address
|
||||||
primary_contact = contact_docs[address.get("primary_contact)", 0)]
|
create_contact_links(contact_docs, client_doc, address_doc)
|
||||||
AddressService.set_primary_contact(address_doc.name, primary_contact.name)
|
|
||||||
address_docs.append(address_doc)
|
|
||||||
|
|
||||||
# Link all addresses to client after creating them
|
|
||||||
client_doc.reload()
|
|
||||||
for address_doc in address_docs:
|
|
||||||
client_doc.append("properties", {
|
|
||||||
"address": address_doc.name
|
|
||||||
})
|
|
||||||
client_doc.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
frappe.local.message_log = []
|
frappe.local.message_log = []
|
||||||
return build_success_response({
|
return build_success_response({
|
||||||
"customer": client_doc.as_dict(),
|
"customer": client_doc.as_dict(),
|
||||||
"address": [address_doc.as_dict() for address_doc in address_docs],
|
"address": address_doc.as_dict(),
|
||||||
"contacts": [contact_doc.as_dict() for contact_doc in contact_docs]
|
"contacts": [contact_doc.as_dict() for contact_doc in contact_docs]
|
||||||
})
|
})
|
||||||
except frappe.ValidationError as ve:
|
except frappe.ValidationError as ve:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from custom_ui.api.db.general import get_doc_history
|
||||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer
|
from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer
|
||||||
from custom_ui.services import DbService, ClientService, AddressService
|
|
||||||
|
|
||||||
# ===============================================================================
|
# ===============================================================================
|
||||||
# ESTIMATES & INVOICES API METHODS
|
# ESTIMATES & INVOICES API METHODS
|
||||||
|
|
@ -245,11 +244,11 @@ def update_response(name, response):
|
||||||
|
|
||||||
if accepted:
|
if accepted:
|
||||||
template = "custom_ui/templates/estimates/accepted.html"
|
template = "custom_ui/templates/estimates/accepted.html"
|
||||||
# if check_if_customer(estimate.party_name):
|
if check_if_customer(estimate.party_name):
|
||||||
# print("DEBUG: Party is already a customer:", estimate.party_name)
|
print("DEBUG: Party is already a customer:", estimate.party_name)
|
||||||
# else:
|
else:
|
||||||
# print("DEBUG: Converting lead to customer for party:", estimate.party_name)
|
print("DEBUG: Converting lead to customer for party:", estimate.party_name)
|
||||||
# convert_lead_to_customer(estimate.party_name)
|
convert_lead_to_customer(estimate.party_name)
|
||||||
elif response == "Requested call":
|
elif response == "Requested call":
|
||||||
template = "custom_ui/templates/estimates/request-call.html"
|
template = "custom_ui/templates/estimates/request-call.html"
|
||||||
else:
|
else:
|
||||||
|
|
@ -385,8 +384,7 @@ def upsert_estimate(data):
|
||||||
print("DEBUG: Upsert estimate data:", data)
|
print("DEBUG: Upsert estimate data:", data)
|
||||||
|
|
||||||
estimate_name = data.get("estimate_name")
|
estimate_name = data.get("estimate_name")
|
||||||
client_doctype = ClientService.get_client_doctype(data.get("customer"))
|
is_customer = True if frappe.db.exists("Customer", data.get("customer")) else False
|
||||||
project_template = data.get("project_template", None)
|
|
||||||
|
|
||||||
# If estimate_name exists, update existing estimate
|
# If estimate_name exists, update existing estimate
|
||||||
if estimate_name:
|
if estimate_name:
|
||||||
|
|
@ -394,21 +392,11 @@ def upsert_estimate(data):
|
||||||
estimate = frappe.get_doc("Quotation", estimate_name)
|
estimate = frappe.get_doc("Quotation", estimate_name)
|
||||||
|
|
||||||
# Update fields
|
# Update fields
|
||||||
# estimate.custom_installation_address = data.get("address")
|
estimate.custom_installation_address = data.get("address")
|
||||||
# estimate.custom_job_address = data.get("address_name")
|
estimate.party_name = data.get("customer")
|
||||||
# estimate.party_name = data.get("customer")
|
estimate.contact_person = data.get("contact_name")
|
||||||
# estimate.contact_person = data.get("contact_name")
|
|
||||||
estimate.custom_requires_half_payment = data.get("requires_half_payment", 0)
|
estimate.custom_requires_half_payment = data.get("requires_half_payment", 0)
|
||||||
estimate.custom_project_template = project_template
|
|
||||||
estimate.custom_quotation_template = data.get("quotation_template", None)
|
|
||||||
# estimate.company = data.get("company")
|
|
||||||
# estimate.contact_email = data.get("contact_email")
|
|
||||||
# estimate.quotation_to = client_doctype
|
|
||||||
# estimate.customer_name = data.get("customer")
|
|
||||||
# estimate.customer_address = data.get("address_name")
|
|
||||||
# estimate.letter_head = data.get("company")
|
|
||||||
# estimate.from_onsite_meeting = data.get("onsite_meeting", None)
|
|
||||||
|
|
||||||
# Clear existing items and add new ones
|
# Clear existing items and add new ones
|
||||||
estimate.items = []
|
estimate.items = []
|
||||||
for item in data.get("items", []):
|
for item in data.get("items", []):
|
||||||
|
|
@ -430,7 +418,6 @@ def upsert_estimate(data):
|
||||||
else:
|
else:
|
||||||
print("DEBUG: Creating new estimate")
|
print("DEBUG: Creating new estimate")
|
||||||
print("DEBUG: Retrieved address name:", data.get("address_name"))
|
print("DEBUG: Retrieved address name:", data.get("address_name"))
|
||||||
client_doctype = ClientService.get_client_doctype(data.get("customer"))
|
|
||||||
new_estimate = frappe.get_doc({
|
new_estimate = frappe.get_doc({
|
||||||
"doctype": "Quotation",
|
"doctype": "Quotation",
|
||||||
"custom_requires_half_payment": data.get("requires_half_payment", 0),
|
"custom_requires_half_payment": data.get("requires_half_payment", 0),
|
||||||
|
|
@ -438,15 +425,13 @@ def upsert_estimate(data):
|
||||||
"custom_current_status": "Draft",
|
"custom_current_status": "Draft",
|
||||||
"contact_email": data.get("contact_email"),
|
"contact_email": data.get("contact_email"),
|
||||||
"party_name": data.get("customer"),
|
"party_name": data.get("customer"),
|
||||||
"quotation_to": client_doctype,
|
"quotation_to": "Customer" if is_customer else "Lead",
|
||||||
"company": data.get("company"),
|
"company": data.get("company"),
|
||||||
"customer": data.get("customer"),
|
"customer_name": data.get("customer"),
|
||||||
"customer_type": client_doctype,
|
|
||||||
"customer_address": data.get("address_name"),
|
"customer_address": data.get("address_name"),
|
||||||
"contact_person": data.get("contact_name"),
|
"contact_person": data.get("contact_name"),
|
||||||
"letter_head": data.get("company"),
|
"letter_head": data.get("company"),
|
||||||
"custom_project_template": data.get("project_template", None),
|
"custom_project_template": data.get("project_template", None),
|
||||||
"custom_quotation_template": data.get("quotation_template", None),
|
|
||||||
"from_onsite_meeting": data.get("onsite_meeting", None)
|
"from_onsite_meeting": data.get("onsite_meeting", None)
|
||||||
})
|
})
|
||||||
for item in data.get("items", []):
|
for item in data.get("items", []):
|
||||||
|
|
@ -458,8 +443,6 @@ def upsert_estimate(data):
|
||||||
"discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0)
|
"discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0)
|
||||||
})
|
})
|
||||||
new_estimate.insert()
|
new_estimate.insert()
|
||||||
AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name)
|
|
||||||
ClientService.append_link(data.get("customer"), "quotations", "quotation", new_estimate.name)
|
|
||||||
print("DEBUG: New estimate created with name:", new_estimate.name)
|
print("DEBUG: New estimate created with name:", new_estimate.name)
|
||||||
return build_success_response(new_estimate.as_dict())
|
return build_success_response(new_estimate.as_dict())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -201,16 +201,3 @@ def get_install_projects(start_date=None, end_date=None):
|
||||||
return {"status": "success", "data": calendar_events}
|
return {"status": "success", "data": calendar_events}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_project_templates_for_company(company_name):
|
|
||||||
"""Get project templates for a specific company."""
|
|
||||||
try:
|
|
||||||
templates = frappe.get_all(
|
|
||||||
"Project Template",
|
|
||||||
fields=["*"],
|
|
||||||
filters={"company": company_name}
|
|
||||||
)
|
|
||||||
return build_success_response(templates)
|
|
||||||
except Exception as e:
|
|
||||||
return build_error_response(str(e), 500),
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import frappe
|
import frappe
|
||||||
from custom_ui.db_utils import build_full_address
|
from custom_ui.db_utils import build_full_address
|
||||||
|
|
||||||
def before_insert(doc, method):
|
def after_insert(doc, method):
|
||||||
print("DEBUG: Before Insert Triggered for Address")
|
print(doc.as_dict())
|
||||||
if not doc.full_address:
|
if not doc.full_address:
|
||||||
doc.full_address = build_full_address(doc)
|
doc.full_address = build_full_address(doc)
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
|
@ -1,69 +1,67 @@
|
||||||
import frappe
|
import frappe
|
||||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
from custom_ui.services import DbService, AddressService, ClientService
|
from custom_ui.services import DbService
|
||||||
|
|
||||||
def after_insert(doc, method):
|
def after_insert(doc, method):
|
||||||
print("DEBUG: After insert hook triggered for Quotation:", doc.name)
|
print("DEBUG: after_insert hook triggered for Quotation:", doc.name)
|
||||||
AddressService.append_link_v2(
|
try:
|
||||||
doc.custom_job_address,
|
template = doc.custom_project_template or "Other"
|
||||||
{"quotations": {"quotation": doc.name, "project_template": doc.custom_project_template}}
|
if template == "Other":
|
||||||
)
|
print("WARN: No project template specified.")
|
||||||
template = doc.custom_project_template or "Other"
|
if template == "SNW Install":
|
||||||
if template == "Other":
|
print("DEBUG: SNW Install template detected, updating custom address field.")
|
||||||
print("WARN: No project template specified.")
|
DbService.set_value(
|
||||||
if template == "SNW Install":
|
doctype="Address",
|
||||||
print("DEBUG: SNW Install template detected, updating custom address field.")
|
name=doc.custom_job_address,
|
||||||
AddressService.update_value(
|
fieldname="custom_estimate_sent_status",
|
||||||
doc.custom_job_address,
|
value="Pending"
|
||||||
"estimate_sent_status",
|
|
||||||
"In Progress"
|
|
||||||
)
|
|
||||||
|
|
||||||
def before_insert(doc, method):
|
|
||||||
print("DEBUG: Before insert hook triggered for Quotation:", doc.name)
|
|
||||||
# if doc.custom_project_template == "SNW Install":
|
|
||||||
# print("DEBUG: Quotation uses SNW Install template, setting initial Address status to 'In Progress'.")
|
|
||||||
# address_doc = AddressService.get_or_throw(doc.custom_job_address)
|
|
||||||
# if "SNW Install" in [link.project_template for link in address_doc.quotations]:
|
|
||||||
# raise frappe.ValidationError("An Estimate with project template 'SNW Install' is already linked to this address.")
|
|
||||||
|
|
||||||
def before_submit(doc, method):
|
|
||||||
print("DEBUG: Before submit hook triggered for Quotation:", doc.name)
|
|
||||||
if doc.custom_project_template == "SNW Install":
|
|
||||||
print("DEBUG: Quotation uses SNW Install template.")
|
|
||||||
if doc.custom_current_status == "Estimate Sent":
|
|
||||||
print("DEBUG: Current status is 'Estimate Sent', updating Address status to 'Sent'.")
|
|
||||||
AddressService.update_value(
|
|
||||||
doc.custom_job_address,
|
|
||||||
"estimate_sent_status",
|
|
||||||
"Completed"
|
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print("ERROR in after_insert hook:", str(e))
|
||||||
|
frappe.log_error(f"Error in estimate after_insert: {str(e)}", "Estimate Hook Error")
|
||||||
|
|
||||||
|
def after_save(doc, method):
|
||||||
|
print("DEBUG: after_save hook triggered for Quotation:", doc.name)
|
||||||
|
if doc.custom_sent and doc.custom_response and doc.custom_project_template == "SNW Install":
|
||||||
|
print("DEBUG: Quotation has been sent, updating Address status")
|
||||||
|
try:
|
||||||
|
DbService.set_value(
|
||||||
|
doctype="Address",
|
||||||
|
name=doc.custom_job_address,
|
||||||
|
fieldname="custom_estimate_sent_status",
|
||||||
|
value="Sent"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print("ERROR updating Address in after_save hook:", str(e))
|
||||||
|
frappe.log_error(f"Error updating Address in estimate after_save: {str(e)}", "Estimate Hook Error")
|
||||||
|
|
||||||
def on_update_after_submit(doc, method):
|
def on_update_after_submit(doc, method):
|
||||||
print("DEBUG: on_update_after_submit hook triggered for Quotation:", doc.name)
|
print("DEBUG: on_update_after_submit hook triggered for Quotation:", doc.name)
|
||||||
print("DEBUG: Current custom_current_status:", doc.custom_current_status)
|
print("DEBUG: Current custom_current_status:", doc.custom_current_status)
|
||||||
if doc.custom_current_status == "Estimate Accepted":
|
if doc.custom_current_status == "Estimate Accepted":
|
||||||
doc.custom_current_status = "Won"
|
doc.custom_current_status = "Won"
|
||||||
print("DEBUG: Quotation marked as Won, updating current status.")
|
if doc.custom_project_template == "SNW Install":
|
||||||
if doc.customer_type == "Lead":
|
DbService.set_value(
|
||||||
print("DEBUG: Customer is a Lead, converting to Customer and updating Quotation.")
|
doctype="Address",
|
||||||
new_customer = ClientService.convert_lead_to_customer(doc.customer, update_quotations=False)
|
name=doc.custom_job_address,
|
||||||
doc.customer = new_customer.name
|
fieldname="custom_estimate_sent_status",
|
||||||
doc.customer_type = "Customer"
|
value="Completed"
|
||||||
doc.save()
|
)
|
||||||
print("DEBUG: Creating Sales Order from accepted Estimate")
|
try:
|
||||||
new_sales_order = make_sales_order(doc.name)
|
print("DEBUG: Creating Sales Order from accepted Estimate")
|
||||||
new_sales_order.custom_requires_half_payment = doc.requires_half_payment
|
new_sales_order = make_sales_order(doc.name)
|
||||||
new_sales_order.customer = doc.customer
|
new_sales_order.custom_requires_half_payment = doc.requires_half_payment
|
||||||
# new_sales_order.custom_installation_address = doc.custom_installation_address
|
new_sales_order.custom_installation_address = doc.custom_installation_address
|
||||||
# new_sales_order.custom_job_address = doc.custom_job_address
|
new_sales_order.payment_schedule = []
|
||||||
new_sales_order.payment_schedule = []
|
print("DEBUG: Setting payment schedule for Sales Order")
|
||||||
print("DEBUG: Setting payment schedule for Sales Order")
|
new_sales_order.set_payment_schedule()
|
||||||
new_sales_order.set_payment_schedule()
|
print("DEBUG: Inserting Sales Order:", new_sales_order.as_dict())
|
||||||
print("DEBUG: Inserting Sales Order:", new_sales_order.as_dict())
|
new_sales_order.delivery_date = new_sales_order.transaction_date
|
||||||
new_sales_order.delivery_date = new_sales_order.transaction_date
|
new_sales_order.insert()
|
||||||
new_sales_order.insert()
|
print("DEBUG: Submitting Sales Order")
|
||||||
print("DEBUG: Submitting Sales Order")
|
new_sales_order.submit()
|
||||||
new_sales_order.submit()
|
frappe.db.commit()
|
||||||
frappe.db.commit()
|
print("DEBUG: Sales Order created successfully:", new_sales_order.name)
|
||||||
print("DEBUG: Sales Order created successfully:", new_sales_order.name)
|
except Exception as e:
|
||||||
|
print("ERROR creating Sales Order from Estimate:", str(e))
|
||||||
|
frappe.log_error(f"Error creating Sales Order from Estimate {doc.name}: {str(e)}", "Estimate on_update_after_submit Error")
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,21 @@
|
||||||
import frappe
|
import frappe
|
||||||
from custom_ui.services import DbService, AddressService, ClientService
|
|
||||||
|
|
||||||
def before_insert(doc, method):
|
|
||||||
print("DEBUG: Before Insert Triggered for On-Site Meeting")
|
|
||||||
if doc.project_template == "SNW Install":
|
|
||||||
address_doc = AddressService.get_or_throw(doc.address)
|
|
||||||
# Address.onsite_meetings is a child table with two fields: onsite_meeting (Link) and project_template (Link). Iterate through to see if there is already an SNW Install meeting linked.
|
|
||||||
for link in address_doc.onsite_meetings:
|
|
||||||
if link.project_template == "SNW Install":
|
|
||||||
raise frappe.ValidationError("An On-Site Meeting with project template 'SNW Install' is already linked to this address.")
|
|
||||||
|
|
||||||
def after_insert(doc, method):
|
def after_insert(doc, method):
|
||||||
print("DEBUG: After Insert Triggered for On-Site Meeting")
|
print("DEBUG: After Insert Triggered for On-Site Meeting")
|
||||||
print("DEBUG: Linking bid meeting to customer and address")
|
print("DEBUG: Updating on-site meeting status in Address")
|
||||||
AddressService.append_link_v2(doc.address, "onsite_meetings", {"onsite_meeting": doc.name, "project_template": doc.project_template})
|
if doc.address and not doc.end_time and not doc.start_time:
|
||||||
ClientService.append_link(doc.party_name, "onsite_meetings", "onsite_meeting", doc.name)
|
address_doc = frappe.get_doc("Address", doc.address)
|
||||||
if doc.project_template == "SNW Install":
|
address_doc.custom_onsite_meeting_scheduled = "In Progress"
|
||||||
print("DEBUG: Project template is SNW Install, updating Address status to In Progress")
|
address_doc.save()
|
||||||
AddressService.update_value(doc.address, "onsite_meeting_scheduled", "In Progress")
|
|
||||||
|
|
||||||
|
def after_save(doc, method):
|
||||||
def before_save(doc, method):
|
print("DEBUG: After Save Triggered for On-Site Meeting")
|
||||||
print("DEBUG: Before Save Triggered for On-Site Meeting")
|
if doc.status == "Completed":
|
||||||
|
print("DEBUG: Meeting marked as Completed, updating Address status")
|
||||||
|
address_doc = frappe.get_doc("Address", doc.address)
|
||||||
|
address_doc.custom_onsite_meeting_scheduled = "Completed"
|
||||||
|
address_doc.save()
|
||||||
|
return
|
||||||
if doc.status != "Scheduled" and doc.start_time and doc.end_time:
|
if doc.status != "Scheduled" and doc.start_time and doc.end_time:
|
||||||
print("DEBUG: Meeting has start and end time, setting status to Scheduled")
|
|
||||||
doc.status = "Scheduled"
|
doc.status = "Scheduled"
|
||||||
if doc.project_template == "SNW Install":
|
doc.save()
|
||||||
print("DEBUG: Project template is SNW Install")
|
|
||||||
if doc.status == "Completed":
|
|
||||||
print("DEBUG: Meeting marked as Completed, updating Address status")
|
|
||||||
current_status = AddressService.get_value(doc.address, "onsite_meeting_scheduled")
|
|
||||||
if current_status != doc.status:
|
|
||||||
AddressService.update_value(doc.address, "onsite_meeting_scheduled", "Completed")
|
|
||||||
|
|
||||||
def validate_address_link(doc, method):
|
|
||||||
print("DEBUG: Validating Address link for On-Site Meeting")
|
|
||||||
if doc.onsite_meeting:
|
|
||||||
meeting = DbService.get_or_throw("On-Site Meeting", doc.onsite_meeting)
|
|
||||||
doc.project_template = meeting.project_template
|
|
||||||
|
|
@ -161,21 +161,19 @@ add_to_apps_screen = [
|
||||||
doc_events = {
|
doc_events = {
|
||||||
"On-Site Meeting": {
|
"On-Site Meeting": {
|
||||||
"after_insert": "custom_ui.events.onsite_meeting.after_insert",
|
"after_insert": "custom_ui.events.onsite_meeting.after_insert",
|
||||||
"before_save": "custom_ui.events.onsite_meeting.before_save",
|
"on_update": "custom_ui.events.onsite_meeting.after_save"
|
||||||
"before_insert": "custom_ui.events.onsite_meeting.before_insert"
|
|
||||||
},
|
},
|
||||||
"Address": {
|
"Address": {
|
||||||
"before_insert": "custom_ui.events.address.before_insert"
|
"after_insert": "custom_ui.events.address.after_insert"
|
||||||
},
|
},
|
||||||
"Quotation": {
|
"Quotation": {
|
||||||
"before_insert": "custom_ui.events.estimate.before_insert",
|
|
||||||
"after_insert": "custom_ui.events.estimate.after_insert",
|
"after_insert": "custom_ui.events.estimate.after_insert",
|
||||||
# "before_save": "custom_ui.events.estimate.before_save",
|
"on_update": "custom_ui.events.estimate.after_save",
|
||||||
"before_submit": "custom_ui.events.estimate.before_submit",
|
"after_submit": "custom_ui.events.estimate.after_submit",
|
||||||
"on_update_after_submit": "custom_ui.events.estimate.on_update_after_submit"
|
"on_update_after_submit": "custom_ui.events.estimate.on_update_after_submit"
|
||||||
},
|
},
|
||||||
"Sales Order": {
|
"Sales Order": {
|
||||||
"on_submit": "custom_ui.events.sales_order.on_submit"
|
"on_submit": "custom_ui.events.sales_order.on_submit",
|
||||||
},
|
},
|
||||||
"Task": {
|
"Task": {
|
||||||
"before_insert": "custom_ui.events.task.before_insert"
|
"before_insert": "custom_ui.events.task.before_insert"
|
||||||
|
|
@ -191,25 +189,7 @@ fixtures = [
|
||||||
"Quotation Template Item",
|
"Quotation Template Item",
|
||||||
"Customer Company Link",
|
"Customer Company Link",
|
||||||
"Customer Address Link",
|
"Customer Address Link",
|
||||||
"Customer Contact 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",
|
|
||||||
]]
|
]]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -247,7 +227,6 @@ fixtures = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Scheduled Tasks
|
# Scheduled Tasks
|
||||||
# ---------------
|
# ---------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
from .address_service import AddressService
|
from .address_service import AddressService
|
||||||
from .contact_service import ContactService
|
|
||||||
from .db_service import DbService
|
from .db_service import DbService
|
||||||
from .client_service import ClientService
|
|
||||||
from .estimate_service import EstimateService
|
|
||||||
from .onsite_meeting_service import OnSiteMeetingService
|
|
||||||
|
|
@ -1,42 +1,7 @@
|
||||||
import frappe
|
import frappe
|
||||||
from .contact_service import ContactService, DbService
|
|
||||||
|
|
||||||
class AddressService:
|
class AddressService:
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_full_dict(
|
|
||||||
address_doc,
|
|
||||||
included_links: list = ["contacts", "on-site meetings", "quotations", "sales orders", "projects", "companies"]) -> dict:
|
|
||||||
"""Build a full dictionary representation of an address, including all links. Can optionally exclude links."""
|
|
||||||
print(f"DEBUG: Building full dict for Address {address_doc.name}")
|
|
||||||
address_dict = address_doc.as_dict()
|
|
||||||
if "contacts" in included_links:
|
|
||||||
address_dict["contacts"] = [ContactService.get_or_throw(link.contact).as_dict() for link in address_doc.contacts]
|
|
||||||
if "on-site meetings" in included_links:
|
|
||||||
address_dict["onsite_meetings"] = [DbService.get_or_throw("On-Site Meeting", link.onsite_meeting).as_dict() for link in address_doc.onsite_meetings]
|
|
||||||
if "quotations" in included_links:
|
|
||||||
address_dict["quotations"] = [DbService.get_or_throw("Quotation", link.quotation).as_dict() for link in address_doc.quotations]
|
|
||||||
if "sales orders" in included_links:
|
|
||||||
address_dict["sales_orders"] = [DbService.get_or_throw("Sales Order", link.sales_order).as_dict() for link in address_doc.sales_orders]
|
|
||||||
if "projects" in included_links:
|
|
||||||
address_dict["projects"] = [DbService.get_or_throw("Project", link.project).as_dict() for link in address_doc.projects]
|
|
||||||
if "companies" in included_links:
|
|
||||||
address_dict["companies"] = [DbService.get_or_throw("Company", link.company).as_dict() for link in address_doc.companies]
|
|
||||||
print(f"DEBUG: Built full dict for Address {address_doc.name}: {address_dict}")
|
|
||||||
return address_dict
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_address_by_full_address(full_address: str):
|
|
||||||
"""Retrieve an address document by its full_address field. Returns None if not found."""
|
|
||||||
print(f"DEBUG: Retrieving Address document with full_address: {full_address}")
|
|
||||||
address_name = frappe.db.get_value("Address", {"full_address": full_address})
|
|
||||||
if address_name:
|
|
||||||
address_doc = frappe.get_doc("Address", address_name)
|
|
||||||
print("DEBUG: Address document found.")
|
|
||||||
return address_doc
|
|
||||||
print("DEBUG: Address document not found.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def exists(address_name: str) -> bool:
|
def exists(address_name: str) -> bool:
|
||||||
"""Check if an address with the given name exists."""
|
"""Check if an address with the given name exists."""
|
||||||
|
|
@ -111,59 +76,4 @@ class AddressService:
|
||||||
address.insert(ignore_permissions=True)
|
address.insert(ignore_permissions=True)
|
||||||
print("DEBUG: Created new Address:", address.as_dict())
|
print("DEBUG: Created new Address:", address.as_dict())
|
||||||
return address
|
return address
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def link_address_to_customer(address_doc, customer_type, customer_name):
|
|
||||||
"""Link an address to a customer or lead."""
|
|
||||||
print(f"DEBUG: Linking Address {address_doc.name} to {customer_type} {customer_name}")
|
|
||||||
address_doc.customer_type = customer_type
|
|
||||||
address_doc.customer_name = customer_name
|
|
||||||
address_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Linked Address {address_doc.name} to {customer_type} {customer_name}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def link_address_to_contact(address_doc, contact_name):
|
|
||||||
"""Link an address to a contact."""
|
|
||||||
print(f"DEBUG: Linking Address {address_doc.name} to Contact {contact_name}")
|
|
||||||
address_doc.append("contacts", {
|
|
||||||
"contact": contact_name
|
|
||||||
})
|
|
||||||
address_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Linked Address {address_doc.name} to Contact {contact_name}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_address(address_data):
|
|
||||||
"""Create a new address."""
|
|
||||||
address = frappe.get_doc({
|
|
||||||
"doctype": "Address",
|
|
||||||
**address_data
|
|
||||||
})
|
|
||||||
address.insert(ignore_permissions=True)
|
|
||||||
return address
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_primary_contact(address_name: str, contact_name: str):
|
|
||||||
"""Set the primary contact for an address."""
|
|
||||||
print(f"DEBUG: Setting primary contact for Address {address_name} to Contact {contact_name}")
|
|
||||||
frappe.db.set_value("Address", address_name, "primary_contact", contact_name)
|
|
||||||
print(f"DEBUG: Set primary contact for Address {address_name} to Contact {contact_name}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def append_link(address_name: str, field: str, link_doctype: str, link_name: str):
|
|
||||||
"""Set a link field for an address."""
|
|
||||||
print(f"DEBUG: Setting link field {field} for Address {address_name} to {link_doctype} {link_name}")
|
|
||||||
address_doc = AddressService.get_or_throw(address_name)
|
|
||||||
address_doc.append(field, {
|
|
||||||
link_doctype.lower(): link_name
|
|
||||||
})
|
|
||||||
address_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Set link field {field} for Address {address_name} to {link_doctype} {link_name}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def append_link_v2(address_name: str, field: str, link: dict):
|
|
||||||
"""Set a link field for an address using a link dictionary."""
|
|
||||||
print(f"DEBUG: Setting link field {field} for Address {address_name} with link data {link}")
|
|
||||||
address_doc = AddressService.get_or_throw(address_name)
|
|
||||||
address_doc.append(field, link)
|
|
||||||
address_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}")
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import frappe
|
|
||||||
from .db_service import DbService
|
|
||||||
from erpnext.crm.doctype.lead.lead import make_customer
|
|
||||||
from .address_service import AddressService
|
|
||||||
from .contact_service import ContactService
|
|
||||||
from .estimate_service import EstimateService
|
|
||||||
from .onsite_meeting_service import OnSiteMeetingService
|
|
||||||
|
|
||||||
class ClientService:
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_client_doctype(client_name: str) -> str:
|
|
||||||
"""Determine if the client is a Customer or Lead."""
|
|
||||||
if DbService.exists("Customer", client_name):
|
|
||||||
return "Customer"
|
|
||||||
elif DbService.exists("Lead", client_name):
|
|
||||||
return "Lead"
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Client with name {client_name} does not exist as Customer or Lead.")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_primary_contact(client_name: str, contact_name: str):
|
|
||||||
"""Set the primary contact for a client (Customer or Lead)."""
|
|
||||||
print(f"DEBUG: Setting primary contact for client {client_name} to contact {contact_name}")
|
|
||||||
client_doctype = ClientService.get_client_doctype(client_name)
|
|
||||||
frappe.db.set_value(client_doctype, client_name, "primary_contact", contact_name)
|
|
||||||
print(f"DEBUG: Set primary contact for client {client_name} to contact {contact_name}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def append_link(client_name: str, field: str, link_doctype: str, link_name: str):
|
|
||||||
"""Set a link field for a client (Customer or Lead)."""
|
|
||||||
print(f"DEBUG: Setting link field {field} for client {client_name} to {link_doctype} {link_name}")
|
|
||||||
client_doctype = ClientService.get_client_doctype(client_name)
|
|
||||||
client_doc = frappe.get_doc(client_doctype, client_name)
|
|
||||||
client_doc.append(field, {
|
|
||||||
link_doctype.lower(): link_name
|
|
||||||
})
|
|
||||||
client_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Set link field {field} for client {client_doc.get('name')} to {link_doctype} {link_name}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def convert_lead_to_customer(
|
|
||||||
lead_name: str,
|
|
||||||
update_quotations: bool = True,
|
|
||||||
update_addresses: bool = True,
|
|
||||||
update_contacts: bool = True,
|
|
||||||
update_onsite_meetings: bool = True
|
|
||||||
):
|
|
||||||
"""Convert a Lead to a Customer."""
|
|
||||||
print(f"DEBUG: Converting Lead {lead_name} to Customer")
|
|
||||||
lead_doc = DbService.get_or_throw("Lead", lead_name)
|
|
||||||
customer_doc = make_customer(lead_doc.name)
|
|
||||||
customer_doc.insert(ignore_permissions=True)
|
|
||||||
if update_addresses:
|
|
||||||
for address in lead_doc.get("addresses", []):
|
|
||||||
address_doc = AddressService.get_or_throw(address.get("address"))
|
|
||||||
AddressService.link_address_to_customer(address_doc, "Customer", customer_doc.name)
|
|
||||||
if update_contacts:
|
|
||||||
for contact in lead_doc.get("contacts", []):
|
|
||||||
contact_doc = ContactService.get_or_throw(contact.get("contact"))
|
|
||||||
ContactService.link_contact_to_customer(contact_doc, "Customer", customer_doc.name)
|
|
||||||
if update_quotations:
|
|
||||||
for quotation in lead_doc.get("quotations", []):
|
|
||||||
quotation_doc = EstimateService.get_or_throw(quotation.get("quotation"))
|
|
||||||
EstimateService.link_estimate_to_customer(quotation_doc, "Customer", customer_doc.name)
|
|
||||||
if update_onsite_meetings:
|
|
||||||
for meeting in lead_doc.get("onsite_meetings", []):
|
|
||||||
meeting_doc = OnSiteMeetingService.get_or_throw(meeting.get("onsite_meeting"))
|
|
||||||
OnSiteMeetingService.link_onsite_meeting_to_customer(meeting_doc, "Customer", customer_doc.name)
|
|
||||||
print(f"DEBUG: Converted Lead {lead_name} to Customer {customer_doc.name}")
|
|
||||||
return customer_doc
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import frappe
|
|
||||||
from .db_service import DbService
|
|
||||||
|
|
||||||
class ContactService:
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create(data: dict):
|
|
||||||
"""Create a new contact."""
|
|
||||||
print("DEBUG: Creating new Contact with data:", data)
|
|
||||||
contact = frappe.get_doc({
|
|
||||||
"doctype": "Contact",
|
|
||||||
**data
|
|
||||||
})
|
|
||||||
contact.insert(ignore_permissions=True)
|
|
||||||
print("DEBUG: Created new Contact:", contact.as_dict())
|
|
||||||
return contact
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def link_contact_to_customer(contact_doc, customer_type, customer_name):
|
|
||||||
"""Link a contact to a customer or lead."""
|
|
||||||
print(f"DEBUG: Linking Contact {contact_doc.name} to {customer_type} {customer_name}")
|
|
||||||
contact_doc.customer_type = customer_type
|
|
||||||
contact_doc.customer_name = customer_name
|
|
||||||
contact_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Linked Contact {contact_doc.name} to {customer_type} {customer_name}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def link_contact_to_address(contact_doc, address_name):
|
|
||||||
"""Link an address to a contact."""
|
|
||||||
print(f"DEBUG: Linking Address {address_name} to Contact {contact_doc.name}")
|
|
||||||
contact_doc.append("addresses", {
|
|
||||||
"address": address_name
|
|
||||||
})
|
|
||||||
contact_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Linked Address {address_name} to Contact {contact_doc.name}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_or_throw(contact_name: str):
|
|
||||||
"""Retrieve a Contact document or throw an error if it does not exist."""
|
|
||||||
return DbService.get_or_throw("Contact", contact_name)
|
|
||||||
|
|
@ -84,13 +84,4 @@ class EstimateService:
|
||||||
estimate_doc.insert(ignore_permissions=True)
|
estimate_doc.insert(ignore_permissions=True)
|
||||||
print(f"DEBUG: Created Quotation document: {estimate_doc.as_dict()}")
|
print(f"DEBUG: Created Quotation document: {estimate_doc.as_dict()}")
|
||||||
return estimate_doc
|
return estimate_doc
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def link_estimate_to_customer(estimate_doc: frappe._dict, customer_type: str, customer_name: str) -> None:
|
|
||||||
"""Link a Quotation document to a client document."""
|
|
||||||
print(f"DEBUG: Linking Quotation {estimate_doc.name} to {customer_type} {customer_name}")
|
|
||||||
estimate_doc.customer_type = customer_type
|
|
||||||
estimate_doc.customer = customer_name
|
|
||||||
estimate_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Linked Quotation {estimate_doc.name} to {customer_type} {customer_name}")
|
|
||||||
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import frappe
|
|
||||||
|
|
||||||
class OnSiteMeetingService:
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def exists(onsite_meeting_name: str) -> bool:
|
|
||||||
"""Check if an OnSite Meeting document exists by name."""
|
|
||||||
result = frappe.db.exists("OnSite Meeting", onsite_meeting_name) is not None
|
|
||||||
print(f"DEBUG: OnSite Meeting existence for {onsite_meeting_name}: {result}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get(onsite_meeting_name: str) -> frappe._dict:
|
|
||||||
"""Retrieve an OnSite Meeting document by name. Returns None if not found."""
|
|
||||||
print(f"DEBUG: Retrieving OnSite Meeting document with name: {onsite_meeting_name}")
|
|
||||||
if OnSiteMeetingService.exists(onsite_meeting_name):
|
|
||||||
onsite_meeting_doc = frappe.get_doc("OnSite Meeting", onsite_meeting_name)
|
|
||||||
print("DEBUG: OnSite Meeting document found.")
|
|
||||||
return onsite_meeting_doc
|
|
||||||
print("DEBUG: OnSite Meeting document not found.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_or_throw(onsite_meeting_name: str) -> frappe._dict:
|
|
||||||
"""Retrieve an OnSite Meeting document or throw an error if not found."""
|
|
||||||
onsite_meeting_doc = OnSiteMeetingService.get(onsite_meeting_name)
|
|
||||||
if not onsite_meeting_doc:
|
|
||||||
raise ValueError(f"OnSite Meeting with name {onsite_meeting_name} does not exist.")
|
|
||||||
return onsite_meeting_doc
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def link_onsite_meeting_to_customer(onsite_meeting_doc, customer_type, customer_name):
|
|
||||||
"""Link an onsite meeting to a customer or lead."""
|
|
||||||
print(f"DEBUG: Linking Onsite Meeting {onsite_meeting_doc.name} to {customer_type} {customer_name}")
|
|
||||||
onsite_meeting_doc.party_type = customer_type
|
|
||||||
onsite_meeting_doc.party_name = customer_name
|
|
||||||
onsite_meeting_doc.save(ignore_permissions=True)
|
|
||||||
print(f"DEBUG: Linked Onsite Meeting {onsite_meeting_doc.name} to {customer_type} {customer_name}")
|
|
||||||
|
|
@ -40,7 +40,7 @@ const FRAPPE_GET_ADDRESSES_METHOD = "custom_ui.api.db.addresses.get_addresses";
|
||||||
const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client";
|
const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client";
|
||||||
const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.clients.get_client_status_counts";
|
const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.clients.get_client_status_counts";
|
||||||
const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_clients_table_data";
|
const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_clients_table_data";
|
||||||
const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2";
|
const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client";
|
||||||
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
|
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
|
|
@ -157,15 +157,10 @@ class Api {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getBidMeeting(name) {
|
static async createBidMeeting(address, notes = "") {
|
||||||
return await this.request("custom_ui.api.db.bid_meetings.get_bid_meeting", {
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async createBidMeeting(data) {
|
|
||||||
return await this.request("custom_ui.api.db.bid_meetings.create_bid_meeting", {
|
return await this.request("custom_ui.api.db.bid_meetings.create_bid_meeting", {
|
||||||
data,
|
address,
|
||||||
|
notes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,17 +196,14 @@
|
||||||
|
|
||||||
<!-- Meeting Details Modal -->
|
<!-- Meeting Details Modal -->
|
||||||
<MeetingDetailsModal
|
<MeetingDetailsModal
|
||||||
:visible="showMeetingModal"
|
v-model:visible="showMeetingModal"
|
||||||
@update:visible="showMeetingModal = $event"
|
|
||||||
:meeting="selectedMeeting"
|
:meeting="selectedMeeting"
|
||||||
@close="closeMeetingModal"
|
@close="closeMeetingModal"
|
||||||
@meeting-updated="handleMeetingUpdated"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- New Meeting Modal -->
|
<!-- New Meeting Modal -->
|
||||||
<BidMeetingModal
|
<BidMeetingModal
|
||||||
:visible="showNewMeetingModal"
|
v-model:visible="showNewMeetingModal"
|
||||||
@update:visible="showNewMeetingModal = $event"
|
|
||||||
:initial-address="queryAddress"
|
:initial-address="queryAddress"
|
||||||
@confirm="handleNewMeetingConfirm"
|
@confirm="handleNewMeetingConfirm"
|
||||||
@cancel="handleNewMeetingCancel"
|
@cancel="handleNewMeetingCancel"
|
||||||
|
|
@ -231,7 +228,6 @@ const notificationStore = useNotificationStore();
|
||||||
// Query parameters
|
// Query parameters
|
||||||
const isNewMode = computed(() => route.query.new === "true");
|
const isNewMode = computed(() => route.query.new === "true");
|
||||||
const queryAddress = computed(() => route.query.address || "");
|
const queryAddress = computed(() => route.query.address || "");
|
||||||
const queryMeetingName = computed(() => route.query.name || "");
|
|
||||||
|
|
||||||
// Date management
|
// Date management
|
||||||
const currentWeekStart = ref(new Date());
|
const currentWeekStart = ref(new Date());
|
||||||
|
|
@ -463,12 +459,6 @@ const closeMeetingModal = () => {
|
||||||
selectedMeeting.value = null;
|
selectedMeeting.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMeetingUpdated = async () => {
|
|
||||||
// Reload both scheduled and unscheduled meetings
|
|
||||||
await loadWeekMeetings();
|
|
||||||
await loadUnscheduledMeetings();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openNewMeetingModal = () => {
|
const openNewMeetingModal = () => {
|
||||||
showNewMeetingModal.value = true;
|
showNewMeetingModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
@ -480,7 +470,7 @@ const handleNewMeetingConfirm = async (meetingData) => {
|
||||||
loadingStore.setLoading(true);
|
loadingStore.setLoading(true);
|
||||||
|
|
||||||
// Create the meeting via API
|
// Create the meeting via API
|
||||||
const result = await Api.createBidMeeting(meetingData);
|
const result = await Api.createBidMeeting(meetingData.address, meetingData.notes || "");
|
||||||
|
|
||||||
showNewMeetingModal.value = false;
|
showNewMeetingModal.value = false;
|
||||||
|
|
||||||
|
|
@ -965,100 +955,6 @@ const navigateToSpecificMeeting = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findAndDisplayMeetingByName = async () => {
|
|
||||||
if (!queryMeetingName.value) return;
|
|
||||||
|
|
||||||
console.log("Searching for meeting:", queryMeetingName.value);
|
|
||||||
|
|
||||||
// First, search in the unscheduled meetings list
|
|
||||||
const unscheduledMeeting = unscheduledMeetings.value.find(
|
|
||||||
(m) => m.name === queryMeetingName.value
|
|
||||||
);
|
|
||||||
|
|
||||||
if (unscheduledMeeting) {
|
|
||||||
console.log("Found in unscheduled meetings:", unscheduledMeeting);
|
|
||||||
// Meeting is unscheduled, just show notification
|
|
||||||
notificationStore.addNotification({
|
|
||||||
type: "info",
|
|
||||||
title: "Unscheduled Meeting",
|
|
||||||
message: "This meeting has not been scheduled yet. Drag it to a time slot to schedule it.",
|
|
||||||
duration: 6000,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not in unscheduled list, fetch from API to get schedule details
|
|
||||||
try {
|
|
||||||
loadingStore.setLoading(true);
|
|
||||||
const meetingData = await Api.getBidMeeting(queryMeetingName.value);
|
|
||||||
|
|
||||||
if (!meetingData) {
|
|
||||||
notificationStore.addNotification({
|
|
||||||
type: "error",
|
|
||||||
title: "Meeting Not Found",
|
|
||||||
message: "Could not find the specified meeting.",
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if meeting is scheduled
|
|
||||||
if (!meetingData.startTime) {
|
|
||||||
notificationStore.addNotification({
|
|
||||||
type: "info",
|
|
||||||
title: "Unscheduled Meeting",
|
|
||||||
message: "This meeting has not been scheduled yet.",
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the start time to get date and time
|
|
||||||
const startDateTime = new Date(meetingData.startTime);
|
|
||||||
const meetingDate = startDateTime.toISOString().split("T")[0];
|
|
||||||
const meetingTime = `${startDateTime.getHours().toString().padStart(2, "0")}:${startDateTime.getMinutes().toString().padStart(2, "0")}`;
|
|
||||||
|
|
||||||
// Navigate to the week containing this meeting
|
|
||||||
currentWeekStart.value = new Date(
|
|
||||||
startDateTime.getFullYear(),
|
|
||||||
startDateTime.getMonth(),
|
|
||||||
startDateTime.getDate()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reload meetings for this week
|
|
||||||
await loadWeekMeetings();
|
|
||||||
|
|
||||||
// Find the meeting in the loaded meetings
|
|
||||||
const scheduledMeeting = meetings.value.find(
|
|
||||||
(m) => m.name === queryMeetingName.value
|
|
||||||
);
|
|
||||||
|
|
||||||
if (scheduledMeeting) {
|
|
||||||
// Auto-open the meeting details modal
|
|
||||||
setTimeout(() => {
|
|
||||||
showMeetingDetails(scheduledMeeting);
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
notificationStore.addNotification({
|
|
||||||
type: "warning",
|
|
||||||
title: "Meeting Found",
|
|
||||||
message: `Meeting is scheduled for ${formatDate(meetingDate)} at ${formatTimeDisplay(meetingTime)}`,
|
|
||||||
duration: 6000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching meeting:", error);
|
|
||||||
notificationStore.addNotification({
|
|
||||||
type: "error",
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to load meeting details.",
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
loadingStore.setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
initializeWeek();
|
initializeWeek();
|
||||||
|
|
@ -1071,9 +967,6 @@ onMounted(async () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
openNewMeetingModal();
|
openNewMeetingModal();
|
||||||
}, 500);
|
}, 500);
|
||||||
} else if (queryMeetingName.value) {
|
|
||||||
// Find and display specific meeting by name
|
|
||||||
await findAndDisplayMeetingByName();
|
|
||||||
} else if (queryAddress.value) {
|
} else if (queryAddress.value) {
|
||||||
// View mode with address - find and show existing meeting details
|
// View mode with address - find and show existing meeting details
|
||||||
await navigateToSpecificMeeting();
|
await navigateToSpecificMeeting();
|
||||||
|
|
|
||||||
|
|
@ -1,144 +1,76 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-header">
|
<h3>Property Address Information</h3>
|
||||||
<h3>Property Address Information</h3>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div
|
<div class="form-field full-width">
|
||||||
v-for="(address, index) in localFormData.addresses"
|
<label for="address-line1"> Address Line 1 <span class="required">*</span> </label>
|
||||||
:key="index"
|
<InputText
|
||||||
class="address-item"
|
id="address-line1"
|
||||||
>
|
v-model="localFormData.addressLine1"
|
||||||
<div class="address-header">
|
:disabled="isSubmitting"
|
||||||
<h4>Address {{ index + 1 }}</h4>
|
placeholder="Street address"
|
||||||
<Button
|
class="w-full"
|
||||||
v-if="localFormData.addresses.length > 1"
|
/>
|
||||||
@click="removeAddress(index)"
|
|
||||||
size="small"
|
|
||||||
severity="danger"
|
|
||||||
label="Delete"
|
|
||||||
class="remove-btn"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="address-fields">
|
|
||||||
<div class="form-field full-width">
|
|
||||||
<label :for="`address-line1-${index}`">
|
|
||||||
Address Line 1 <span class="required">*</span>
|
|
||||||
</label>
|
|
||||||
<InputText
|
|
||||||
:id="`address-line1-${index}`"
|
|
||||||
v-model="address.addressLine1"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
placeholder="Street address"
|
|
||||||
class="w-full"
|
|
||||||
@input="formatAddressLine(index, 'addressLine1', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-field full-width">
|
|
||||||
<label :for="`address-line2-${index}`">Address Line 2</label>
|
|
||||||
<InputText
|
|
||||||
:id="`address-line2-${index}`"
|
|
||||||
v-model="address.addressLine2"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
placeholder="Apt, suite, unit, etc."
|
|
||||||
class="w-full"
|
|
||||||
@input="formatAddressLine(index, 'addressLine2', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-field full-width checkbox-row">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:id="`isBilling-${index}`"
|
|
||||||
v-model="address.isBillingAddress"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
style="margin-top: 0"
|
|
||||||
/>
|
|
||||||
<label :for="`isBilling-${index}`">Is Billing Address</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-field">
|
|
||||||
<label :for="`zipcode-${index}`">
|
|
||||||
Zip Code <span class="required">*</span>
|
|
||||||
</label>
|
|
||||||
<InputText
|
|
||||||
:id="`zipcode-${index}`"
|
|
||||||
v-model="address.pincode"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
@input="handleZipcodeInput(index, $event)"
|
|
||||||
maxlength="5"
|
|
||||||
placeholder="12345"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label :for="`city-${index}`">
|
|
||||||
City <span class="required">*</span>
|
|
||||||
</label>
|
|
||||||
<InputText
|
|
||||||
:id="`city-${index}`"
|
|
||||||
v-model="address.city"
|
|
||||||
:disabled="isSubmitting || address.zipcodeLookupDisabled"
|
|
||||||
placeholder="City"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label :for="`state-${index}`">
|
|
||||||
State <span class="required">*</span>
|
|
||||||
</label>
|
|
||||||
<InputText
|
|
||||||
:id="`state-${index}`"
|
|
||||||
v-model="address.state"
|
|
||||||
:disabled="isSubmitting || address.zipcodeLookupDisabled"
|
|
||||||
placeholder="State"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-field">
|
|
||||||
<label :for="`contacts-${index}`">Assigned Contacts</label>
|
|
||||||
<MultiSelect
|
|
||||||
:id="`contacts-${index}`"
|
|
||||||
v-model="address.contacts"
|
|
||||||
:options="contactOptions"
|
|
||||||
optionLabel="label"
|
|
||||||
optionValue="value"
|
|
||||||
:disabled="isSubmitting || contactOptions.length === 0"
|
|
||||||
placeholder="Select contacts"
|
|
||||||
class="w-full"
|
|
||||||
display="chip"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label :for="`primaryContact-${index}`">Primary Contact</label>
|
|
||||||
<Select
|
|
||||||
:id="`primaryContact-${index}`"
|
|
||||||
v-model="address.primaryContact"
|
|
||||||
:options="contactOptions"
|
|
||||||
optionLabel="label"
|
|
||||||
optionValue="value"
|
|
||||||
:disabled="isSubmitting || contactOptions.length === 0"
|
|
||||||
placeholder="Select primary contact"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field full-width">
|
<div class="form-field full-width">
|
||||||
<Button label="Add another address" @click="addAddress" :disabled="isSubmitting" />
|
<label for="address-line2">Address Line 2</label>
|
||||||
|
<InputText
|
||||||
|
id="address-line2"
|
||||||
|
v-model="localFormData.addressLine2"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
placeholder="Apt, suite, unit, etc."
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field full-width checkbox-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="isBilling"
|
||||||
|
v-model="localFormData.isBillingAddress"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
style="margin-top: 0"
|
||||||
|
/>
|
||||||
|
<label for="isBilling">Is Billing Address</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="zipcode"> Zip Code <span class="required">*</span> </label>
|
||||||
|
<InputText
|
||||||
|
id="zipcode"
|
||||||
|
v-model="localFormData.pincode"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
@input="handleZipcodeInput"
|
||||||
|
maxlength="5"
|
||||||
|
placeholder="12345"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="city"> City <span class="required">*</span> </label>
|
||||||
|
<InputText
|
||||||
|
id="city"
|
||||||
|
v-model="localFormData.city"
|
||||||
|
:disabled="isSubmitting || zipcodeLookupDisabled"
|
||||||
|
placeholder="City"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="state"> State <span class="required">*</span> </label>
|
||||||
|
<InputText
|
||||||
|
id="state"
|
||||||
|
v-model="localFormData.state"
|
||||||
|
:disabled="isSubmitting || zipcodeLookupDisabled"
|
||||||
|
placeholder="State"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
import Select from "primevue/select";
|
|
||||||
import MultiSelect from "primevue/multiselect";
|
|
||||||
import Button from "primevue/button";
|
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||||
|
|
||||||
|
|
@ -162,92 +94,19 @@ const emit = defineEmits(["update:formData"]);
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
const localFormData = computed({
|
const localFormData = computed({
|
||||||
get: () => {
|
get: () => props.formData,
|
||||||
if (!props.formData.addresses || props.formData.addresses.length === 0) {
|
|
||||||
props.formData.addresses = [
|
|
||||||
{
|
|
||||||
addressLine1: "",
|
|
||||||
addressLine2: "",
|
|
||||||
isBillingAddress: true,
|
|
||||||
pincode: "",
|
|
||||||
city: "",
|
|
||||||
state: "",
|
|
||||||
contacts: [],
|
|
||||||
primaryContact: null,
|
|
||||||
zipcodeLookupDisabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return props.formData;
|
|
||||||
},
|
|
||||||
set: (value) => emit("update:formData", value),
|
set: (value) => emit("update:formData", value),
|
||||||
});
|
});
|
||||||
|
|
||||||
const contactOptions = computed(() => {
|
|
||||||
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return localFormData.value.contacts.map((contact, index) => ({
|
|
||||||
label: `${contact.firstName || ""} ${contact.lastName || ""}`.trim() || `Contact ${index + 1}`,
|
|
||||||
value: index,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!localFormData.value.addresses || localFormData.value.addresses.length === 0) {
|
if (localFormData.value.isBillingAddress === undefined) {
|
||||||
localFormData.value.addresses = [
|
localFormData.value.isBillingAddress = true;
|
||||||
{
|
|
||||||
addressLine1: "",
|
|
||||||
addressLine2: "",
|
|
||||||
isBillingAddress: true,
|
|
||||||
pincode: "",
|
|
||||||
city: "",
|
|
||||||
state: "",
|
|
||||||
contacts: [],
|
|
||||||
primaryContact: null,
|
|
||||||
zipcodeLookupDisabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const addAddress = () => {
|
const zipcodeLookupDisabled = ref(true);
|
||||||
localFormData.value.addresses.push({
|
|
||||||
addressLine1: "",
|
|
||||||
addressLine2: "",
|
|
||||||
isBillingAddress: false,
|
|
||||||
pincode: "",
|
|
||||||
city: "",
|
|
||||||
state: "",
|
|
||||||
contacts: [],
|
|
||||||
primaryContact: null,
|
|
||||||
zipcodeLookupDisabled: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeAddress = (index) => {
|
const handleZipcodeInput = async (event) => {
|
||||||
if (localFormData.value.addresses.length > 1) {
|
|
||||||
localFormData.value.addresses.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAddressLine = (index, field, event) => {
|
|
||||||
const value = event.target.value;
|
|
||||||
if (!value) return;
|
|
||||||
|
|
||||||
// Capitalize first letter of each word
|
|
||||||
const formatted = value
|
|
||||||
.split(' ')
|
|
||||||
.map(word => {
|
|
||||||
if (!word) return word;
|
|
||||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
||||||
})
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
localFormData.value.addresses[index][field] = formatted;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleZipcodeInput = async (index, event) => {
|
|
||||||
const input = event.target.value;
|
const input = event.target.value;
|
||||||
|
|
||||||
// Only allow digits
|
// Only allow digits
|
||||||
|
|
@ -258,13 +117,13 @@ const handleZipcodeInput = async (index, event) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
localFormData.value.addresses[index].pincode = digitsOnly;
|
localFormData.value.pincode = digitsOnly;
|
||||||
|
|
||||||
// Reset city/state if zipcode is not complete
|
// Reset city/state if zipcode is not complete
|
||||||
if (digitsOnly.length < 5 && localFormData.value.addresses[index].zipcodeLookupDisabled) {
|
if (digitsOnly.length < 5 && zipcodeLookupDisabled.value) {
|
||||||
localFormData.value.addresses[index].city = "";
|
localFormData.value.city = "";
|
||||||
localFormData.value.addresses[index].state = "";
|
localFormData.value.state = "";
|
||||||
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
|
zipcodeLookupDisabled.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch city/state when 5 digits entered
|
// Fetch city/state when 5 digits entered
|
||||||
|
|
@ -275,14 +134,14 @@ const handleZipcodeInput = async (index, event) => {
|
||||||
console.log("DEBUG: Retrieved places:", places);
|
console.log("DEBUG: Retrieved places:", places);
|
||||||
if (places && places.length > 0) {
|
if (places && places.length > 0) {
|
||||||
// Auto-populate city and state
|
// Auto-populate city and state
|
||||||
localFormData.value.addresses[index].city = places[0]["city"];
|
localFormData.value.city = places[0]["city"];
|
||||||
localFormData.value.addresses[index].state = places[0]["state"];
|
localFormData.value.state = places[0]["state"];
|
||||||
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
|
zipcodeLookupDisabled.value = true;
|
||||||
notificationStore.addSuccess(`Found: ${places[0]["city"]}, ${places[0]["state"]}`);
|
notificationStore.addSuccess(`Found: ${places[0]["city"]}, ${places[0]["state"]}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Enable manual entry if lookup fails
|
// Enable manual entry if lookup fails
|
||||||
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
|
zipcodeLookupDisabled.value = false;
|
||||||
notificationStore.addWarning(
|
notificationStore.addWarning(
|
||||||
"Could not find city/state for this zip code. Please enter manually.",
|
"Could not find city/state for this zip code. Please enter manually.",
|
||||||
);
|
);
|
||||||
|
|
@ -300,15 +159,8 @@ const handleZipcodeInput = async (index, event) => {
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.form-section h3 {
|
||||||
display: flex;
|
margin: 0 0 1rem 0;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -317,43 +169,7 @@ const handleZipcodeInput = async (index, event) => {
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
flex-wrap: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
.address-item {
|
|
||||||
border: 1px solid var(--surface-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
background: var(--surface-section);
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-header h4 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-btn {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-fields {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,11 +177,10 @@ const handleZipcodeInput = async (index, event) => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field.full-width {
|
.form-field.full-width {
|
||||||
width: 100%;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field label {
|
.form-field label {
|
||||||
|
|
@ -394,10 +209,6 @@ const handleZipcodeInput = async (index, event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.form-row {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
@change="setPrimary(index)"
|
@change="setPrimary(index)"
|
||||||
/>
|
/>
|
||||||
<label :for="`checkbox-${index}`">
|
<label :for="`checkbox-${index}`">
|
||||||
Client Primary Contact
|
Primary Contact
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -48,7 +48,6 @@
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
placeholder="Enter first name"
|
placeholder="Enter first name"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@input="formatName(index, 'firstName', $event)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
|
|
@ -61,7 +60,6 @@
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
placeholder="Enter last name"
|
placeholder="Enter last name"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@input="formatName(index, 'lastName', $event)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
|
|
@ -212,15 +210,6 @@ const setPrimary = (index) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatName = (index, field, event) => {
|
|
||||||
const value = event.target.value;
|
|
||||||
if (!value) return;
|
|
||||||
|
|
||||||
// Capitalize first letter, lowercase the rest
|
|
||||||
const formatted = value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
|
||||||
localFormData.value.contacts[index][field] = formatted;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatPhoneNumber = (value) => {
|
const formatPhoneNumber = (value) => {
|
||||||
const digits = value.replace(/\D/g, "").slice(0, 10);
|
const digits = value.replace(/\D/g, "").slice(0, 10);
|
||||||
if (digits.length <= 3) return digits;
|
if (digits.length <= 3) return digits;
|
||||||
|
|
|
||||||
|
|
@ -16,31 +16,8 @@
|
||||||
size="small"
|
size="small"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
@click="handleCreateBidMeeting"
|
|
||||||
icon=""
|
|
||||||
label="Create Bid Meeting"
|
|
||||||
size="small"
|
|
||||||
severity="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- SNW Installation Status -->
|
|
||||||
<div v-if="!isNew && !editMode" class="install-status-section">
|
|
||||||
<InstallStatus
|
|
||||||
:onsite-meeting-status="snwInstallData.onsiteMeetingStatus"
|
|
||||||
:estimate-sent-status="snwInstallData.estimateSentStatus"
|
|
||||||
:job-status="snwInstallData.jobStatus"
|
|
||||||
:payment-status="snwInstallData.paymentStatus"
|
|
||||||
:full-address="fullAddress"
|
|
||||||
:bid-meeting="snwInstallData.onsiteMeeting"
|
|
||||||
:estimate="snwInstallData.estimate"
|
|
||||||
:job="snwInstallData.job"
|
|
||||||
:payment="snwInstallData.payment"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-cards">
|
<div class="status-cards">
|
||||||
<template v-if="isNew || editMode">
|
<template v-if="isNew || editMode">
|
||||||
<ClientInformationForm
|
<ClientInformationForm
|
||||||
|
|
@ -240,7 +217,6 @@ import ClientInformationForm from "./ClientInformationForm.vue";
|
||||||
import ContactInformationForm from "./ContactInformationForm.vue";
|
import ContactInformationForm from "./ContactInformationForm.vue";
|
||||||
import AddressInformationForm from "./AddressInformationForm.vue";
|
import AddressInformationForm from "./AddressInformationForm.vue";
|
||||||
import History from "./History.vue";
|
import History from "./History.vue";
|
||||||
import InstallStatus from "../clientView/InstallStatus.vue";
|
|
||||||
import DataUtils from "../../utils";
|
import DataUtils from "../../utils";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
@ -302,7 +278,6 @@ onMounted(() => {
|
||||||
console.log("Mounted in new client mode - initialized empty form");
|
console.log("Mounted in new client mode - initialized empty form");
|
||||||
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
|
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
|
||||||
populateFormFromClientData();
|
populateFormFromClientData();
|
||||||
checkClientCompanyAssociation(props.clientData);
|
|
||||||
console.log("Mounted with existing client data - populated form");
|
console.log("Mounted with existing client data - populated form");
|
||||||
} else {
|
} else {
|
||||||
resetForm();
|
resetForm();
|
||||||
|
|
@ -318,7 +293,6 @@ watch(
|
||||||
resetForm();
|
resetForm();
|
||||||
} else if (newData && Object.keys(newData).length > 0) {
|
} else if (newData && Object.keys(newData).length > 0) {
|
||||||
populateFormFromClientData();
|
populateFormFromClientData();
|
||||||
checkClientCompanyAssociation(newData);
|
|
||||||
} else {
|
} else {
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
|
|
@ -343,16 +317,6 @@ watch(
|
||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Watch for company changes to re-check association
|
|
||||||
watch(
|
|
||||||
() => companyStore.currentCompany,
|
|
||||||
() => {
|
|
||||||
if (!props.isNew && props.clientData && Object.keys(props.clientData).length > 0) {
|
|
||||||
checkClientCompanyAssociation(props.clientData);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find the address data object that matches the selected address string
|
// Find the address data object that matches the selected address string
|
||||||
const selectedAddressData = computed(() => {
|
const selectedAddressData = computed(() => {
|
||||||
if (!props.clientData?.addresses || !props.selectedAddress) {
|
if (!props.clientData?.addresses || !props.selectedAddress) {
|
||||||
|
|
@ -383,48 +347,6 @@ const fullAddress = computed(() => {
|
||||||
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Computed data for SNW Install status
|
|
||||||
const snwInstallData = computed(() => {
|
|
||||||
if (!selectedAddressData.value) {
|
|
||||||
return {
|
|
||||||
onsiteMeetingStatus: "Not Started",
|
|
||||||
estimateSentStatus: "Not Started",
|
|
||||||
jobStatus: "Not Started",
|
|
||||||
paymentStatus: "Not Started",
|
|
||||||
bidMeeting: "",
|
|
||||||
estimate: "",
|
|
||||||
job: "",
|
|
||||||
payment: "dummy-payment-string",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const addr = selectedAddressData.value;
|
|
||||||
|
|
||||||
// Filter for SNW Install template
|
|
||||||
const snwBidMeeting = addr.onsiteMeetings?.find(
|
|
||||||
(m) => m.projectTemplate === "SNW Install"
|
|
||||||
);
|
|
||||||
const snwEstimate = addr.quotations?.find(
|
|
||||||
(q) => q.projectTemplate === "SNW Install"
|
|
||||||
);
|
|
||||||
const snwJob = addr.projects?.find(
|
|
||||||
(p) => p.projectTemplate === "SNW Install"
|
|
||||||
);
|
|
||||||
|
|
||||||
const stuff = {
|
|
||||||
onsiteMeetingStatus: addr.onsiteMeetingScheduled || "Not Started",
|
|
||||||
estimateSentStatus: addr.estimateSentStatus || "Not Started",
|
|
||||||
jobStatus: addr.jobStatus || "Not Started",
|
|
||||||
paymentStatus: addr.paymentReceivedStatus || "Not Started",
|
|
||||||
onsiteMeeting: snwBidMeeting?.onsiteMeeting || "",
|
|
||||||
estimate: snwEstimate?.quotation || "",
|
|
||||||
job: snwJob?.project || "",
|
|
||||||
payment: "dummy-payment-string",
|
|
||||||
};
|
|
||||||
console.log("DEBUG: SNW Install Data Computed:", stuff);
|
|
||||||
return stuff;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get contacts linked to the selected address
|
// Get contacts linked to the selected address
|
||||||
const contactsForAddress = computed(() => {
|
const contactsForAddress = computed(() => {
|
||||||
console.log("DEBUG: props.clientData:", props.clientData);
|
console.log("DEBUG: props.clientData:", props.clientData);
|
||||||
|
|
@ -476,8 +398,11 @@ const primaryContactEmail = computed(() => {
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
const hasCustomerName = formData.value.customerName?.trim();
|
const hasCustomerName = formData.value.customerName?.trim();
|
||||||
const hasCustomerType = formData.value.customerType?.trim();
|
const hasCustomerType = formData.value.customerType?.trim();
|
||||||
|
const hasAddressLine1 = formData.value.addressLine1?.trim();
|
||||||
|
const hasPincode = formData.value.pincode?.trim();
|
||||||
|
const hasCity = formData.value.city?.trim();
|
||||||
|
const hasState = formData.value.state?.trim();
|
||||||
const hasContacts = formData.value.contacts && formData.value.contacts.length > 0;
|
const hasContacts = formData.value.contacts && formData.value.contacts.length > 0;
|
||||||
const hasAddresses = formData.value.addresses && formData.value.addresses.length > 0;
|
|
||||||
|
|
||||||
// Check that all contacts have required fields
|
// Check that all contacts have required fields
|
||||||
const allContactsValid = formData.value.contacts?.every((contact) => {
|
const allContactsValid = formData.value.contacts?.every((contact) => {
|
||||||
|
|
@ -490,30 +415,15 @@ const isFormValid = computed(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that all addresses have required fields
|
|
||||||
const allAddressesValid = formData.value.addresses?.every((address) => {
|
|
||||||
const hasRequiredFields =
|
|
||||||
address.addressLine1?.trim() &&
|
|
||||||
address.pincode?.trim() &&
|
|
||||||
address.city?.trim() &&
|
|
||||||
address.state?.trim();
|
|
||||||
|
|
||||||
// Each address must have at least one contact selected
|
|
||||||
const hasContactsAssigned = address.contacts && address.contacts.length > 0;
|
|
||||||
|
|
||||||
// Each address must have a primary contact
|
|
||||||
const hasPrimaryContact = address.primaryContact !== null && address.primaryContact !== undefined;
|
|
||||||
|
|
||||||
return hasRequiredFields && hasContactsAssigned && hasPrimaryContact;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
hasCustomerName &&
|
hasCustomerName &&
|
||||||
hasCustomerType &&
|
hasCustomerType &&
|
||||||
|
hasAddressLine1 &&
|
||||||
|
hasPincode &&
|
||||||
|
hasCity &&
|
||||||
|
hasState &&
|
||||||
hasContacts &&
|
hasContacts &&
|
||||||
allContactsValid &&
|
allContactsValid
|
||||||
hasAddresses &&
|
|
||||||
allAddressesValid
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -613,24 +523,6 @@ const populateFormFromClientData = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkClientCompanyAssociation = (clientData) => {
|
|
||||||
if (!clientData || !companyStore.currentCompany) return;
|
|
||||||
|
|
||||||
// Check if client has companies array
|
|
||||||
if (!clientData.companies || !Array.isArray(clientData.companies)) return;
|
|
||||||
|
|
||||||
// Check if current company is in the client's companies list
|
|
||||||
const isAssociated = clientData.companies.some(
|
|
||||||
(companyObj) => companyObj.company === companyStore.currentCompany
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isAssociated) {
|
|
||||||
notificationStore.addWarning(
|
|
||||||
`Warning: This client is not associated with the currently selected company (${companyStore.currentCompany}).`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
const handleNewClientToggle = (isNewClient) => {
|
const handleNewClientToggle = (isNewClient) => {
|
||||||
isNewClientMode.value = isNewClient;
|
isNewClientMode.value = isNewClient;
|
||||||
|
|
@ -661,12 +553,6 @@ const handleCreateEstimate = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateBidMeeting = () => {
|
|
||||||
if (props.selectedAddress) {
|
|
||||||
router.push(`/calendar?tab=bids&new=true&address=${encodeURIComponent(props.selectedAddress)}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Edit mode methods
|
// Edit mode methods
|
||||||
const toggleEditMode = () => {
|
const toggleEditMode = () => {
|
||||||
showEditConfirmDialog.value = true;
|
showEditConfirmDialog.value = true;
|
||||||
|
|
@ -693,8 +579,13 @@ const handleSave = async () => {
|
||||||
customerName: formData.value.customerName,
|
customerName: formData.value.customerName,
|
||||||
customerType: formData.value.customerType,
|
customerType: formData.value.customerType,
|
||||||
companyName: companyStore.currentCompany,
|
companyName: companyStore.currentCompany,
|
||||||
|
addressTitle: formData.value.addressTitle,
|
||||||
|
addressLine1: formData.value.addressLine1,
|
||||||
|
addressLine2: formData.value.addressLine2,
|
||||||
|
pincode: formData.value.pincode,
|
||||||
|
city: formData.value.city,
|
||||||
|
state: formData.value.state,
|
||||||
contacts: formData.value.contacts,
|
contacts: formData.value.contacts,
|
||||||
addresses: formData.value.addresses,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Upserting client with data:", clientData);
|
console.log("Upserting client with data:", clientData);
|
||||||
|
|
@ -702,14 +593,13 @@ const handleSave = async () => {
|
||||||
// Call the upsert API
|
// Call the upsert API
|
||||||
const result = await Api.createClient(clientData);
|
const result = await Api.createClient(clientData);
|
||||||
|
|
||||||
// Calculate full address for redirect (use first address)
|
// Calculate full address for redirect
|
||||||
const firstAddress = formData.value.addresses[0];
|
const fullAddressParts = [formData.value.addressLine1];
|
||||||
const fullAddressParts = [firstAddress.addressLine1];
|
if (formData.value.addressLine2?.trim()) {
|
||||||
if (firstAddress.addressLine2?.trim()) {
|
fullAddressParts.push(formData.value.addressLine2);
|
||||||
fullAddressParts.push(firstAddress.addressLine2);
|
|
||||||
}
|
}
|
||||||
fullAddressParts.push(`${firstAddress.city}, ${firstAddress.state}`);
|
fullAddressParts.push(`${formData.value.city}, ${formData.value.state}`);
|
||||||
fullAddressParts.push(firstAddress.pincode);
|
fullAddressParts.push(formData.value.pincode);
|
||||||
const fullAddress = fullAddressParts.join(" ");
|
const fullAddress = fullAddressParts.join(" ");
|
||||||
|
|
||||||
if (props.isNew) {
|
if (props.isNew) {
|
||||||
|
|
@ -767,12 +657,6 @@ const handleCancel = () => {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-status-section {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card,
|
.info-card,
|
||||||
.map-card {
|
.map-card {
|
||||||
background: var(--surface-card);
|
background: var(--surface-card);
|
||||||
|
|
|
||||||
|
|
@ -1,245 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="install-status-card">
|
|
||||||
<h4>SNW Install</h4>
|
|
||||||
<div class="status-items">
|
|
||||||
<div
|
|
||||||
class="status-item"
|
|
||||||
:class="getStatusClass(onsiteMeetingStatus)"
|
|
||||||
@click="handleBidMeetingClick"
|
|
||||||
>
|
|
||||||
<span class="status-label">Meeting</span>
|
|
||||||
<span class="status-badge">{{ onsiteMeetingStatus }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="status-item"
|
|
||||||
:class="getStatusClass(estimateSentStatus)"
|
|
||||||
@click="handleEstimateClick"
|
|
||||||
>
|
|
||||||
<span class="status-label">Estimate</span>
|
|
||||||
<span class="status-badge">{{ estimateSentStatus }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="status-item"
|
|
||||||
:class="getStatusClass(jobStatus)"
|
|
||||||
@click="handleJobClick"
|
|
||||||
>
|
|
||||||
<span class="status-label">Job</span>
|
|
||||||
<span class="status-badge">{{ jobStatus }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="status-item"
|
|
||||||
:class="getStatusClass(paymentStatus)"
|
|
||||||
@click="handlePaymentClick"
|
|
||||||
>
|
|
||||||
<span class="status-label">Payment</span>
|
|
||||||
<span class="status-badge">{{ paymentStatus }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
onsiteMeetingStatus: {
|
|
||||||
type: String,
|
|
||||||
default: "Not Started",
|
|
||||||
},
|
|
||||||
estimateSentStatus: {
|
|
||||||
type: String,
|
|
||||||
default: "Not Started",
|
|
||||||
},
|
|
||||||
jobStatus: {
|
|
||||||
type: String,
|
|
||||||
default: "Not Started",
|
|
||||||
},
|
|
||||||
paymentStatus: {
|
|
||||||
type: String,
|
|
||||||
default: "Not Started",
|
|
||||||
},
|
|
||||||
fullAddress: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
bidMeeting: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
estimate: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
job: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
payment: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const notificationStore = useNotificationStore();
|
|
||||||
|
|
||||||
const getStatusClass = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case "Not Started":
|
|
||||||
return "status-not-started";
|
|
||||||
case "In Progress":
|
|
||||||
return "status-in-progress";
|
|
||||||
case "Completed":
|
|
||||||
return "status-completed";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBidMeetingClick = () => {
|
|
||||||
if (props.onsiteMeetingStatus === "Not Started") {
|
|
||||||
router.push(`/calendar?tab=bid&new=true&address=${encodeURIComponent(props.fullAddress)}&template=SNW%20Install`);
|
|
||||||
} else {
|
|
||||||
router.push(`/calendar?tab=bid&name=${encodeURIComponent(props.bidMeeting)}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEstimateClick = () => {
|
|
||||||
if (props.estimateSentStatus === "Not Started") {
|
|
||||||
router.push(`/estimate?new=true&address=${encodeURIComponent(props.fullAddress)}`);
|
|
||||||
} else {
|
|
||||||
router.push(`/estimate?name=${encodeURIComponent(props.estimate)}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleJobClick = () => {
|
|
||||||
if (props.jobStatus === "Not Started") {
|
|
||||||
notificationStore.addWarning(
|
|
||||||
"The job will be created automatically once a quotation has been accepted by the customer."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
router.push(`/job?name=${encodeURIComponent(props.job)}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePaymentClick = () => {
|
|
||||||
if (props.paymentStatus === "Not Started") {
|
|
||||||
notificationStore.addWarning(
|
|
||||||
"An Invoice will be automatically created once the job has been completed."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
notificationStore.addWarning("Page coming soon.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.install-status-card {
|
|
||||||
background: var(--surface-card);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: 1px solid var(--surface-border);
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
||||||
min-width: 240px;
|
|
||||||
max-width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-status-card h4 {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-items {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.4rem 0.6rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-item:hover {
|
|
||||||
transform: translateX(2px);
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.15rem 0.5rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status color variants */
|
|
||||||
.status-not-started {
|
|
||||||
background: rgba(239, 68, 68, 0.08);
|
|
||||||
border-color: rgba(239, 68, 68, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-not-started .status-badge {
|
|
||||||
background: #ef4444;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-not-started:hover {
|
|
||||||
background: rgba(239, 68, 68, 0.12);
|
|
||||||
border-color: rgba(239, 68, 68, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-in-progress {
|
|
||||||
background: rgba(59, 130, 246, 0.08);
|
|
||||||
border-color: rgba(59, 130, 246, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-in-progress .status-badge {
|
|
||||||
background: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-in-progress:hover {
|
|
||||||
background: rgba(59, 130, 246, 0.12);
|
|
||||||
border-color: rgba(59, 130, 246, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-completed {
|
|
||||||
background: rgba(34, 197, 94, 0.08);
|
|
||||||
border-color: rgba(34, 197, 94, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-completed .status-badge {
|
|
||||||
background: #22c55e;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-completed:hover {
|
|
||||||
background: rgba(34, 197, 94, 0.12);
|
|
||||||
border-color: rgba(34, 197, 94, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.install-status-card {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,130 +1,83 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- New Meeting Creation Modal -->
|
||||||
<!-- New Meeting Creation Modal -->
|
<Modal
|
||||||
<Modal
|
v-model:visible="showModal"
|
||||||
:visible="showModal"
|
:options="modalOptions"
|
||||||
@update:visible="showModal = $event"
|
@confirm="handleConfirm"
|
||||||
:options="modalOptions"
|
@cancel="handleCancel"
|
||||||
@confirm="handleConfirm"
|
>
|
||||||
@cancel="handleCancel"
|
<template #title>Schedule New Bid Meeting</template>
|
||||||
>
|
<div class="new-meeting-form">
|
||||||
<template #title>Schedule New Bid Meeting</template>
|
<div class="form-group">
|
||||||
<div class="new-meeting-form">
|
<label for="meeting-address">Address: <span class="required">*</span></label>
|
||||||
<div class="form-group">
|
<div class="address-input-group">
|
||||||
<label for="meeting-address">Address: <span class="required">*</span></label>
|
<InputText
|
||||||
<div class="address-input-group">
|
id="meeting-address"
|
||||||
<InputText
|
v-model="formData.address"
|
||||||
id="meeting-address"
|
class="address-input"
|
||||||
v-model="formData.address"
|
placeholder="Enter meeting address"
|
||||||
class="address-input"
|
@input="validateForm"
|
||||||
placeholder="Enter meeting address"
|
|
||||||
@input="validateForm"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Search"
|
|
||||||
icon="pi pi-search"
|
|
||||||
size="small"
|
|
||||||
:disabled="!formData.address.trim()"
|
|
||||||
@click="searchAddress"
|
|
||||||
class="search-btn"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="meeting-contact">Contact: <span class="required">*</span></label>
|
|
||||||
<Select
|
|
||||||
id="meeting-contact"
|
|
||||||
v-model="formData.contact"
|
|
||||||
:options="availableContacts"
|
|
||||||
optionLabel="label"
|
|
||||||
optionValue="value"
|
|
||||||
:disabled="!formData.addressName || availableContacts.length === 0"
|
|
||||||
placeholder="Select a contact"
|
|
||||||
class="w-full"
|
|
||||||
@change="validateForm"
|
|
||||||
>
|
|
||||||
<template #option="slotProps">
|
|
||||||
<div class="contact-option">
|
|
||||||
<div class="contact-name">{{ slotProps.option.displayName }}</div>
|
|
||||||
<div class="contact-details">
|
|
||||||
<span v-if="slotProps.option.role" class="contact-role">{{ slotProps.option.role }}</span>
|
|
||||||
<span v-if="slotProps.option.email" class="contact-email">{{ slotProps.option.email }}</span>
|
|
||||||
<span v-if="slotProps.option.phone" class="contact-phone">{{ slotProps.option.phone }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="meeting-project-template">Project Template (Optional):</label>
|
|
||||||
<Select
|
|
||||||
id="meeting-project-template"
|
|
||||||
v-model="formData.projectTemplate"
|
|
||||||
:options="availableProjectTemplates"
|
|
||||||
optionLabel="label"
|
|
||||||
optionValue="value"
|
|
||||||
placeholder="Select a project template"
|
|
||||||
class="w-full"
|
|
||||||
showClear
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<Button
|
||||||
<div class="form-group">
|
label="Search"
|
||||||
<label for="meeting-notes">Notes (Optional):</label>
|
icon="pi pi-search"
|
||||||
<Textarea
|
size="small"
|
||||||
id="meeting-notes"
|
:disabled="!formData.address.trim()"
|
||||||
v-model="formData.notes"
|
@click="searchAddress"
|
||||||
class="w-full"
|
class="search-btn"
|
||||||
placeholder="Additional notes..."
|
|
||||||
rows="3"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
<div class="form-group">
|
||||||
|
<label for="meeting-notes">Notes (Optional):</label>
|
||||||
|
<Textarea
|
||||||
|
id="meeting-notes"
|
||||||
|
v-model="formData.notes"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Additional notes..."
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<!-- Address Search Results Modal -->
|
<!-- Address Search Results Modal -->
|
||||||
<Modal
|
<Modal
|
||||||
:visible="showAddressSearchModal"
|
v-model:visible="showAddressSearchModal"
|
||||||
@update:visible="showAddressSearchModal = $event"
|
:options="searchModalOptions"
|
||||||
:options="searchModalOptions"
|
@confirm="closeAddressSearch"
|
||||||
@confirm="closeAddressSearch"
|
>
|
||||||
>
|
<template #title>Address Search Results</template>
|
||||||
<template #title>Address Search Results</template>
|
<div class="address-search-results">
|
||||||
<div class="address-search-results">
|
<div v-if="addressSearchResults.length === 0" class="no-results">
|
||||||
<div v-if="addressSearchResults.length === 0" class="no-results">
|
<i class="pi pi-info-circle"></i>
|
||||||
<i class="pi pi-info-circle"></i>
|
<p>No addresses found matching your search.</p>
|
||||||
<p>No addresses found matching your search.</p>
|
</div>
|
||||||
</div>
|
<div v-else class="results-list">
|
||||||
<div v-else class="results-list">
|
<div
|
||||||
<div
|
v-for="(address, index) in addressSearchResults"
|
||||||
v-for="(address, index) in addressSearchResults"
|
:key="index"
|
||||||
:key="index"
|
class="address-result-item"
|
||||||
class="address-result-item"
|
@click="selectAddress(address)"
|
||||||
@click="selectAddress(address)"
|
>
|
||||||
>
|
<i class="pi pi-map-marker"></i>
|
||||||
<i class="pi pi-map-marker"></i>
|
<span>{{ address }}</span>
|
||||||
<span>{{ typeof address === 'string' ? address : (address.fullAddress || address.name) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from "vue";
|
import { ref, computed, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
import Modal from "../common/Modal.vue";
|
import Modal from "../common/Modal.vue";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
import Textarea from "primevue/textarea";
|
import Textarea from "primevue/textarea";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import Select from "primevue/select";
|
|
||||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||||
import { useCompanyStore } from "../../stores/company";
|
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
|
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
const companyStore = useCompanyStore();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -153,17 +106,11 @@ const showModal = computed({
|
||||||
|
|
||||||
const showAddressSearchModal = ref(false);
|
const showAddressSearchModal = ref(false);
|
||||||
const addressSearchResults = ref([]);
|
const addressSearchResults = ref([]);
|
||||||
const availableContacts = ref([]);
|
|
||||||
const availableProjectTemplates = ref([]);
|
|
||||||
const selectedAddressDetails = ref(null);
|
|
||||||
const isFormValid = ref(false);
|
const isFormValid = ref(false);
|
||||||
|
|
||||||
// Form data
|
// Form data
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
address: "",
|
address: "",
|
||||||
addressName: "",
|
|
||||||
contact: "",
|
|
||||||
projectTemplate: "",
|
|
||||||
notes: "",
|
notes: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -193,9 +140,7 @@ const searchModalOptions = computed(() => ({
|
||||||
// Methods
|
// Methods
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const hasValidAddress = formData.value.address && formData.value.address.trim().length > 0;
|
const hasValidAddress = formData.value.address && formData.value.address.trim().length > 0;
|
||||||
const hasValidAddressName = formData.value.addressName && formData.value.addressName.trim().length > 0;
|
isFormValid.value = hasValidAddress;
|
||||||
const hasValidContact = formData.value.contact && formData.value.contact.trim().length > 0;
|
|
||||||
isFormValid.value = hasValidAddress && hasValidAddressName && hasValidContact;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchAddress = async () => {
|
const searchAddress = async () => {
|
||||||
|
|
@ -206,7 +151,8 @@ const searchAddress = async () => {
|
||||||
const results = await Api.searchAddresses(searchTerm);
|
const results = await Api.searchAddresses(searchTerm);
|
||||||
console.info("Address search results:", results);
|
console.info("Address search results:", results);
|
||||||
|
|
||||||
// Store full address objects instead of just strings
|
// Ensure results is always an array
|
||||||
|
// const safeResults = Array.isArray(results) ? results : [];
|
||||||
addressSearchResults.value = results;
|
addressSearchResults.value = results;
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
|
|
@ -221,125 +167,33 @@ const searchAddress = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAddress = async (addressData) => {
|
const selectAddress = (address) => {
|
||||||
// Get the address string for the API call
|
formData.value.address = address;
|
||||||
const addressString = typeof addressData === 'string' ? addressData : (addressData.fullAddress || addressData.name);
|
|
||||||
|
|
||||||
// Set the display address immediately
|
|
||||||
formData.value.address = addressString;
|
|
||||||
showAddressSearchModal.value = false;
|
showAddressSearchModal.value = false;
|
||||||
|
validateForm();
|
||||||
try {
|
|
||||||
// Fetch the full address details with contacts
|
|
||||||
const fullAddressDetails = await Api.getAddressByFullAddress(addressString);
|
|
||||||
console.info("Fetched address details:", fullAddressDetails);
|
|
||||||
|
|
||||||
// Store the fetched address details
|
|
||||||
selectedAddressDetails.value = fullAddressDetails;
|
|
||||||
|
|
||||||
// Set the address name for the API request
|
|
||||||
formData.value.addressName = fullAddressDetails.name;
|
|
||||||
|
|
||||||
// Populate contacts from the fetched address
|
|
||||||
if (fullAddressDetails.contacts && Array.isArray(fullAddressDetails.contacts)) {
|
|
||||||
availableContacts.value = fullAddressDetails.contacts.map(contact => ({
|
|
||||||
label: contact.fullName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
||||||
value: contact.name,
|
|
||||||
displayName: contact.fullName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
||||||
role: contact.role || contact.designation || '',
|
|
||||||
email: contact.email || contact.emailId || '',
|
|
||||||
phone: contact.phone || contact.mobileNo || ''
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Auto-select primary contact if available, otherwise first contact if only one
|
|
||||||
if (fullAddressDetails.primaryContact) {
|
|
||||||
formData.value.contact = fullAddressDetails.primaryContact;
|
|
||||||
} else if (availableContacts.value.length === 1) {
|
|
||||||
formData.value.contact = availableContacts.value[0].value;
|
|
||||||
} else {
|
|
||||||
formData.value.contact = "";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
availableContacts.value = [];
|
|
||||||
formData.value.contact = "";
|
|
||||||
notificationStore.addWarning("No contacts found for this address.");
|
|
||||||
}
|
|
||||||
|
|
||||||
validateForm();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching address details:", error);
|
|
||||||
notificationStore.addError("Failed to fetch address details. Please try again.");
|
|
||||||
|
|
||||||
// Reset on error
|
|
||||||
formData.value.addressName = "";
|
|
||||||
availableContacts.value = [];
|
|
||||||
formData.value.contact = "";
|
|
||||||
selectedAddressDetails.value = null;
|
|
||||||
validateForm();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeAddressSearch = () => {
|
const closeAddressSearch = () => {
|
||||||
showAddressSearchModal.value = false;
|
showAddressSearchModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchProjectTemplates = async () => {
|
|
||||||
try {
|
|
||||||
const company = companyStore.currentCompany;
|
|
||||||
if (!company) {
|
|
||||||
console.warn("No company selected, cannot fetch project templates");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templates = await Api.getJobTemplates(company);
|
|
||||||
console.info("Fetched project templates:", templates);
|
|
||||||
|
|
||||||
if (templates && Array.isArray(templates)) {
|
|
||||||
availableProjectTemplates.value = templates.map(template => ({
|
|
||||||
label: template.name,
|
|
||||||
value: template.name
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
availableProjectTemplates.value = [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching project templates:", error);
|
|
||||||
availableProjectTemplates.value = [];
|
|
||||||
notificationStore.addWarning("Failed to load project templates.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
if (!isFormValid.value) return;
|
if (!isFormValid.value) return;
|
||||||
|
|
||||||
// Send only the necessary data (addressName and contact, not full address)
|
emit("confirm", { ...formData.value });
|
||||||
const confirmData = {
|
|
||||||
address: formData.value.addressName,
|
|
||||||
contact: formData.value.contact,
|
|
||||||
projectTemplate: formData.value.projectTemplate || null,
|
|
||||||
notes: formData.value.notes,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("BidMeetingModal - Emitting confirm with data:", confirmData);
|
|
||||||
|
|
||||||
emit("confirm", confirmData);
|
|
||||||
resetForm();
|
resetForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
showModal.value = false;
|
emit("cancel");
|
||||||
resetForm();
|
resetForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
address: props.initialAddress || "",
|
address: props.initialAddress || "",
|
||||||
addressName: "",
|
|
||||||
contact: "",
|
|
||||||
projectTemplate: "",
|
|
||||||
notes: "",
|
notes: "",
|
||||||
};
|
};
|
||||||
availableContacts.value = [];
|
|
||||||
validateForm();
|
validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -353,66 +207,11 @@ watch(
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => companyStore.currentCompany,
|
|
||||||
async (newCompany) => {
|
|
||||||
if (newCompany && props.visible) {
|
|
||||||
await fetchProjectTemplates();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (isVisible) => {
|
(isVisible) => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
resetForm();
|
resetForm();
|
||||||
|
|
||||||
// Fetch project templates
|
|
||||||
await fetchProjectTemplates();
|
|
||||||
|
|
||||||
// Auto-select template from query parameter if provided
|
|
||||||
if (route.query.template) {
|
|
||||||
const templateName = decodeURIComponent(route.query.template);
|
|
||||||
const templateExists = availableProjectTemplates.value.some(
|
|
||||||
t => t.value === templateName
|
|
||||||
);
|
|
||||||
if (templateExists) {
|
|
||||||
formData.value.projectTemplate = templateName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's an initial address, automatically search and fetch it
|
|
||||||
if (formData.value.address && formData.value.address.trim()) {
|
|
||||||
try {
|
|
||||||
const results = await Api.searchAddresses(formData.value.address.trim());
|
|
||||||
console.info("Auto-search results for initial address:", results);
|
|
||||||
|
|
||||||
if (results.length === 1) {
|
|
||||||
// Auto-select if only one result
|
|
||||||
await selectAddress(results[0]);
|
|
||||||
} else if (results.length > 1) {
|
|
||||||
// Try to find exact match
|
|
||||||
const exactMatch = results.find(addr => {
|
|
||||||
const addrString = typeof addr === 'string' ? addr : (addr.fullAddress || addr.name);
|
|
||||||
return addrString === formData.value.address;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exactMatch) {
|
|
||||||
await selectAddress(exactMatch);
|
|
||||||
} else {
|
|
||||||
// Show search results if multiple matches
|
|
||||||
addressSearchResults.value = results;
|
|
||||||
showAddressSearchModal.value = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notificationStore.addWarning("No addresses found for the provided address.");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error auto-searching address:", error);
|
|
||||||
notificationStore.addError("Failed to load address details.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -509,42 +308,4 @@ validateForm();
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-name {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-details {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-role {
|
|
||||||
color: #2196f3;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-email,
|
|
||||||
.contact-phone {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-email::before {
|
|
||||||
content: "📧 ";
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-phone::before {
|
|
||||||
content: "📞 ";
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,129 +1,42 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal :visible="showModal" @update:visible="showModal = $event" :options="modalOptions" @confirm="handleClose">
|
<Modal v-model:visible="showModal" :options="modalOptions" @confirm="handleClose">
|
||||||
<template #title>Meeting Details</template>
|
<template #title>Meeting Details</template>
|
||||||
<div v-if="meeting" class="meeting-details">
|
<div v-if="meeting" class="meeting-details">
|
||||||
<!-- Meeting ID -->
|
|
||||||
<div class="detail-row">
|
|
||||||
<v-icon class="mr-2">mdi-identifier</v-icon>
|
|
||||||
<strong>Meeting ID:</strong>
|
|
||||||
<span class="detail-value">{{ meeting.name }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Address -->
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<v-icon class="mr-2">mdi-map-marker</v-icon>
|
<v-icon class="mr-2">mdi-map-marker</v-icon>
|
||||||
<strong>Address:</strong>
|
<strong>Addresss:</strong> {{ meeting.address.fullAddress }}
|
||||||
<span class="detail-value">{{ meeting.address?.fullAddress || meeting.address }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-row" v-if="meeting.client">
|
||||||
<!-- Contact -->
|
|
||||||
<div class="detail-row" v-if="meeting.contact">
|
|
||||||
<v-icon class="mr-2">mdi-account</v-icon>
|
<v-icon class="mr-2">mdi-account</v-icon>
|
||||||
<strong>Contact:</strong>
|
<strong>Client:</strong> {{ meeting.client }}
|
||||||
<span class="detail-value">{{ meeting.contact }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Party Name (Customer) -->
|
|
||||||
<div class="detail-row" v-if="meeting.partyName">
|
|
||||||
<v-icon class="mr-2">mdi-account-group</v-icon>
|
|
||||||
<strong>Customer:</strong>
|
|
||||||
<span class="detail-value">{{ meeting.partyName }} ({{ meeting.partyType }})</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Project Template -->
|
|
||||||
<div class="detail-row" v-if="meeting.projectTemplate">
|
|
||||||
<v-icon class="mr-2">mdi-folder-outline</v-icon>
|
|
||||||
<strong>Template:</strong>
|
|
||||||
<span class="detail-value">{{ meeting.projectTemplate }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scheduled Time -->
|
|
||||||
<div class="detail-row" v-if="meeting.startTime">
|
|
||||||
<v-icon class="mr-2">mdi-calendar-clock</v-icon>
|
|
||||||
<strong>Scheduled:</strong>
|
|
||||||
<span class="detail-value">{{ formatDateTime(meeting.startTime) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Duration -->
|
|
||||||
<div class="detail-row" v-if="meeting.startTime && meeting.endTime">
|
|
||||||
<v-icon class="mr-2">mdi-timer</v-icon>
|
|
||||||
<strong>Duration:</strong>
|
|
||||||
<span class="detail-value">{{ calculateDuration(meeting.startTime, meeting.endTime) }} minutes</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status -->
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
<v-icon class="mr-2">mdi-calendar</v-icon>
|
||||||
<strong>Status:</strong>
|
<strong>Date:</strong> {{ formatDate(meeting.date) }}
|
||||||
<v-chip size="small" :color="getStatusColor(meeting.status)">
|
|
||||||
{{ meeting.status }}
|
|
||||||
</v-chip>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
<!-- Assigned Employee -->
|
<v-icon class="mr-2">mdi-clock</v-icon>
|
||||||
<div class="detail-row" v-if="meeting.assignedEmployee">
|
<strong>Time:</strong> {{ formatTimeDisplay(meeting.scheduledTime) }}
|
||||||
<v-icon class="mr-2">mdi-account-tie</v-icon>
|
|
||||||
<strong>Assigned To:</strong>
|
|
||||||
<span class="detail-value">{{ meeting.assignedEmployee }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-row" v-if="meeting.duration">
|
||||||
<!-- Completed By -->
|
<v-icon class="mr-2">mdi-timer</v-icon>
|
||||||
<div class="detail-row" v-if="meeting.completedBy">
|
<strong>Duration:</strong> {{ meeting.duration }} minutes
|
||||||
<v-icon class="mr-2">mdi-account-check</v-icon>
|
|
||||||
<strong>Completed By:</strong>
|
|
||||||
<span class="detail-value">{{ meeting.completedBy }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Company -->
|
|
||||||
<div class="detail-row" v-if="meeting.company">
|
|
||||||
<v-icon class="mr-2">mdi-domain</v-icon>
|
|
||||||
<strong>Company:</strong>
|
|
||||||
<span class="detail-value">{{ meeting.company }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notes -->
|
|
||||||
<div class="detail-row" v-if="meeting.notes">
|
<div class="detail-row" v-if="meeting.notes">
|
||||||
<v-icon class="mr-2">mdi-note-text</v-icon>
|
<v-icon class="mr-2">mdi-note-text</v-icon>
|
||||||
<strong>Notes:</strong>
|
<strong>Notes:</strong> {{ meeting.notes }}
|
||||||
<span class="detail-value">{{ meeting.notes }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-row" v-if="meeting.status">
|
||||||
<!-- Action Buttons -->
|
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
||||||
<div class="action-buttons">
|
<strong>Status:</strong> {{ meeting.status }}
|
||||||
<v-btn
|
|
||||||
v-if="meeting.status !== 'Completed'"
|
|
||||||
@click="handleMarkComplete"
|
|
||||||
color="success"
|
|
||||||
variant="elevated"
|
|
||||||
:loading="isUpdating"
|
|
||||||
>
|
|
||||||
<v-icon left>mdi-check</v-icon>
|
|
||||||
Mark as Completed
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
v-if="meeting.status === 'Completed'"
|
|
||||||
@click="handleCreateEstimate"
|
|
||||||
color="primary"
|
|
||||||
variant="elevated"
|
|
||||||
>
|
|
||||||
<v-icon left>mdi-file-document-outline</v-icon>
|
|
||||||
Create Estimate
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import Modal from "../common/Modal.vue";
|
import Modal from "../common/Modal.vue";
|
||||||
import Api from "../../api";
|
|
||||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const notificationStore = useNotificationStore();
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -138,11 +51,9 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(["update:visible", "close", "meetingUpdated"]);
|
const emit = defineEmits(["update:visible", "close"]);
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const isUpdating = ref(false);
|
|
||||||
|
|
||||||
const showModal = computed({
|
const showModal = computed({
|
||||||
get() {
|
get() {
|
||||||
return props.visible;
|
return props.visible;
|
||||||
|
|
@ -154,7 +65,7 @@ const showModal = computed({
|
||||||
|
|
||||||
// Modal options
|
// Modal options
|
||||||
const modalOptions = computed(() => ({
|
const modalOptions = computed(() => ({
|
||||||
maxWidth: "700px",
|
maxWidth: "600px",
|
||||||
showCancelButton: false,
|
showCancelButton: false,
|
||||||
confirmButtonText: "Close",
|
confirmButtonText: "Close",
|
||||||
confirmButtonColor: "primary",
|
confirmButtonColor: "primary",
|
||||||
|
|
@ -165,89 +76,6 @@ const handleClose = () => {
|
||||||
emit("close");
|
emit("close");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkComplete = async () => {
|
|
||||||
if (!props.meeting?.name) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isUpdating.value = true;
|
|
||||||
|
|
||||||
await Api.updateBidMeeting(props.meeting.name, {
|
|
||||||
status: "Completed",
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationStore.addNotification({
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
isUpdating.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateEstimate = () => {
|
|
||||||
if (!props.meeting) return;
|
|
||||||
|
|
||||||
const addressText = props.meeting.address?.fullAddress || props.meeting.address || "";
|
|
||||||
const template = props.meeting.projectTemplate || "";
|
|
||||||
const fromMeeting = props.meeting.name || "";
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
path: "/estimate",
|
|
||||||
query: {
|
|
||||||
new: "true",
|
|
||||||
address: addressText,
|
|
||||||
template: template,
|
|
||||||
from: fromMeeting,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateTime = (dateTimeStr) => {
|
|
||||||
if (!dateTimeStr) return "";
|
|
||||||
const date = new Date(dateTimeStr);
|
|
||||||
return date.toLocaleString("en-US", {
|
|
||||||
weekday: "short",
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
hour12: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateDuration = (startTime, endTime) => {
|
|
||||||
if (!startTime || !endTime) return 0;
|
|
||||||
const start = new Date(startTime);
|
|
||||||
const end = new Date(endTime);
|
|
||||||
const diffMs = end - start;
|
|
||||||
return Math.round(diffMs / (1000 * 60)); // Convert to minutes
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status) => {
|
|
||||||
const statusColors = {
|
|
||||||
Unscheduled: "warning",
|
|
||||||
Scheduled: "info",
|
|
||||||
Completed: "success",
|
|
||||||
Cancelled: "error",
|
|
||||||
};
|
|
||||||
return statusColors[status] || "default";
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTimeDisplay = (time) => {
|
const formatTimeDisplay = (time) => {
|
||||||
if (!time) return "";
|
if (!time) return "";
|
||||||
const [hours, minutes] = time.split(":").map(Number);
|
const [hours, minutes] = time.split(":").map(Number);
|
||||||
|
|
@ -273,39 +101,17 @@ const formatDate = (dateStr) => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row {
|
.detail-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-row:last-of-type {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row strong {
|
.detail-row strong {
|
||||||
min-width: 120px;
|
margin-right: 8px;
|
||||||
color: #666;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value {
|
|
||||||
flex: 1;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 2px solid #e0e0e0;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -201,21 +201,6 @@ const tableActions = [
|
||||||
router.push(`/estimate?new=true&address=${address}`);
|
router.push(`/estimate?new=true&address=${address}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Action",
|
|
||||||
rowAction: true,
|
|
||||||
type: "menu",
|
|
||||||
menuItems: [
|
|
||||||
{
|
|
||||||
label: "View Client Details",
|
|
||||||
action: (rowData) => {
|
|
||||||
const client = encodeURIComponent(rowData.customerName);
|
|
||||||
const address = encodeURIComponent(rowData.address);
|
|
||||||
router.push(`/client?client=${client}&address=${address}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
// {
|
// {
|
||||||
// label: "Export Selected",
|
// label: "Export Selected",
|
||||||
// action: (selectedRows) => {
|
// action: (selectedRows) => {
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ const columns = [
|
||||||
{ label: "Address", fieldName: "customInstallationAddress", type: "text", sortable: true },
|
{ label: "Address", fieldName: "customInstallationAddress", type: "text", sortable: true },
|
||||||
{ label: "Customer", fieldName: "customer", type: "text", sortable: true, filterable: true },
|
{ label: "Customer", fieldName: "customer", type: "text", sortable: true, filterable: true },
|
||||||
{ label: "Overall Status", fieldName: "status", type: "status", sortable: true },
|
{ label: "Overall Status", fieldName: "status", type: "status", sortable: true },
|
||||||
{ label: "Progress", fieldName: "percentComplete", type: "text", sortable: true }
|
{ label: "Progress", fieldName: "percentComplete", type: "text", sortable: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
const STORAGE_KEY = "selectedCompany";
|
|
||||||
|
|
||||||
export const useCompanyStore = defineStore("company", {
|
export const useCompanyStore = defineStore("company", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
companies: ["Sprinklers Northwest", "Nuco Yard Care", "Lowe Fencing", "Veritas Stone", "Daniels Landscape Supplies"],
|
companies: ["Sprinklers Northwest", "Nuco Yard Care", "Lowe Fencing", "Veritas Stone", "Daniels Landscape Supplies"],
|
||||||
selectedCompany: localStorage.getItem(STORAGE_KEY) || "Sprinklers Northwest",
|
selectedCompany: "Sprinklers Northwest",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
|
@ -16,7 +14,6 @@ export const useCompanyStore = defineStore("company", {
|
||||||
setSelectedCompany(companyName) {
|
setSelectedCompany(companyName) {
|
||||||
if (this.companies.includes(companyName)) {
|
if (this.companies.includes(companyName)) {
|
||||||
this.selectedCompany = companyName;
|
this.selectedCompany = companyName;
|
||||||
localStorage.setItem(STORAGE_KEY, companyName);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -24,9 +21,6 @@ export const useCompanyStore = defineStore("company", {
|
||||||
this.companies = [...companies];
|
this.companies = [...companies];
|
||||||
if (!this.companies.includes(this.selectedCompany)) {
|
if (!this.companies.includes(this.selectedCompany)) {
|
||||||
this.selectedCompany = this.companies[0] || null;
|
this.selectedCompany = this.companies[0] || null;
|
||||||
if (this.selectedCompany) {
|
|
||||||
localStorage.setItem(STORAGE_KEY, this.selectedCompany);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue