estimate view

This commit is contained in:
Casey 2025-12-02 12:27:58 -06:00
parent 7c738ef9f9
commit 0663cd2d8c
4 changed files with 116 additions and 23 deletions

View File

@ -32,9 +32,10 @@ def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10):
tableRows = []
for estimate in estimates:
full_address = frappe.db.get_value("Address", estimate.get("custom_installation_address"), "full_address")
tableRow = {}
tableRow["id"] = estimate["name"]
tableRow["name"] = estimate["name"]
tableRow["address"] = full_address
tableRow["quotation_to"] = estimate.get("quotation_to", "")
tableRow["customer"] = estimate.get("party_name", "")
tableRow["status"] = estimate.get("custom_current_status", "")
@ -78,17 +79,21 @@ def get_estimate_items():
@frappe.whitelist()
def get_estimate_from_address(full_address):
quotation = frappe.db.sql("""
SELECT q.name, q.custom_installation_address
FROM `tabQuotation` q
JOIN `tabAddress` a
ON q.custom_installation_address = a.name
WHERE a.full_address =%s
""", (full_address,), as_dict=True)
if quotation:
return build_success_response(quotation)
else:
return build_error_response("No quotation found for the given address.", 404)
address_name = frappe.db.get_value("Address", {"full_address": full_address}, "name")
quotation_name = frappe.db.get_value("Quotation", {"custom_installation_address": address_name}, "name")
quotation_doc = frappe.get_doc("Quotation", quotation_name)
return build_success_response(quotation_doc.as_dict())
# quotation = frappe.db.sql("""
# SELECT q.name, q.custom_installation_address
# FROM `tabQuotation` q
# JOIN `tabAddress` a
# ON q.custom_installation_address = a.name
# WHERE a.full_address =%s
# """, (full_address,), as_dict=True)
# if quotation:
# return build_success_response(quotation)
# else:
# return build_error_response("No quotation found for the given address.", 404)
@frappe.whitelist()
def upsert_estimate(data):

View File

@ -7,6 +7,7 @@ const FRAPPE_PROXY_METHOD = "custom_ui.api.proxy.request";
// Estimate methods
const FRAPPE_UPSERT_ESTIMATE_METHOD = "custom_ui.api.db.estimates.upsert_estimate";
const FRAPPE_GET_ESTIMATES_METHOD = "custom_ui.api.db.estimates.get_estimate_table_data";
const FRAPPE_GET_ESTIMATE_BY_ADDRESS_METHOD = "custom_ui.api.db.estimates.get_estimate_from_address";
// Job methods
const FRAPPE_GET_JOBS_METHOD = "custom_ui.api.db.get_jobs";
const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.jobs.upsert_job";
@ -61,7 +62,7 @@ class Api {
}
static async getEstimateFromAddress(fullAddress) {
return await this.request("custom_ui.api.db.estimates.get_estimate_from_address", {
return await this.request(FRAPPE_GET_ESTIMATE_BY_ADDRESS_METHOD, {
full_address: fullAddress,
});
}

View File

@ -1,6 +1,6 @@
<template>
<div class="estimate-page">
<h2>Create Estimate</h2>
<h2>{{ isNew ? 'Create Estimate' : 'View Estimate' }}</h2>
<!-- Address Section -->
<div class="address-section">
@ -13,13 +13,14 @@
id="address"
v-model="formData.address"
placeholder="Enter address to search"
:disabled="!isNew"
fluid
/>
<Button
label="Search"
icon="pi pi-search"
@click="searchAddresses"
:disabled="!formData.address.trim()"
:disabled="!formData.address.trim() || !isNew"
class="search-button"
/>
</div>
@ -41,7 +42,7 @@
optionLabel="label"
optionValue="value"
placeholder="Select a contact"
:disabled="!formData.address"
:disabled="!formData.address || !isEditable"
fluid
/>
<div v-if="selectedContact" class="verification-info">
@ -55,24 +56,35 @@
<!-- Items Section -->
<div class="items-section">
<h3>Items</h3>
<Button label="Add Item" icon="pi pi-plus" @click="showAddItemModal = true" />
<Button
v-if="isEditable"
label="Add Item"
icon="pi pi-plus"
@click="showAddItemModal = true"
/>
<div v-for="(item, index) in selectedItems" :key="item.itemCode" class="item-row">
<span>{{ item.itemName }}</span>
<InputNumber
v-model="item.qty"
:min="1"
:disabled="!isEditable"
showButtons
buttonLayout="horizontal"
@input="updateTotal"
/>
<span>Price: ${{ (item.standardRate || 0).toFixed(2) }}</span>
<span>Total: ${{ ((item.qty || 0) * (item.standardRate || 0)).toFixed(2) }}</span>
<Button icon="pi pi-trash" @click="removeItem(index)" severity="danger" />
<Button
v-if="isEditable"
icon="pi pi-trash"
@click="removeItem(index)"
severity="danger"
/>
</div>
<div class="total-section">
<strong>Total Cost: ${{ totalCost.toFixed(2) }}</strong>
</div>
<div class="action-buttons">
<div v-if="isEditable" class="action-buttons">
<Button label="Clear Items" @click="clearItems" severity="secondary" />
<Button
label="Submit"
@ -198,6 +210,7 @@ const loadingStore = useLoadingStore();
const notificationStore = useNotificationStore();
const addressQuery = route.query.address;
const isNew = route.query.new === "true" ? true : false;
const isSubmitting = ref(false);
@ -220,6 +233,16 @@ const showConfirmationModal = ref(false);
const addressSearchResults = ref([]);
const itemSearchTerm = ref("");
const estimate = ref(null);
// Computed property to determine if fields are editable
const isEditable = computed(() => {
if (isNew) return true;
if (!estimate.value) return false;
// If docstatus is 0 (draft), allow editing of contact and items
return estimate.value.docstatus === 0;
});
const itemColumns = [
{ label: "Item Code", fieldName: "itemCode", type: "text" },
{ label: "Item Name", fieldName: "itemName", type: "text" },
@ -263,7 +286,9 @@ const selectAddress = async (address) => {
value: c.name,
}));
const primary = contacts.value.find((c) => c.isPrimaryContact);
formData.contact = primary ? primary.name : contacts.value[0]?.name || "";
console.log("DEBUG: Selected address contacts:", contacts.value);
const existingContactName = estimate.value ? contacts.value.find((c) => c.fullName === estimate.value.partyName)?.name || "" : null;
formData.contact = estimate.value ? existingContactName : primary ? primary.name : contacts.value[0]?.name || "";
showAddressModal.value = false;
};
@ -371,13 +396,49 @@ watch(
);
onMounted(async () => {
console.log("DEBUG: Query params:", route.query);
try {
quotationItems.value = await Api.getQuotationItems();
} catch (error) {
console.error("Error loading quotation items:", error);
}
if (addressQuery) {
if (addressQuery && isNew) {
// Creating new estimate - pre-fill address
await selectAddress(addressQuery);
} else if (addressQuery && !isNew) {
// Viewing existing estimate - load and populate all fields
try {
estimate.value = await Api.getEstimateFromAddress(addressQuery);
console.log("DEBUG: Loaded estimate:", estimate.value);
if (estimate.value) {
await selectAddress(addressQuery);
// Set the contact from the estimate
formData.contact = estimate.value.partyName;
selectedContact.value = contacts.value.find((c) => c.name === estimate.value.partyName) || null;
// Populate items from the estimate
if (estimate.value.items && estimate.value.items.length > 0) {
selectedItems.value = estimate.value.items.map(item => {
// Find the full item details from quotationItems
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"
);
}
}
});
</script>
@ -435,6 +496,11 @@ onMounted(async () => {
border-radius: 4px;
}
/* When viewing (not editing), adjust grid to remove delete button column */
.estimate-page:has(h2:contains("View")) .item-row {
grid-template-columns: 2fr 1fr auto auto;
}
.total-section {
margin-top: 1rem;
font-size: 1.2rem;

View File

@ -4,6 +4,7 @@
<DataTable
:data="tableData"
:columns="columns"
:tableActions="tableActions"
tableName="estimates"
:lazy="true"
:totalRecords="totalRecords"
@ -38,10 +39,12 @@ import Api from "../../api";
import { useLoadingStore } from "../../stores/loading";
import { usePaginationStore } from "../../stores/pagination";
import { useFiltersStore } from "../../stores/filters";
import { useRouter } from "vue-router";
const loadingStore = useLoadingStore();
const paginationStore = usePaginationStore();
const filtersStore = useFiltersStore();
const router = useRouter();
const tableData = ref([]);
const totalRecords = ref(0);
@ -53,7 +56,7 @@ const filteredItems= []
// End junk
const columns = [
{ label: "Estimate ID", fieldName: "name", type: "text", sortable: true, filterable: true },
{ label: "Estimate Address", fieldName: "address", type: "text", sortable: true, filterable: true },
//{ label: "Address", fieldName: "customInstallationAddress", type: "text", sortable: true },
{ label: "Customer", fieldName: "customer", type: "text", sortable: true, filterable: true },
{
@ -70,8 +73,26 @@ const columns = [
//{ label: "Estimate Amount", fieldName:
];
const tableActions = [
{
label: "View Details",
action: (rowData) => {
router.push(`/estimate?address=${encodeURIComponent(rowData.address)}`);
},
type: "button",
style: "info",
icon: "pi pi-eye",
requiresSelection: true,
layout: {
position: "center",
variant: "outlined",
},
},
];
const handleEstimateClick = (status, rowData) => {
showSubmitEstimateModal.value = true;
// Navigate to estimate details page with the address
router.push(`/estimate?address=${encodeURIComponent(rowData.address)}`);
};
const closeSubmitEstimateModal = () => {