update estimate page, add stripe script and docker compose for mail server
This commit is contained in:
parent
cacbc764c1
commit
678eb18583
1 changed files with 59 additions and 114 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue