estimate view
This commit is contained in:
parent
7c738ef9f9
commit
0663cd2d8c
@ -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):
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user