update estimate page, add stripe script and docker compose for mail server

This commit is contained in:
Casey 2026-02-03 17:03:35 -06:00
parent cacbc764c1
commit 678eb18583

View file

@ -131,6 +131,7 @@
label="Add Item" label="Add Item"
icon="pi pi-plus" icon="pi pi-plus"
@click="showAddItemModal = true" @click="showAddItemModal = true"
:disabled="!quotationItems || Object.keys(quotationItems).length === 0"
/> />
<div v-for="(item, index) in selectedItems" :key="item.itemCode" class="item-row"> <div v-for="(item, index) in selectedItems" :key="item.itemCode" class="item-row">
<span>{{ item.itemName }}</span> <span>{{ item.itemName }}</span>
@ -146,7 +147,20 @@
class="qty-input" class="qty-input"
/> />
</div> </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"> <div class="input-wrapper">
<span class="input-label">Discount</span> <span class="input-label">Discount</span>
<div class="discount-container"> <div class="discount-container">
@ -193,7 +207,7 @@
</div> </div>
</div> </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 <Button
v-if="isEditable" v-if="isEditable"
icon="pi pi-trash" icon="pi pi-trash"
@ -281,39 +295,12 @@
</Modal> </Modal>
<!-- Add Item Modal --> <!-- Add Item Modal -->
<Modal <AddItemModal
:visible="showAddItemModal" :visible="showAddItemModal"
@update:visible="showAddItemModal = $event" @update:visible="showAddItemModal = $event"
@close="closeAddItemModal" :quotation-items="quotationItems"
:options="{ showActions: false }" @add-items="addSelectedItems"
>
<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>
<!-- Down Payment Warning Modal --> <!-- Down Payment Warning Modal -->
<Modal <Modal
:visible="showDownPaymentWarningModal" :visible="showDownPaymentWarningModal"
@ -363,7 +350,7 @@
<ul> <ul>
<li v-for="item in selectedItems" :key="item.itemCode"> <li v-for="item in selectedItems" :key="item.itemCode">
{{ item.itemName }} - Qty: {{ item.qty || 0 }} - Total: ${{ {{ item.itemName }} - Qty: {{ item.qty || 0 }} - Total: ${{
((item.qty || 0) * (item.standardRate || 0)).toFixed(2) ((item.qty || 0) * (item.rate || 0)).toFixed(2)
}} }}
</li> </li>
</ul> </ul>
@ -417,6 +404,7 @@ import { ref, reactive, computed, onMounted, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import Modal from "../common/Modal.vue"; import Modal from "../common/Modal.vue";
import SaveTemplateModal from "../modals/SaveTemplateModal.vue"; import SaveTemplateModal from "../modals/SaveTemplateModal.vue";
import AddItemModal from "../modals/AddItemModal.vue";
import DataTable from "../common/DataTable.vue"; import DataTable from "../common/DataTable.vue";
import DocHistory from "../common/DocHistory.vue"; import DocHistory from "../common/DocHistory.vue";
import BidMeetingNotes from "../modals/BidMeetingNotes.vue"; import BidMeetingNotes from "../modals/BidMeetingNotes.vue";
@ -482,7 +470,6 @@ const showDownPaymentWarningModal = ref(false);
const showResponseModal = ref(false); const showResponseModal = ref(false);
const showSaveTemplateModal = ref(false); const showSaveTemplateModal = ref(false);
const addressSearchResults = ref([]); const addressSearchResults = ref([]);
const itemSearchTerm = ref("");
const showDrawer = ref(false); const showDrawer = ref(false);
const estimate = ref(null); const estimate = ref(null);
@ -684,7 +671,7 @@ const selectAddress = async (address) => {
const addItem = (item) => { const addItem = (item) => {
const existing = selectedItems.value.find((i) => i.itemCode === item.itemCode); const existing = selectedItems.value.find((i) => i.itemCode === item.itemCode);
if (!existing) { 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; showAddItemModal.value = false;
}; };
@ -697,16 +684,12 @@ const addSelectedItems = (selectedRows) => {
existing.qty += 1; existing.qty += 1;
} else { } else {
// Add new item with quantity 1 // 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; showAddItemModal.value = false;
}; };
const closeAddItemModal = () => {
showAddItemModal.value = false;
};
const removeItem = (index) => { const removeItem = (index) => {
selectedItems.value.splice(index, 1); selectedItems.value.splice(index, 1);
}; };
@ -716,7 +699,7 @@ const clearItems = () => {
}; };
const updateDiscountFromAmount = (item) => { const updateDiscountFromAmount = (item) => {
const total = (item.qty || 0) * (item.standardRate || 0); const total = (item.qty || 0) * (item.rate || 0);
if (total === 0) { if (total === 0) {
item.discountPercentage = 0; item.discountPercentage = 0;
} else { } else {
@ -725,7 +708,7 @@ const updateDiscountFromAmount = (item) => {
}; };
const updateDiscountFromPercentage = (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); 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 () => { const saveDraft = async () => {
if (!formData.projectTemplate) { if (!formData.projectTemplate) {
notificationStore.addNotification("Project Template is required.", "error"); notificationStore.addNotification("Project Template is required.", "error");
@ -752,6 +743,7 @@ const saveDraft = async () => {
items: selectedItems.value.map((i) => ({ items: selectedItems.value.map((i) => ({
itemCode: i.itemCode, itemCode: i.itemCode,
qty: i.qty, qty: i.qty,
rate: i.rate,
discountAmount: i.discountAmount, discountAmount: i.discountAmount,
discountPercentage: i.discountPercentage discountPercentage: i.discountPercentage
})), })),
@ -844,38 +836,15 @@ const onTabClick = () => {
console.log('Set showDrawer to true'); 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(() => { const totalCost = computed(() => {
return (selectedItems.value || []).reduce((sum, item) => { return (selectedItems.value || []).reduce((sum, item) => {
const qty = item.qty || 0; const qty = item.qty || 0;
const rate = item.standardRate || 0; const rate = item.rate || 0;
const discount = item.discountAmount || 0; const discount = item.discountAmount || 0;
return sum + (qty * rate) - discount; return sum + (qty * rate) - discount;
}, 0); }, 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( watch(
() => formData.contact, () => formData.contact,
(newVal) => { (newVal) => {
@ -883,6 +852,10 @@ watch(
}, },
); );
watch(() => formData.projectTemplate, async (newValue) => {
quotationItems.value = await Api.getQuotationItems(newValue);
})
watch(() => company.currentCompany, () => { watch(() => company.currentCompany, () => {
if (isNew.value) { if (isNew.value) {
fetchTemplates(); fetchTemplates();
@ -1023,11 +996,11 @@ watch(
onMounted(async () => { onMounted(async () => {
console.log("DEBUG: Query params:", route.query); console.log("DEBUG: Query params:", route.query);
try { // try {
quotationItems.value = await Api.getQuotationItems(); // quotationItems.value = await Api.getQuotationItems(selectedTemplate.value);
} catch (error) { // } catch (error) {
console.error("Error loading quotation items:", error); // console.error("Error loading quotation items:", error);
} // }
await fetchProjectTemplates(); await fetchProjectTemplates();
if (isNew.value) { if (isNew.value) {
@ -1063,6 +1036,7 @@ onMounted(async () => {
// Handle project-template query parameter // Handle project-template query parameter
if (projectTemplateQuery.value) { if (projectTemplateQuery.value) {
formData.projectTemplate = projectTemplateQuery.value; formData.projectTemplate = projectTemplateQuery.value;
quotationItems.value = await Api.getQuotationItems(projectTemplateQuery.value);
} }
if (addressQuery.value && isNew.value) { if (addressQuery.value && isNew.value) {
@ -1138,7 +1112,7 @@ onMounted(async () => {
<style scoped> <style scoped>
.estimate-page { .estimate-page {
max-width: 800px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
} }
@ -1238,7 +1212,7 @@ onMounted(async () => {
.item-row { .item-row {
display: grid; 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; align-items: center;
gap: 1rem; gap: 1rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -1262,6 +1236,16 @@ onMounted(async () => {
padding: 0; padding: 0;
} }
.rate-input {
width: 100%;
}
.rate-input :deep(.p-inputtext) {
width: 100%;
padding: 0.5rem;
text-align: right;
}
.discount-container { .discount-container {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1294,7 +1278,7 @@ onMounted(async () => {
/* When viewing (not editing), adjust grid to remove delete button column */ /* When viewing (not editing), adjust grid to remove delete button column */
.estimate-page:has(h2:contains("View")) .item-row { .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 { .total-section {
@ -1316,45 +1300,6 @@ onMounted(async () => {
overflow-y: auto; 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 { .confirmation-buttons {
display: flex; display: flex;
gap: 1rem; gap: 1rem;