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
+
- Confirm Estimate
+ Confirm Send Estimate
Does this information look correct?
Address: {{ formData.address }}
@@ -171,6 +176,10 @@
: ""
}}
+
+ Email:
+ {{ selectedContact?.emailId || "N/A" }}
+
Items:
Total: ${{ totalCost.toFixed(2) }}
+
⚠️ Warning: After sending this estimate, it will be locked and cannot be edited.
-
+
-
-
- 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,