diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index 4d5416b..ef1735b 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -253,17 +253,25 @@ def upsert_client(data): print("#####DEBUG: Processing contact data:", contact_data) contact_exists = frappe.db.exists("Contact", {"email_id": contact_data.get("email"), "phone": contact_data.get("phone_number")}) if not contact_exists: - is_primary_contact = 1 if contact_data.get("is_primary_contact") else 0 contact_doc = frappe.get_doc({ "doctype": "Contact", "first_name": contact_data.get("first_name"), "last_name": contact_data.get("last_name"), - "email_id": contact_data.get("email"), - "phone": contact_data.get("phone_number"), + # "email_id": contact_data.get("email"), + # "phone": contact_data.get("phone_number"), "custom_customer": customer_doc.name, "role": contact_data.get("contact_role", "Other"), "custom_email": contact_data.get("email"), - "is_primary_contact": is_primary_contact + "is_primary_contact": data.get("is_primary", False), + "email_ids": [{ + "email_id": contact_data.get("email"), + "is_primary": 1 + }], + "phone_nos": [{ + "phone": contact_data.get("phone_number"), + "is_primary_mobile_no": 1, + "is_primary_phone": 1 + }] }).insert(ignore_permissions=True) print("Created new contact:", contact_doc.as_dict()) else: @@ -316,6 +324,7 @@ def upsert_client(data): # Contact -> Customer & Address print("#####DEBUG: Linking contacts to customer.") for contact_doc in contact_docs: + contact_doc.address = address_doc.name contact_doc.append("links", { "link_doctype": "Customer", "link_name": customer_doc.name diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index d8a33ac..3496a06 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -66,12 +66,6 @@ def get_estimate(estimate_name): except Exception as e: return build_error_response(str(e), 500) -@frappe.whitelist() -def upsert_estimate(data): - """Create or update an estimate.""" - # TODO: Implement estimate creation/update logic - pass - @frappe.whitelist() def get_estimate_items(): items = frappe.db.get_all("Quotation Item", fields=["*"]) @@ -98,25 +92,54 @@ def get_estimate_from_address(full_address): @frappe.whitelist() def upsert_estimate(data): """Create or update an estimate.""" - print("DOIFJSEOFJISLFK") try: data = json.loads(data) if isinstance(data, str) else data - print("DEBUG: Retrieved address name:", data.get("address_name")) - new_estimate = frappe.get_doc({ - "doctype": "Quotation", - "custom_installation_address": data.get("address_name"), - "contact_email": data.get("contact_email"), - "party_name": data.get("contact_name"), - "customer_name": data.get("customer_name"), - }) - for item in data.get("items", []): - item = json.loads(item) if isinstance(item, str) else item - new_estimate.append("items", { - "item_code": item.get("item_code"), - "qty": item.get("qty"), + print("DEBUG: Upsert estimate data:", data) + + estimate_name = data.get("estimate_name") + + # If estimate_name exists, update existing estimate + if estimate_name: + print(f"DEBUG: Updating existing estimate: {estimate_name}") + estimate = frappe.get_doc("Quotation", estimate_name) + + # Update fields + estimate.custom_installation_address = data.get("address_name") + estimate.party_name = data.get("contact_name") + + # Clear existing items and add new ones + estimate.items = [] + for item in data.get("items", []): + item = json.loads(item) if isinstance(item, str) else item + estimate.append("items", { + "item_code": item.get("item_code"), + "qty": item.get("qty"), + }) + + estimate.save() + print(f"DEBUG: Estimate updated: {estimate.name}") + return build_success_response(estimate.as_dict()) + + # Otherwise, create new estimate + else: + print("DEBUG: Creating new estimate") + print("DEBUG: Retrieved address name:", data.get("address_name")) + new_estimate = frappe.get_doc({ + "doctype": "Quotation", + "custom_installation_address": data.get("address_name"), + "contact_email": data.get("contact_email"), + "party_name": data.get("contact_name"), + "customer_name": data.get("customer_name"), }) - new_estimate.insert() - print("DEBUG: New estimate created with name:", new_estimate.name) - return build_success_response(new_estimate.as_dict()) + for item in data.get("items", []): + item = json.loads(item) if isinstance(item, str) else item + new_estimate.append("items", { + "item_code": item.get("item_code"), + "qty": item.get("qty"), + }) + new_estimate.insert() + print("DEBUG: New estimate created with name:", new_estimate.name) + return build_success_response(new_estimate.as_dict()) except Exception as e: + print(f"DEBUG: Error in upsert_estimate: {str(e)}") return build_error_response(str(e), 500) \ No newline at end of file diff --git a/frontend/src/components/pages/Clients.vue b/frontend/src/components/pages/Clients.vue index 798bbf5..3e12294 100644 --- a/frontend/src/components/pages/Clients.vue +++ b/frontend/src/components/pages/Clients.vue @@ -269,41 +269,11 @@ const handleLazyLoad = async (event) => { }); } - // Clear cache when filters or sorting are active to ensure fresh data - const hasActiveFilters = Object.keys(filters).length > 0; - const hasActiveSorting = event.sortField && event.sortOrder; - if (hasActiveFilters || hasActiveSorting) { - paginationStore.clearTableCache("clients"); - } - // For cache key, use primary sort field/order for compatibility const primarySortField = filtersStore.getPrimarySortField("clients") || event.sortField; const primarySortOrder = filtersStore.getPrimarySortOrder("clients") || event.sortOrder; - // Check cache first - const cachedData = paginationStore.getCachedPage( - "clients", - paginationParams.page, - paginationParams.pageSize, - primarySortField, - primarySortOrder, - filters, - ); - - if (cachedData) { - // Use cached data - tableData.value = cachedData.records; - totalRecords.value = cachedData.totalRecords; - paginationStore.setTotalRecords("clients", cachedData.totalRecords); - - console.log("Loaded from cache:", { - records: cachedData.records.length, - total: cachedData.totalRecords, - page: paginationParams.page + 1, - }); - return; - } - + // Always fetch fresh data from API (cache only stores pagination/filter/sort state, not data) // Call API with pagination, filters, and sorting in backend format console.log("Making API call with:", { paginationParams, @@ -325,19 +295,6 @@ const handleLazyLoad = async (event) => { // Update pagination store with new total paginationStore.setTotalRecords("clients", result.pagination.total); - // Cache the result using primary sort for compatibility - paginationStore.setCachedPage( - "clients", - paginationParams.page, - paginationParams.pageSize, - primarySortField, - primarySortOrder, - filters, - { - records: result.data, - totalRecords: result.pagination.total, - }, - ); } catch (error) { console.error("Error loading client data:", error); // You could also show a toast or other error notification here diff --git a/frontend/src/components/pages/Estimate.vue b/frontend/src/components/pages/Estimate.vue index 706392c..90273c2 100644 --- a/frontend/src/components/pages/Estimate.vue +++ b/frontend/src/components/pages/Estimate.vue @@ -46,7 +46,7 @@ fluid />
- Email: {{ selectedContact.customEmail || "N/A" }}
+ Email: {{ selectedContact.emailId || "N/A" }}
Phone: {{ selectedContact.phone || "N/A" }}
Primary Contact: {{ selectedContact.isPrimaryContact ? "Yes" : "No" }} @@ -87,13 +87,13 @@
-
-
@@ -141,6 +141,10 @@ fluid /> +
+ + Tip: Hold Ctrl (or Cmd on Mac) to select multiple items +
- + - - -

Submit Estimate and Email {{ selectedContact.firstName }} {{ - selectedContact.lastName}}?

-

This cannot be undone, please make sure all information is correct.

-
-
-
- @@ -242,6 +232,7 @@ const formData = reactive({ address: "", addressName: "", contact: "", + estimateName: null, }); const selectedAddress = ref(null); @@ -254,7 +245,6 @@ const selectedItems = ref([]); const showAddressModal = ref(false); const showAddItemModal = ref(false); const showConfirmationModal = ref(false); -const showSubmitEstimateModal = ref(false); const addressSearchResults = ref([]); const itemSearchTerm = ref(""); @@ -355,33 +345,37 @@ const updateTotal = () => { // Computed will update }; -const confirmSubmit = async () => { +const saveDraft = async () => { isSubmitting.value = true; - showConfirmationModal.value = false; try { const data = { addressName: formData.addressName, contactName: selectedContact.value.name, items: selectedItems.value.map((i) => ({ itemCode: i.itemCode, qty: i.qty })), + estimateName: formData.estimateName, }; - await Api.createEstimate(data); - notificationStore.addSuccess("Estimate created successfully", "success"); + estimate.value = await Api.createEstimate(data); + notificationStore.addSuccess( + formData.estimateName ? "Estimate updated successfully" : "Estimate created successfully", + "success" + ); + + // Redirect to view mode (remove new param) router.push(`/estimate?address=${encodeURIComponent(formData.address)}`); - // Reset form - formData.address = ""; - formData.addressName = ""; - formData.contact = ""; - selectedAddress.value = null; - selectedContact.value = null; - selectedItems.value = []; } catch (error) { - console.error("Error creating estimate:", error); - notificationStore.addError("Failed to create estimate", "error"); + console.error("Error saving estimate:", error); + notificationStore.addNotification("Failed to save estimate", "error"); } finally { isSubmitting.value = false; } }; +const confirmAndSendEstimate = async () => { + showConfirmationModal.value = false; + // TODO: Implement send estimate functionality + notificationStore.addWarning("Send estimate functionality coming soon"); +}; + const tableActions = [ { label: "Add Selected Items", @@ -420,6 +414,67 @@ watch( }, ); +// Watch for query param changes to refresh page behavior +watch( + () => route.query, + async (newQuery, oldQuery) => { + // If 'new' param or address changed, reload component state + if (newQuery.new !== oldQuery.new || newQuery.address !== oldQuery.address) { + // Reset all state + formData.address = ""; + formData.addressName = ""; + formData.contact = ""; + formData.estimateName = null; + selectedAddress.value = null; + selectedContact.value = null; + contacts.value = []; + contactOptions.value = []; + selectedItems.value = []; + estimate.value = null; + + // Reload data based on new query params + const newIsNew = newQuery.new === "true"; + const newAddressQuery = newQuery.address; + + if (newAddressQuery && newIsNew) { + // Creating new estimate - pre-fill address + await selectAddress(newAddressQuery); + } else if (newAddressQuery && !newIsNew) { + // Viewing existing estimate - load and populate all fields + try { + estimate.value = await Api.getEstimateFromAddress(newAddressQuery); + + if (estimate.value) { + formData.estimateName = estimate.value.name; + await selectAddress(newAddressQuery); + formData.contact = estimate.value.partyName; + selectedContact.value = contacts.value.find((c) => c.name === estimate.value.partyName) || null; + + if (estimate.value.items && estimate.value.items.length > 0) { + selectedItems.value = estimate.value.items.map(item => { + const fullItem = quotationItems.value.find(qi => qi.itemCode === item.itemCode); + return { + itemCode: item.itemCode, + itemName: item.itemName, + qty: item.qty, + standardRate: item.rate || fullItem?.standardRate || 0, + }; + }); + } + } + } catch (error) { + console.error("Error loading estimate:", error); + notificationStore.addNotification( + "Failed to load estimate details.", + "error" + ); + } + } + } + }, + { deep: true } +); + onMounted(async () => { console.log("DEBUG: Query params:", route.query); try { @@ -438,6 +493,9 @@ onMounted(async () => { console.log("DEBUG: Loaded estimate:", estimate.value); if (estimate.value) { + // Set the estimate name for upserting + formData.estimateName = estimate.value.name; + await selectAddress(addressQuery); // Set the contact from the estimate formData.contact = estimate.value.partyName; @@ -549,6 +607,33 @@ onMounted(async () => { margin-bottom: 1rem; } +.tip-section { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem; + margin-bottom: 1rem; + background-color: #e3f2fd; + border: 1px solid #2196f3; + border-radius: 4px; + color: #1565c0; + font-size: 0.9rem; +} + +.tip-section i { + color: #2196f3; +} + +.tip-section kbd { + background-color: #fff; + border: 1px solid #ccc; + border-radius: 3px; + padding: 2px 6px; + font-family: monospace; + font-size: 0.85em; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + .confirmation-buttons { display: flex; gap: 1rem; @@ -556,6 +641,15 @@ onMounted(async () => { margin-top: 1rem; } +.warning-text { + margin-top: 1rem; + padding: 0.75rem; + background-color: #fff3cd; + border: 1px solid #ffc107; + border-radius: 4px; + color: #856404; +} + .address-search-results { min-height: 200px; } diff --git a/frontend/src/components/pages/Estimates.vue b/frontend/src/components/pages/Estimates.vue index 5eb45df..b421eef 100644 --- a/frontend/src/components/pages/Estimates.vue +++ b/frontend/src/components/pages/Estimates.vue @@ -128,37 +128,7 @@ const handleLazyLoad = async (event) => { }); } - // Clear cache when filters or sorting are active to ensure fresh data - const hasActiveFilters = Object.keys(filters).length > 0; - const hasActiveSorting = paginationParams.sortField && paginationParams.sortOrder; - if (hasActiveFilters || hasActiveSorting) { - paginationStore.clearTableCache("estimates"); - } - - // Check cache first - const cachedData = paginationStore.getCachedPage( - "estimates", - paginationParams.page, - paginationParams.pageSize, - sorting.field || paginationParams.sortField, - sorting.order || paginationParams.sortOrder, - filters, - ); - - if (cachedData) { - // Use cached data - tableData.value = cachedData.records; - totalRecords.value = cachedData.totalRecords; - paginationStore.setTotalRecords("estimates", cachedData.totalRecords); - - console.log("Loaded from cache:", { - records: cachedData.records.length, - total: cachedData.totalRecords, - page: paginationParams.page + 1, - }); - return; - } - + // Always fetch fresh data from API (cache only stores pagination/filter/sort state, not data) console.log("Making API call with:", { paginationParams, filters }); // Call API with pagination, filters, and sorting @@ -180,20 +150,6 @@ const handleLazyLoad = async (event) => { storeTotalPages: paginationStore.getTotalPages("estimates"), }); - // Cache the result - paginationStore.setCachedPage( - "estimates", - paginationParams.page, - paginationParams.pageSize, - sorting.field || paginationParams.sortField, - sorting.order || paginationParams.sortOrder, - filters, - { - records: result.data, - totalRecords: result.pagination.total, - }, - ); - console.log("Loaded from API:", { records: result.data.length, total: result.pagination.total, diff --git a/frontend/src/components/pages/Invoices.vue b/frontend/src/components/pages/Invoices.vue index 0856e3c..1e4f379 100644 --- a/frontend/src/components/pages/Invoices.vue +++ b/frontend/src/components/pages/Invoices.vue @@ -75,37 +75,7 @@ const handleLazyLoad = async (event) => { }); } - // Clear cache when filters or sorting are active to ensure fresh data - const hasActiveFilters = Object.keys(filters).length > 0; - const hasActiveSorting = paginationParams.sortField && paginationParams.sortOrder; - if (hasActiveFilters || hasActiveSorting) { - paginationStore.clearTableCache("invoices"); - } - - // Check cache first - const cachedData = paginationStore.getCachedPage( - "invoices", - paginationParams.page, - paginationParams.pageSize, - sorting.field || paginationParams.sortField, - sorting.order || paginationParams.sortOrder, - filters, - ); - - if (cachedData) { - // Use cached data - tableData.value = cachedData.records; - totalRecords.value = cachedData.totalRecords; - paginationStore.setTotalRecords("invoices", cachedData.totalRecords); - - console.log("Loaded from cache:", { - records: cachedData.records.length, - total: cachedData.totalRecords, - page: paginationParams.page + 1, - }); - return; - } - + // Always fetch fresh data from API (cache only stores pagination/filter/sort state, not data) console.log("Making API call with:", { paginationParams, filters }); // Call API with pagination, filters, and sorting @@ -127,20 +97,6 @@ const handleLazyLoad = async (event) => { storeTotalPages: paginationStore.getTotalPages("invoices"), }); - // Cache the result - paginationStore.setCachedPage( - "invoices", - paginationParams.page, - paginationParams.pageSize, - sorting.field || paginationParams.sortField, - sorting.order || paginationParams.sortOrder, - filters, - { - records: result.data, - totalRecords: result.pagination.total, - }, - ); - console.log("Loaded from API:", { records: result.data.length, total: result.pagination.total,