978 lines
26 KiB
Vue
978 lines
26 KiB
Vue
<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>
|