job creation working

This commit is contained in:
Casey 2026-01-16 09:06:59 -06:00
parent bd9e00c6f1
commit d3818d1985
22 changed files with 591 additions and 179 deletions

View file

@ -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 });
}
// ============================================================================

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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 {

View file

@ -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>

View file

@ -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 {

View file

@ -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);