job creation working
This commit is contained in:
parent
bd9e00c6f1
commit
d3818d1985
22 changed files with 591 additions and 179 deletions
|
|
@ -394,7 +394,11 @@ class Api {
|
|||
static async getTaskStatusOptions() {
|
||||
console.log("DEBUG: API - Loading Task Status options form the backend.");
|
||||
const result = await this.request(FRAPPE_GET_TASKS_STATUS_OPTIONS, {});
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
static async setTaskStatus(taskName, newStatus) {
|
||||
return await this.request(FRAPPE_SET_TASK_STATUS_METHOD, { taskName, newStatus });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
:id="`isBilling-${index}`"
|
||||
v-model="address.isBillingAddress"
|
||||
:disabled="isSubmitting"
|
||||
@change="handleBillingChange(index)"
|
||||
style="margin-top: 0"
|
||||
/>
|
||||
<label :for="`isBilling-${index}`"><i class="pi pi-dollar" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Is Billing Address</label>
|
||||
|
|
@ -251,6 +252,36 @@ const formatAddressLine = (index, field, event) => {
|
|||
localFormData.value.addresses[index][field] = formatted;
|
||||
};
|
||||
|
||||
const handleBillingChange = (selectedIndex) => {
|
||||
// If the selected address is now checked as billing
|
||||
if (localFormData.value.addresses[selectedIndex].isBillingAddress) {
|
||||
// Uncheck all other addresses
|
||||
localFormData.value.addresses.forEach((addr, idx) => {
|
||||
if (idx !== selectedIndex) {
|
||||
addr.isBillingAddress = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-select all contacts
|
||||
if (contactOptions.value.length > 0) {
|
||||
localFormData.value.addresses[selectedIndex].contacts = contactOptions.value.map(
|
||||
(opt) => opt.value,
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-select primary contact
|
||||
if (localFormData.value.contacts && localFormData.value.contacts.length > 0) {
|
||||
const primaryIndex = localFormData.value.contacts.findIndex((c) => c.isPrimary);
|
||||
if (primaryIndex !== -1) {
|
||||
localFormData.value.addresses[selectedIndex].primaryContact = primaryIndex;
|
||||
} else {
|
||||
// Fallback to first contact if no primary found
|
||||
localFormData.value.addresses[selectedIndex].primaryContact = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleZipcodeInput = async (index, event) => {
|
||||
const input = event.target.value;
|
||||
|
||||
|
|
@ -299,7 +330,7 @@ const handleZipcodeInput = async (index, event) => {
|
|||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
|
@ -313,7 +344,7 @@ const handleZipcodeInput = async (index, event) => {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ defineExpose({
|
|||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
|
@ -268,7 +268,7 @@ defineExpose({
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ defineExpose({});
|
|||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
|
@ -299,7 +299,7 @@ defineExpose({});
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1074,8 +1074,9 @@ const handleCancel = () => {
|
|||
|
||||
.status-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
|
|
|
|||
|
|
@ -50,19 +50,30 @@
|
|||
placeholder="Select a contact"
|
||||
:disabled="!formData.address || !isEditable"
|
||||
fluid
|
||||
/>
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="contact-option">
|
||||
<div class="contact-name">{{ slotProps.option.label }}</div>
|
||||
<div class="contact-detail" v-if="slotProps.option.email">{{ slotProps.option.email }}</div>
|
||||
<div class="contact-detail" v-if="slotProps.option.phone">{{ slotProps.option.phone }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
<div v-if="selectedContact" class="verification-info">
|
||||
<strong>Email:</strong> {{ selectedContact.emailId || "N/A" }} <br />
|
||||
<strong>Phone:</strong> {{ selectedContact.phone || "N/A" }} <br />
|
||||
<strong>Primary Contact:</strong>
|
||||
{{ selectedContact.isPrimaryContact ? "Yes" : "No" }}
|
||||
{{ selectedAddress?.primaryContact === selectedContact.name ? "Yes" : "No" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Section -->
|
||||
<div class="template-section">
|
||||
<div v-if="isNew">
|
||||
<label for="template" class="field-label">From Template</label>
|
||||
<label for="template" class="field-label">
|
||||
From Template
|
||||
<i class="pi pi-question-circle help-icon" v-tooltip.right="'Pre-fills estimate items and sets default Project Template. Serves as a starting point for this estimate.'"></i>
|
||||
</label>
|
||||
<div class="template-input-group">
|
||||
<Select
|
||||
v-model="selectedTemplate"
|
||||
|
|
@ -99,6 +110,7 @@
|
|||
<label for="projectTemplate" class="field-label">
|
||||
Project Template
|
||||
<span class="required">*</span>
|
||||
<i class="pi pi-question-circle help-icon" v-tooltip.right="'Used when generating a Project from this estimate. Defines tasks and default settings for the new Project.'"></i>
|
||||
</label>
|
||||
<Select
|
||||
v-model="formData.projectTemplate"
|
||||
|
|
@ -122,57 +134,63 @@
|
|||
/>
|
||||
<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="onQtyChange(item)"
|
||||
class="qty-input"
|
||||
/>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Quantity</span>
|
||||
<InputNumber
|
||||
v-model="item.qty"
|
||||
:min="1"
|
||||
:disabled="!isEditable"
|
||||
showButtons
|
||||
buttonLayout="horizontal"
|
||||
@input="onQtyChange(item)"
|
||||
class="qty-input"
|
||||
/>
|
||||
</div>
|
||||
<span>Price: ${{ (item.standardRate || 0).toFixed(2) }}</span>
|
||||
<div class="discount-container">
|
||||
<div class="discount-input-wrapper">
|
||||
<InputNumber
|
||||
v-if="item.discountType === 'currency'"
|
||||
v-model="item.discountAmount"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromAmount(item)"
|
||||
placeholder="$0.00"
|
||||
class="discount-input"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else
|
||||
v-model="item.discountPercentage"
|
||||
suffix="%"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromPercentage(item)"
|
||||
placeholder="0%"
|
||||
class="discount-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="discount-toggle">
|
||||
<Button
|
||||
icon="pi pi-dollar"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'currency' }"
|
||||
@click="toggleDiscountType(item, 'currency')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-percentage"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'percentage' }"
|
||||
@click="toggleDiscountType(item, 'percentage')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Discount</span>
|
||||
<div class="discount-container">
|
||||
<div class="discount-input-wrapper">
|
||||
<InputNumber
|
||||
v-if="item.discountType === 'currency'"
|
||||
v-model="item.discountAmount"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromAmount(item)"
|
||||
placeholder="$0.00"
|
||||
class="discount-input"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else
|
||||
v-model="item.discountPercentage"
|
||||
suffix="%"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromPercentage(item)"
|
||||
placeholder="0%"
|
||||
class="discount-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="discount-toggle">
|
||||
<Button
|
||||
icon="pi pi-dollar"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'currency' }"
|
||||
@click="toggleDiscountType(item, 'currency')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-percentage"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'percentage' }"
|
||||
@click="toggleDiscountType(item, 'percentage')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>Total: ${{ ((item.qty || 0) * (item.standardRate || 0) - (item.discountAmount || 0)).toFixed(2) }}</span>
|
||||
|
|
@ -202,7 +220,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div v-if="estimate">
|
||||
<Button label="Send Estimate" @click="showConfirmationModal = true" :disabled="estimate.customSent === 1"/>
|
||||
<Button label="Send Estimate" @click="initiateSendEstimate" :disabled="estimate.customSent === 1"/>
|
||||
</div>
|
||||
<div v-if="estimate && estimate.customSent === 1" class="response-status">
|
||||
<h4>Customer Response:</h4>
|
||||
|
|
@ -296,6 +314,27 @@
|
|||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Down Payment Warning Modal -->
|
||||
<Modal
|
||||
:visible="showDownPaymentWarningModal"
|
||||
@update:visible="showDownPaymentWarningModal = $event"
|
||||
@close="showDownPaymentWarningModal = false"
|
||||
:options="{ showActions: false }"
|
||||
>
|
||||
<template #title>Warning</template>
|
||||
<div class="modal-content">
|
||||
<p>Down payment is not required for this estimate. Ok to proceed?</p>
|
||||
<div class="confirmation-buttons">
|
||||
<Button
|
||||
label="No"
|
||||
@click="showDownPaymentWarningModal = false"
|
||||
severity="secondary"
|
||||
/>
|
||||
<Button label="Yes" @click="proceedFromWarning" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<Modal
|
||||
:visible="showConfirmationModal"
|
||||
|
|
@ -356,6 +395,7 @@ import InputText from "primevue/inputtext";
|
|||
import InputNumber from "primevue/inputnumber";
|
||||
import Button from "primevue/button";
|
||||
import Select from "primevue/select";
|
||||
import Tooltip from "primevue/tooltip";
|
||||
import Api from "../../api";
|
||||
import DataUtils from "../../utils";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
|
|
@ -367,6 +407,7 @@ const router = useRouter();
|
|||
const loadingStore = useLoadingStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const company = useCompanyStore();
|
||||
const vTooltip = Tooltip;
|
||||
|
||||
const addressQuery = computed(() => route.query.address || "");
|
||||
const nameQuery = computed(() => route.query.name || "");
|
||||
|
|
@ -406,6 +447,7 @@ const selectedTemplate = ref(null);
|
|||
const showAddressModal = ref(false);
|
||||
const showAddItemModal = ref(false);
|
||||
const showConfirmationModal = ref(false);
|
||||
const showDownPaymentWarningModal = ref(false);
|
||||
const showResponseModal = ref(false);
|
||||
const showSaveTemplateModal = ref(false);
|
||||
const addressSearchResults = ref([]);
|
||||
|
|
@ -459,10 +501,20 @@ const fetchTemplates = async () => {
|
|||
const templateParam = route.query.template;
|
||||
if (templateParam) {
|
||||
console.log("DEBUG: Setting template from query param:", templateParam);
|
||||
console.log("DEBUG: Available templates:", templates.value.map(t => t.name));
|
||||
selectedTemplate.value = templateParam;
|
||||
// Trigger template change to load items and project template
|
||||
onTemplateChange();
|
||||
|
||||
// Find template by name (ID) or templateName (Label)
|
||||
const matchedTemplate = templates.value.find(t =>
|
||||
t.name === templateParam || t.templateName === templateParam
|
||||
);
|
||||
|
||||
if (matchedTemplate) {
|
||||
console.log("DEBUG: Found matched template:", matchedTemplate);
|
||||
selectedTemplate.value = matchedTemplate.name;
|
||||
// Trigger template change to load items and project template
|
||||
onTemplateChange();
|
||||
} else {
|
||||
console.log("DEBUG: No matching template found for param:", templateParam);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates:", error);
|
||||
|
|
@ -580,8 +632,10 @@ const selectAddress = async (address) => {
|
|||
contactOptions.value = contacts.value.map((c) => ({
|
||||
label: `${c.firstName || ""} ${c.lastName || ""}`.trim() || c.name,
|
||||
value: c.name,
|
||||
email: c.emailId,
|
||||
phone: c.phone || c.mobileNo
|
||||
}));
|
||||
const primary = contacts.value.find((c) => c.isPrimaryContact);
|
||||
const primary = contacts.value.find((c) => c.name === selectedAddress.value.primaryContact);
|
||||
console.log("DEBUG: Selected address contacts:", contacts.value);
|
||||
const existingContactName = estimate.value ? contacts.value.find((c) => c.fullName === estimate.value.partyName)?.name || "" : null;
|
||||
// Check for contact query param, then existing contact, then primary, then first contact
|
||||
|
|
@ -721,6 +775,19 @@ const getResponseText = (response) => {
|
|||
return "No response yet";
|
||||
};
|
||||
|
||||
const initiateSendEstimate = () => {
|
||||
if (!formData.requiresHalfPayment) {
|
||||
showDownPaymentWarningModal.value = true;
|
||||
} else {
|
||||
showConfirmationModal.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const proceedFromWarning = () => {
|
||||
showDownPaymentWarningModal.value = false;
|
||||
showConfirmationModal.value = true;
|
||||
};
|
||||
|
||||
const confirmAndSendEstimate = async () => {
|
||||
loadingStore.setLoading(true, "Sending estimate...");
|
||||
const updatedEstimate = await Api.sendEstimateEmail(estimate.value.name);
|
||||
|
|
@ -1265,5 +1332,38 @@ onMounted(async () => {
|
|||
.field-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #2196f3;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.contact-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contact-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.contact-detail {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
<parameter name="filePath"></parameter>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
<DataTable
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:tableActions="tableActions"
|
||||
tableName="jobtasks"
|
||||
:lazy="true"
|
||||
:totalRecords="totalRecords"
|
||||
|
|
@ -80,9 +81,49 @@ const columns = [
|
|||
{ label: "ID", fieldName: "id", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Address", fieldname: "address", type: "text" },
|
||||
{ label: "Category", fieldName: "", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Status", fieldName: "status", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Status", fieldName: "status", type: "status", sortable: true, filterable: true },
|
||||
];
|
||||
|
||||
const statusOptions = ref([
|
||||
"Open",
|
||||
"Working",
|
||||
"Pending Review",
|
||||
"Overdue",
|
||||
"Completed",
|
||||
"Cancelled",
|
||||
]);
|
||||
|
||||
const tableActions = computed(() => [
|
||||
{
|
||||
label: "Set Status",
|
||||
rowAction: true,
|
||||
type: "menu",
|
||||
menuItems: statusOptions.value.map((option) => ({
|
||||
label: option,
|
||||
command: async (rowData) => {
|
||||
console.log("Setting status for row:", rowData, "to:", option);
|
||||
try {
|
||||
await Api.setTaskStatus(rowData.id, option);
|
||||
|
||||
// Find and update the row in the table data
|
||||
const rowIndex = tableData.value.findIndex((row) => row.id === rowData.id);
|
||||
if (rowIndex >= 0) {
|
||||
// Update reactively
|
||||
tableData.value[rowIndex].status = option;
|
||||
notifications.addSuccess(`Status updated to ${option}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
notifications.addError("Failed to update status");
|
||||
}
|
||||
},
|
||||
})),
|
||||
layout: {
|
||||
priority: "menu",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const handleLazyLoad = async (event) => {
|
||||
console.log("Task list on Job Page - handling lazy load:", event);
|
||||
try {
|
||||
|
|
@ -191,6 +232,16 @@ const handleLazyLoad = async (event) => {
|
|||
|
||||
onMounted(async () => {
|
||||
console.log("DEBUG: Query params:", route.query);
|
||||
|
||||
try {
|
||||
const optionsResult = await Api.getTaskStatusOptions();
|
||||
if (optionsResult && optionsResult.length > 0) {
|
||||
statusOptions.value = optionsResult;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading task status options:", error);
|
||||
}
|
||||
|
||||
if (jobIdQuery.value) {
|
||||
// Viewing existing Job
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ const showCompleted = ref(false);
|
|||
const statusOptions = ref([
|
||||
"Open",
|
||||
"Working",
|
||||
"Pending",
|
||||
"Review",
|
||||
"Pending Review",
|
||||
"Overdue",
|
||||
"Completed",
|
||||
"Cancelled",
|
||||
|
|
@ -92,7 +91,7 @@ const tableActions = [
|
|||
console.log("Setting status for row:", rowData, "to:", option);
|
||||
try {
|
||||
// Uncomment when API is ready
|
||||
// await Api.setTaskStatus(rowData.id, option);
|
||||
await Api.setTaskStatus(rowData.id, option);
|
||||
|
||||
// Find and update the row in the table data
|
||||
const rowIndex = tableData.value.findIndex(row => row.id === rowData.id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue