update with main
This commit is contained in:
parent
5e192a61e1
commit
ba3e2a4d8e
29 changed files with 51749 additions and 139 deletions
978
frontend/src/components/modals/BidMeetingNoteForm.vue
Normal file
978
frontend/src/components/modals/BidMeetingNoteForm.vue
Normal file
|
|
@ -0,0 +1,978 @@
|
|||
<template>
|
||||
<Modal
|
||||
:visible="showModal"
|
||||
@update:visible="showModal = $event"
|
||||
:options="modalOptions"
|
||||
@confirm="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<template #title>
|
||||
<div class="modal-header">
|
||||
<i class="pi pi-file-edit" style="color: var(--primary-color); font-size: 1.2rem; margin-right: 0.5rem;"></i>
|
||||
{{ formTitle }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<ProgressSpinner style="width: 50px; height: 50px" strokeWidth="4" />
|
||||
<p>Loading form...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="formConfig" class="form-container">
|
||||
<Button @click="debugLog" label="Debug" severity="secondary" size="small" class="debug-button" />
|
||||
<template v-for="row in groupedFields" :key="`row-${row.rowIndex}`">
|
||||
<div class="form-row">
|
||||
<div
|
||||
v-for="field in row.fields"
|
||||
:key="field.name"
|
||||
:class="`form-column-${Math.min(Math.max(field.columns || 12, 1), 12)}`"
|
||||
>
|
||||
<div class="form-field">
|
||||
<!-- Field Label -->
|
||||
<label :for="field.name" class="field-label">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="required-indicator">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Help Text -->
|
||||
<small v-if="field.helpText" class="field-help-text">
|
||||
{{ field.helpText }}
|
||||
</small>
|
||||
|
||||
<!-- Data/Text Field -->
|
||||
<template v-if="field.type === 'Data' || field.type === 'Text'">
|
||||
<InputText
|
||||
v-if="field.type === 'Data'"
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="field.label"
|
||||
class="w-full"
|
||||
/>
|
||||
<Textarea
|
||||
v-else
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="field.label"
|
||||
rows="3"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Check Field -->
|
||||
<template v-else-if="field.type === 'Check'">
|
||||
<div class="checkbox-container">
|
||||
<Checkbox
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:binary="true"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
/>
|
||||
<label :for="field.name" class="checkbox-label">
|
||||
{{ formData[field.name].value ? 'Yes' : 'No' }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Date Field -->
|
||||
<template v-else-if="field.type === 'Date'">
|
||||
<DatePicker
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
dateFormat="yy-mm-dd"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Datetime Field -->
|
||||
<template v-else-if="field.type === 'Datetime'">
|
||||
<DatePicker
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
showTime
|
||||
hourFormat="12"
|
||||
dateFormat="yy-mm-dd"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Time Field -->
|
||||
<template v-else-if="field.type === 'Time'">
|
||||
<DatePicker
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
timeOnly
|
||||
hourFormat="12"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Number Field -->
|
||||
<template v-else-if="field.type === 'Number'">
|
||||
<InputNumber
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Select Field -->
|
||||
<template v-else-if="field.type === 'Select'">
|
||||
<div @click="console.log('Select wrapper clicked:', field.name, 'disabled:', field.readOnly || !isFieldVisible(field), 'options:', optionsForFields[field.name])">
|
||||
<Select
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:options="optionsForFields[field.name]"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="`Select ${field.label}`"
|
||||
:optionLabel="'label'"
|
||||
:optionValue="'value'"
|
||||
:editable="false"
|
||||
:showClear="true"
|
||||
:baseZIndex="10000"
|
||||
@click.native="console.log('Select native click:', field.name)"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Multi-Select Field -->
|
||||
<template v-else-if="field.type === 'Multi-Select'">
|
||||
<MultiSelect
|
||||
:id="field.name"
|
||||
v-model="formData[field.name].value"
|
||||
:options="optionsForFields[field.name]"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="`Select ${field.label}`"
|
||||
:key="`${field.name}-${field.readOnly || !isFieldVisible(field)}`"
|
||||
:optionLabel="'label'"
|
||||
:optionValue="'value'"
|
||||
:showClear="true"
|
||||
:baseZIndex="9999"
|
||||
display="chip"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Multi-Select w/ Quantity Field -->
|
||||
<template v-else-if="field.type === 'Multi-Select w/ Quantity'">
|
||||
<div class="multi-select-quantity-container">
|
||||
<!-- Item Selector -->
|
||||
<div class="item-selector">
|
||||
<Select
|
||||
v-model="currentItemSelection[field.name]"
|
||||
:options="optionsForFields[field.name]"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
:placeholder="`Add ${field.label}`"
|
||||
:key="`${field.name}-${field.readOnly || !isFieldVisible(field)}`"
|
||||
:optionLabel="'label'"
|
||||
:showClear="true"
|
||||
:baseZIndex="9999"
|
||||
class="w-full"
|
||||
@change="addItemToQuantityList(field)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Selected Items with Quantities -->
|
||||
<div v-if="formData[field.name].value && formData[field.name].value.length > 0" class="selected-items-list">
|
||||
<div
|
||||
v-for="(item, index) in formData[field.name].value"
|
||||
:key="index"
|
||||
class="quantity-item"
|
||||
>
|
||||
<div class="item-name">{{ getOptionLabel(field, item) }}</div>
|
||||
<div class="quantity-controls">
|
||||
<InputNumber
|
||||
v-model="item.quantity"
|
||||
:min="1"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
showButtons
|
||||
buttonLayout="horizontal"
|
||||
:step="1"
|
||||
decrementButtonClass="p-button-secondary"
|
||||
incrementButtonClass="p-button-secondary"
|
||||
incrementButtonIcon="pi pi-plus"
|
||||
decrementButtonIcon="pi pi-minus"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
@click="removeItemFromQuantityList(field, index)"
|
||||
:disabled="field.readOnly || !isFieldVisible(field)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else class="error-container">
|
||||
<i class="pi pi-exclamation-triangle" style="font-size: 2rem; color: var(--red-500);"></i>
|
||||
<p>Failed to load form configuration</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, reactive } from "vue";
|
||||
import Modal from "../common/Modal.vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Textarea from "primevue/textarea";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
import DatePicker from "primevue/datepicker";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import Select from "primevue/select";
|
||||
import MultiSelect from "primevue/multiselect";
|
||||
import Button from "primevue/button";
|
||||
import ProgressSpinner from "primevue/progressspinner";
|
||||
import Api from "../../api";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
|
||||
const docsForSelectFields = ref({});
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
bidMeetingName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectTemplate: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "submit", "cancel"]);
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const showModal = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit("update:visible", value),
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
const formConfig = ref(null);
|
||||
const formData = ref({}); // Will store fieldName: {fieldConfig, value}
|
||||
const currentItemSelection = ref({}); // For tracking current selection in Multi-Select w/ Quantity
|
||||
const doctypeOptions = ref({}); // Cache for doctype options
|
||||
|
||||
const formTitle = computed(() => {
|
||||
return formConfig.value?.title || "Bid Meeting Notes";
|
||||
});
|
||||
|
||||
// Include all fields from config plus a general notes field
|
||||
const allFields = computed(() => {
|
||||
if (!formConfig.value) return [];
|
||||
|
||||
const fields = [...(formConfig.value.fields || [])];
|
||||
|
||||
// Always add a general notes field at the end
|
||||
const generalNotesField = {
|
||||
name: 'general_notes',
|
||||
label: 'General Notes',
|
||||
type: 'Text',
|
||||
required: 0,
|
||||
readOnly: 0,
|
||||
helpText: 'Any additional notes or observations from the meeting',
|
||||
row: Math.max(...fields.map(f => f.row || 1), 0) + 1,
|
||||
columns: 12,
|
||||
};
|
||||
|
||||
fields.push(generalNotesField);
|
||||
return fields;
|
||||
});
|
||||
|
||||
// Group fields by row for grid layout
|
||||
const groupedFields = computed(() => {
|
||||
const groups = {};
|
||||
|
||||
allFields.value.forEach(field => {
|
||||
const rowNum = field.row || 1;
|
||||
if (!groups[rowNum]) {
|
||||
groups[rowNum] = { rowIndex: rowNum, fields: [] };
|
||||
}
|
||||
groups[rowNum].fields.push(field);
|
||||
});
|
||||
|
||||
// Sort fields by column and set columns span
|
||||
Object.values(groups).forEach(group => {
|
||||
group.fields.sort((a, b) => (a.column || 0) - (b.column || 0));
|
||||
const numFields = group.fields.length;
|
||||
const span = Math.floor(12 / numFields);
|
||||
group.fields.forEach(field => {
|
||||
field.columns = span;
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(groups).sort((a, b) => a.rowIndex - b.rowIndex);
|
||||
});
|
||||
|
||||
// Update field value in reactive form data
|
||||
const updateFieldValue = (fieldName, value) => {
|
||||
if (formData.value[fieldName]) {
|
||||
formData.value[fieldName].value = value;
|
||||
}
|
||||
};
|
||||
|
||||
// Get CSS class for field column span
|
||||
const getFieldColumnClass = (field) => {
|
||||
const columns = field.columns || 12; // Default to full width if not specified
|
||||
return `form-column-${Math.min(Math.max(columns, 1), 12)}`; // Ensure between 1-12
|
||||
};
|
||||
|
||||
const modalOptions = computed(() => ({
|
||||
maxWidth: "800px",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Submit",
|
||||
cancelButtonText: "Cancel",
|
||||
confirmButtonColor: "primary",
|
||||
zIndex: 1000, // Lower than select baseZIndex
|
||||
}));
|
||||
|
||||
// Helper to find field name by label
|
||||
const findFieldNameByLabel = (label) => {
|
||||
if (!formConfig.value || !formConfig.value.fields) return null;
|
||||
const field = formConfig.value.fields.find(f => f.label === label);
|
||||
return field ? field.name : null;
|
||||
};
|
||||
|
||||
const fetchDocsForSelectField = async (doctype, fieldName) => {
|
||||
const docs = await Api.getDocsList(doctype);
|
||||
docsForSelectFields[fieldName] = docs;
|
||||
}
|
||||
|
||||
// Check if a field should be visible based on conditional logic
|
||||
const isFieldVisible = (field) => {
|
||||
if (!field.conditionalOnField) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find the actual field name from the label (conditionalOnField contains the label)
|
||||
const dependentFieldName = findFieldNameByLabel(field.conditionalOnField);
|
||||
if (!dependentFieldName) {
|
||||
console.warn(`Could not find field with label: ${field.conditionalOnField}`);
|
||||
return true; // Show field if we can't find the dependency
|
||||
}
|
||||
|
||||
const dependentFieldValue = formData.value[dependentFieldName]?.value;
|
||||
|
||||
console.log(`Checking visibility for ${field.label}:`, {
|
||||
conditionalOnField: field.conditionalOnField,
|
||||
dependentFieldName,
|
||||
dependentFieldValue,
|
||||
conditionalOnValue: field.conditionalOnValue,
|
||||
});
|
||||
|
||||
// If the dependent field is a checkbox, it should be true
|
||||
if (typeof dependentFieldValue === "boolean") {
|
||||
return dependentFieldValue === true;
|
||||
}
|
||||
|
||||
// If conditional_on_value is specified, check for exact match
|
||||
if (field.conditionalOnValue !== null && field.conditionalOnValue !== undefined) {
|
||||
return dependentFieldValue === field.conditionalOnValue;
|
||||
}
|
||||
|
||||
// Otherwise, just check if the dependent field has any truthy value
|
||||
return !!dependentFieldValue;
|
||||
};
|
||||
|
||||
// Get options for select/multi-select fields
|
||||
const getFieldOptions = (field) => {
|
||||
// Access reactive data to ensure reactivity
|
||||
const optionsData = docsForSelectFields.value[field.name];
|
||||
|
||||
console.log(`getFieldOptions called for ${field.label}:`, {
|
||||
type: field.type,
|
||||
options: field.options,
|
||||
optionsType: typeof field.options,
|
||||
doctypeForSelect: field.doctypeForSelect,
|
||||
doctypeLabelField: field.doctypeLabelField,
|
||||
hasDoctypeOptions: !!optionsData,
|
||||
});
|
||||
|
||||
// If options should be fetched from a doctype
|
||||
if (field.doctypeForSelect && optionsData) {
|
||||
console.log(`Using doctype options for ${field.label}:`, optionsData);
|
||||
return [...optionsData]; // Return a copy to ensure reactivity
|
||||
}
|
||||
|
||||
// If options are provided as a string (comma-separated), parse them
|
||||
if (field.options && typeof field.options === "string" && field.options.trim() !== "") {
|
||||
const optionStrings = field.options.split(",").map((opt) => opt.trim()).filter(opt => opt !== "");
|
||||
// Convert to objects for consistency with PrimeVue MultiSelect
|
||||
const options = optionStrings.map((opt) => ({
|
||||
label: opt,
|
||||
value: opt,
|
||||
}));
|
||||
console.log(`Parsed options for ${field.label}:`, options);
|
||||
return options;
|
||||
}
|
||||
|
||||
console.warn(`No options found for ${field.label}`);
|
||||
return [];
|
||||
};
|
||||
|
||||
// Get options for select/multi-select fields
|
||||
const optionsForFields = computed(() => {
|
||||
// Ensure reactivity by accessing docsForSelectFields
|
||||
const docsData = docsForSelectFields.value;
|
||||
const opts = {};
|
||||
allFields.value.forEach(field => {
|
||||
const options = getFieldOptions(field);
|
||||
opts[field.name] = options;
|
||||
console.log(`Computed options for ${field.name}:`, options);
|
||||
});
|
||||
console.log('optionsForFields computed:', opts);
|
||||
return opts;
|
||||
});
|
||||
|
||||
// Add item to quantity list for Multi-Select w/ Quantity fields
|
||||
const addItemToQuantityList = (field) => {
|
||||
const selectedItem = currentItemSelection.value[field.name];
|
||||
if (!selectedItem) return;
|
||||
|
||||
// selectedItem is now an object with { label, value }
|
||||
const itemValue = selectedItem.value || selectedItem;
|
||||
const itemLabel = selectedItem.label || selectedItem;
|
||||
|
||||
// Initialize array if it doesn't exist
|
||||
const fieldData = formData.value[field.name];
|
||||
if (!fieldData.value) {
|
||||
fieldData.value = [];
|
||||
}
|
||||
|
||||
// Check if item already exists (compare by value)
|
||||
const existingItem = fieldData.value.find((item) => item.item === itemValue);
|
||||
if (existingItem) {
|
||||
// Increment quantity if item already exists
|
||||
existingItem.quantity += 1;
|
||||
} else {
|
||||
// Add new item with quantity 1
|
||||
fieldData.value.push({
|
||||
item: itemValue,
|
||||
label: itemLabel,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
currentItemSelection.value[field.name] = null;
|
||||
};
|
||||
|
||||
// Remove item from quantity list
|
||||
const removeItemFromQuantityList = (field, index) => {
|
||||
const fieldData = formData.value[field.name];
|
||||
if (fieldData && fieldData.value) {
|
||||
fieldData.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Get option label for display
|
||||
const getOptionLabel = (field, item) => {
|
||||
const options = optionsForFields.value[field.name];
|
||||
const option = options.find(o => o.value === item.item);
|
||||
return option ? option.label : item.label || item.item;
|
||||
};
|
||||
|
||||
// Initialize form data with default values
|
||||
const initializeFormData = () => {
|
||||
if (!formConfig.value) return;
|
||||
|
||||
const data = {};
|
||||
allFields.value.forEach((field) => {
|
||||
// Create reactive object with field config and value
|
||||
data[field.name] = {
|
||||
...field, // Include all field configuration
|
||||
value: null, // Initialize value
|
||||
};
|
||||
|
||||
// Set default value if provided
|
||||
if (field.defaultValue !== null && field.defaultValue !== undefined) {
|
||||
data[field.name].value = field.defaultValue;
|
||||
} else {
|
||||
// Initialize based on field type
|
||||
switch (field.type) {
|
||||
case "Check":
|
||||
data[field.name].value = false;
|
||||
break;
|
||||
case "Multi-Select":
|
||||
data[field.name].value = [];
|
||||
break;
|
||||
case "Multi-Select w/ Quantity":
|
||||
data[field.name].value = [];
|
||||
break;
|
||||
case "Number":
|
||||
data[field.name].value = null;
|
||||
break;
|
||||
case "Select":
|
||||
data[field.name].value = null;
|
||||
break;
|
||||
default:
|
||||
data[field.name].value = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
formData.value = data;
|
||||
};
|
||||
|
||||
// Load form configuration
|
||||
const loadFormConfig = async () => {
|
||||
if (!props.projectTemplate) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const config = await Api.getBidMeetingNoteForm(props.projectTemplate);
|
||||
formConfig.value = config;
|
||||
console.log("Loaded form config:", config);
|
||||
|
||||
// Load doctype options for fields that need them
|
||||
await loadDoctypeOptions();
|
||||
|
||||
// Initialize form data
|
||||
initializeFormData();
|
||||
} catch (error) {
|
||||
console.error("Error loading form config:", error);
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: "Failed to load form configuration",
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load options for fields that reference doctypes
|
||||
const loadDoctypeOptions = async () => {
|
||||
if (!formConfig.value || !formConfig.value.fields) return;
|
||||
|
||||
const fieldsWithDoctype = formConfig.value.fields.filter(
|
||||
(field) => field.doctypeForSelect && field.doctypeForSelect !== ""
|
||||
);
|
||||
|
||||
for (const field of fieldsWithDoctype) {
|
||||
try {
|
||||
// Use the new API method for fetching docs
|
||||
let docs = await Api.getEstimateItems();
|
||||
|
||||
// Deduplicate by value field
|
||||
const valueField = field.doctypeValueField || 'name';
|
||||
const seen = new Set();
|
||||
docs = docs.filter(doc => {
|
||||
const val = doc[valueField] || doc.name || doc;
|
||||
if (seen.has(val)) return false;
|
||||
seen.add(val);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Transform docs into options format
|
||||
// Use doctypeLabelField if specified, otherwise default to 'name'
|
||||
const labelField = field.doctypeLabelField || 'name';
|
||||
|
||||
const options = docs.map((doc) => ({
|
||||
label: doc[labelField] || doc.name || doc,
|
||||
value: doc[valueField] || doc.name || doc,
|
||||
}));
|
||||
|
||||
docsForSelectFields.value[field.name] = options;
|
||||
console.log(`Loaded ${options.length} options for ${field.label} from ${field.doctypeForSelect}`);
|
||||
} catch (error) {
|
||||
console.error(`Error loading options for ${field.doctypeForSelect}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Validate form
|
||||
const validateForm = () => {
|
||||
const errors = [];
|
||||
|
||||
if (!formConfig.value) return errors;
|
||||
|
||||
allFields.value.forEach((field) => {
|
||||
// Only validate if field is visible
|
||||
if (!isFieldVisible(field)) return;
|
||||
|
||||
// Skip required validation for checkboxes (they always have a value: true or false)
|
||||
if (field.type === 'Check') return;
|
||||
|
||||
if (field.required) {
|
||||
const value = formData.value[field.name]?.value;
|
||||
|
||||
if (value === null || value === undefined || value === "") {
|
||||
errors.push(`${field.label} is required`);
|
||||
} else if (Array.isArray(value) && value.length === 0) {
|
||||
errors.push(`${field.label} is required`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
// Format field data for submission
|
||||
const formatFieldData = (field) => {
|
||||
const value = formData.value[field.name]?.value;
|
||||
|
||||
// Include the entire field configuration
|
||||
const fieldData = {
|
||||
...field, // Include all field properties
|
||||
value: value, // Override with current value
|
||||
};
|
||||
|
||||
// Handle options: include unless fetched from doctype
|
||||
if (field.doctypeForSelect) {
|
||||
// Remove options if they were fetched from doctype
|
||||
delete fieldData.options;
|
||||
}
|
||||
|
||||
// For fields with include_options flag, include the selected options
|
||||
if (field.includeOptions) {
|
||||
if (field.type === "Multi-Select") {
|
||||
fieldData.selectedOptions = value || [];
|
||||
} else if (field.type === "Multi-Select w/ Quantity") {
|
||||
fieldData.items = value || [];
|
||||
} else if (field.type === "Select") {
|
||||
fieldData.selectedOption = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Format dates as strings
|
||||
if (field.type === "Date" || field.type === "Datetime" || field.type === "Time") {
|
||||
if (value instanceof Date) {
|
||||
if (field.type === "Date") {
|
||||
fieldData.value = value.toISOString().split("T")[0];
|
||||
} else if (field.type === "Datetime") {
|
||||
fieldData.value = value.toISOString();
|
||||
} else if (field.type === "Time") {
|
||||
fieldData.value = value.toTimeString().split(" ")[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldData;
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async () => {
|
||||
// Validate form
|
||||
const errors = validateForm();
|
||||
if (errors.length > 0) {
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Validation Error",
|
||||
message: errors.join(", "),
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loadingStore.setLoading(true);
|
||||
|
||||
// Format data for submission
|
||||
const submissionData = {
|
||||
bidMeeting: props.bidMeetingName,
|
||||
projectTemplate: props.projectTemplate,
|
||||
formName: formConfig.value?.name || formConfig.value?.title,
|
||||
formTemplate: formConfig.value?.name || formConfig.value?.title,
|
||||
fields: allFields.value
|
||||
.filter((field) => isFieldVisible(field))
|
||||
.map((field) => formatFieldData(field)),
|
||||
};
|
||||
|
||||
console.log("Submitting form data:", submissionData);
|
||||
|
||||
// Submit to API
|
||||
await Api.submitBidMeetingNoteForm(submissionData);
|
||||
|
||||
notificationStore.addNotification({
|
||||
type: "success",
|
||||
title: "Success",
|
||||
message: "Bid meeting notes submitted successfully",
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
emit("submit", submissionData);
|
||||
showModal.value = false;
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
notificationStore.addNotification({
|
||||
type: "error",
|
||||
title: "Error",
|
||||
message: "Failed to submit form. Please try again.",
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
loadingStore.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle cancel
|
||||
const handleCancel = () => {
|
||||
emit("cancel");
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
// Debug function to log current form data and select options
|
||||
const debugLog = () => {
|
||||
console.log("=== FORM DEBUG ===");
|
||||
|
||||
const debugData = {};
|
||||
allFields.value.forEach(field => {
|
||||
const fieldValue = formData.value[field.name]?.value;
|
||||
const fieldOptions = optionsForFields.value[field.name];
|
||||
const isVisible = isFieldVisible(field);
|
||||
const isDisabled = field.readOnly || !isVisible;
|
||||
|
||||
debugData[field.name] = {
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
value: fieldValue,
|
||||
isVisible,
|
||||
isDisabled,
|
||||
...(field.type.includes('Select') ? {
|
||||
options: fieldOptions,
|
||||
optionsCount: fieldOptions?.length || 0
|
||||
} : {}),
|
||||
};
|
||||
});
|
||||
|
||||
console.log("Current Form Data:", debugData);
|
||||
console.log("==================");
|
||||
};
|
||||
|
||||
// Watch for modal visibility changes
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
loadFormConfig();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Load form config on mount if modal is visible
|
||||
onMounted(() => {
|
||||
if (props.visible) {
|
||||
loadFormConfig();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
gap: 1rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.debug-button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Grid Layout Styles */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-column-1 { grid-column: span 1; }
|
||||
.form-column-2 { grid-column: span 2; }
|
||||
.form-column-3 { grid-column: span 3; }
|
||||
.form-column-4 { grid-column: span 4; }
|
||||
.form-column-5 { grid-column: span 5; }
|
||||
.form-column-6 { grid-column: span 6; }
|
||||
.form-column-7 { grid-column: span 7; }
|
||||
.form-column-8 { grid-column: span 8; }
|
||||
.form-column-9 { grid-column: span 9; }
|
||||
.form-column-10 { grid-column: span 10; }
|
||||
.form-column-11 { grid-column: span 11; }
|
||||
.form-column-12 { grid-column: span 12; }
|
||||
|
||||
.form-field-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.form-field :deep(.p-select) {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.form-field :deep(.p-multiselect) {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.required-indicator {
|
||||
color: var(--red-500);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.field-help-text {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.85rem;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.checkbox-container :deep(.p-checkbox) {
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.checkbox-container :deep(.p-checkbox .p-checkbox-box) {
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
border: 1px solid var(--surface-border) !important;
|
||||
border-radius: 3px !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.checkbox-container :deep(.p-checkbox .p-checkbox-input) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
opacity: 0 !important;
|
||||
cursor: pointer !important;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
.checkbox-container :deep(.p-checkbox .p-checkbox-box .p-checkbox-icon) {
|
||||
font-size: 0.875rem !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color-secondary);
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.multi-select-quantity-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--surface-50);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.quantity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--surface-0);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.item-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.form-container {
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.quantity-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue