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"
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;