update estimate page, add stripe script and docker compose for mail server
This commit is contained in:
parent
cacbc764c1
commit
678eb18583
@ -131,6 +131,7 @@
|
||||
label="Add Item"
|
||||
icon="pi pi-plus"
|
||||
@click="showAddItemModal = true"
|
||||
:disabled="!quotationItems || Object.keys(quotationItems).length === 0"
|
||||
/>
|
||||
<div v-for="(item, index) in selectedItems" :key="item.itemCode" class="item-row">
|
||||
<span>{{ item.itemName }}</span>
|
||||
@ -146,7 +147,20 @@
|
||||
class="qty-input"
|
||||
/>
|
||||
</div>
|
||||
<span>Price: ${{ (item.standardRate || 0).toFixed(2) }}</span>
|
||||
<span>X</span>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Rate</span>
|
||||
<InputNumber
|
||||
v-model="item.rate"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="onRateChange(item)"
|
||||
class="rate-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Discount</span>
|
||||
<div class="discount-container">
|
||||
@ -193,7 +207,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>Total: ${{ ((item.qty || 0) * (item.standardRate || 0) - (item.discountAmount || 0)).toFixed(2) }}</span>
|
||||
<span>Total: ${{ ((item.qty || 0) * (item.rate || 0) - (item.discountAmount || 0)).toFixed(2) }}</span>
|
||||
<Button
|
||||
v-if="isEditable"
|
||||
icon="pi pi-trash"
|
||||
@ -281,39 +295,12 @@
|
||||
</Modal>
|
||||
|
||||
<!-- Add Item Modal -->
|
||||
<Modal
|
||||
<AddItemModal
|
||||
:visible="showAddItemModal"
|
||||
@update:visible="showAddItemModal = $event"
|
||||
@close="closeAddItemModal"
|
||||
:options="{ showActions: false }"
|
||||
>
|
||||
<template #title>Add Item</template>
|
||||
<div class="modal-content items-modal-content">
|
||||
<div class="search-section">
|
||||
<label for="item-search" class="field-label">Search Items</label>
|
||||
<InputText
|
||||
id="item-search"
|
||||
v-model="itemSearchTerm"
|
||||
placeholder="Search by item code or name..."
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
<div class="tip-section">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<span>Tip: Hold <kbd>Ctrl</kbd> (or <kbd>Cmd</kbd> on Mac) to select multiple items</span>
|
||||
</div>
|
||||
<DataTable
|
||||
:data="filteredItems"
|
||||
:columns="itemColumns"
|
||||
:tableName="'estimate-items'"
|
||||
:tableActions="tableActions"
|
||||
selectable
|
||||
:paginator="false"
|
||||
:scrollHeight="'55vh'"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
:quotation-items="quotationItems"
|
||||
@add-items="addSelectedItems"
|
||||
/>
|
||||
<!-- Down Payment Warning Modal -->
|
||||
<Modal
|
||||
:visible="showDownPaymentWarningModal"
|
||||
@ -363,7 +350,7 @@
|
||||
<ul>
|
||||
<li v-for="item in selectedItems" :key="item.itemCode">
|
||||
{{ item.itemName }} - Qty: {{ item.qty || 0 }} - Total: ${{
|
||||
((item.qty || 0) * (item.standardRate || 0)).toFixed(2)
|
||||
((item.qty || 0) * (item.rate || 0)).toFixed(2)
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
@ -417,6 +404,7 @@ import { ref, reactive, computed, onMounted, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import Modal from "../common/Modal.vue";
|
||||
import SaveTemplateModal from "../modals/SaveTemplateModal.vue";
|
||||
import AddItemModal from "../modals/AddItemModal.vue";
|
||||
import DataTable from "../common/DataTable.vue";
|
||||
import DocHistory from "../common/DocHistory.vue";
|
||||
import BidMeetingNotes from "../modals/BidMeetingNotes.vue";
|
||||
@ -482,7 +470,6 @@ const showDownPaymentWarningModal = ref(false);
|
||||
const showResponseModal = ref(false);
|
||||
const showSaveTemplateModal = ref(false);
|
||||
const addressSearchResults = ref([]);
|
||||
const itemSearchTerm = ref("");
|
||||
const showDrawer = ref(false);
|
||||
|
||||
const estimate = ref(null);
|
||||
@ -684,7 +671,7 @@ const selectAddress = async (address) => {
|
||||
const addItem = (item) => {
|
||||
const existing = selectedItems.value.find((i) => i.itemCode === item.itemCode);
|
||||
if (!existing) {
|
||||
selectedItems.value.push({ ...item, qty: 1, discountAmount: null, discountPercentage: null, discountType: 'currency' });
|
||||
selectedItems.value.push({ ...item, qty: 1, rate: item.standardRate, discountAmount: null, discountPercentage: null, discountType: 'currency' });
|
||||
}
|
||||
showAddItemModal.value = false;
|
||||
};
|
||||
@ -697,16 +684,12 @@ const addSelectedItems = (selectedRows) => {
|
||||
existing.qty += 1;
|
||||
} else {
|
||||
// Add new item with quantity 1
|
||||
selectedItems.value.push({ ...item, qty: 1, discountAmount: null, discountPercentage: null, discountType: 'currency' });
|
||||
selectedItems.value.push({ ...item, qty: 1, rate: item.standardRate, discountAmount: null, discountPercentage: null, discountType: 'currency' });
|
||||
}
|
||||
});
|
||||
showAddItemModal.value = false;
|
||||
};
|
||||
|
||||
const closeAddItemModal = () => {
|
||||
showAddItemModal.value = false;
|
||||
};
|
||||
|
||||
const removeItem = (index) => {
|
||||
selectedItems.value.splice(index, 1);
|
||||
};
|
||||
@ -716,7 +699,7 @@ const clearItems = () => {
|
||||
};
|
||||
|
||||
const updateDiscountFromAmount = (item) => {
|
||||
const total = (item.qty || 0) * (item.standardRate || 0);
|
||||
const total = (item.qty || 0) * (item.rate || 0);
|
||||
if (total === 0) {
|
||||
item.discountPercentage = 0;
|
||||
} else {
|
||||
@ -725,7 +708,7 @@ const updateDiscountFromAmount = (item) => {
|
||||
};
|
||||
|
||||
const updateDiscountFromPercentage = (item) => {
|
||||
const total = (item.qty || 0) * (item.standardRate || 0);
|
||||
const total = (item.qty || 0) * (item.rate || 0);
|
||||
item.discountAmount = total * ((item.discountPercentage || 0) / 100);
|
||||
};
|
||||
|
||||
@ -737,6 +720,14 @@ const onQtyChange = (item) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onRateChange = (item) => {
|
||||
if (item.discountType === 'percentage') {
|
||||
updateDiscountFromPercentage(item);
|
||||
} else {
|
||||
updateDiscountFromAmount(item);
|
||||
}
|
||||
};
|
||||
|
||||
const saveDraft = async () => {
|
||||
if (!formData.projectTemplate) {
|
||||
notificationStore.addNotification("Project Template is required.", "error");
|
||||
@ -752,6 +743,7 @@ const saveDraft = async () => {
|
||||
items: selectedItems.value.map((i) => ({
|
||||
itemCode: i.itemCode,
|
||||
qty: i.qty,
|
||||
rate: i.rate,
|
||||
discountAmount: i.discountAmount,
|
||||
discountPercentage: i.discountPercentage
|
||||
})),
|
||||
@ -844,38 +836,15 @@ const onTabClick = () => {
|
||||
console.log('Set showDrawer to true');
|
||||
};
|
||||
|
||||
const tableActions = [
|
||||
{
|
||||
label: "Add Selected Items",
|
||||
action: addSelectedItems,
|
||||
requiresMultipleSelection: true,
|
||||
icon: "pi pi-plus",
|
||||
style: "primary",
|
||||
},
|
||||
];
|
||||
|
||||
const totalCost = computed(() => {
|
||||
return (selectedItems.value || []).reduce((sum, item) => {
|
||||
const qty = item.qty || 0;
|
||||
const rate = item.standardRate || 0;
|
||||
const rate = item.rate || 0;
|
||||
const discount = item.discountAmount || 0;
|
||||
return sum + (qty * rate) - discount;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
if (!itemSearchTerm.value.trim()) {
|
||||
return quotationItems.value.map((item) => ({ ...item, id: item.itemCode }));
|
||||
}
|
||||
const term = itemSearchTerm.value.toLowerCase();
|
||||
return quotationItems.value
|
||||
.filter(
|
||||
(item) =>
|
||||
item.itemCode.toLowerCase().includes(term) ||
|
||||
item.itemName.toLowerCase().includes(term),
|
||||
)
|
||||
.map((item) => ({ ...item, id: item.itemCode }));
|
||||
});
|
||||
watch(
|
||||
() => formData.contact,
|
||||
(newVal) => {
|
||||
@ -883,6 +852,10 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(() => formData.projectTemplate, async (newValue) => {
|
||||
quotationItems.value = await Api.getQuotationItems(newValue);
|
||||
})
|
||||
|
||||
watch(() => company.currentCompany, () => {
|
||||
if (isNew.value) {
|
||||
fetchTemplates();
|
||||
@ -1023,11 +996,11 @@ 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);
|
||||
}
|
||||
// try {
|
||||
// quotationItems.value = await Api.getQuotationItems(selectedTemplate.value);
|
||||
// } catch (error) {
|
||||
// console.error("Error loading quotation items:", error);
|
||||
// }
|
||||
await fetchProjectTemplates();
|
||||
|
||||
if (isNew.value) {
|
||||
@ -1063,6 +1036,7 @@ onMounted(async () => {
|
||||
// Handle project-template query parameter
|
||||
if (projectTemplateQuery.value) {
|
||||
formData.projectTemplate = projectTemplateQuery.value;
|
||||
quotationItems.value = await Api.getQuotationItems(projectTemplateQuery.value);
|
||||
}
|
||||
|
||||
if (addressQuery.value && isNew.value) {
|
||||
@ -1138,7 +1112,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
.estimate-page {
|
||||
max-width: 800px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
@ -1238,7 +1212,7 @@ onMounted(async () => {
|
||||
|
||||
.item-row {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 140px 1.5fr 220px 1.5fr auto;
|
||||
grid-template-columns: 3fr 140px 30px 120px 220px 1.5fr auto;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
@ -1262,6 +1236,16 @@ onMounted(async () => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rate-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rate-input :deep(.p-inputtext) {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.discount-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -1294,7 +1278,7 @@ onMounted(async () => {
|
||||
|
||||
/* When viewing (not editing), adjust grid to remove delete button column */
|
||||
.estimate-page:has(h2:contains("View")) .item-row {
|
||||
grid-template-columns: 3fr 140px 1.5fr 220px 1.5fr;
|
||||
grid-template-columns: 3fr 140px 30px 120px 220px 1.5fr;
|
||||
}
|
||||
|
||||
.total-section {
|
||||
@ -1316,45 +1300,6 @@ onMounted(async () => {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.items-modal-content {
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user