add global loading state, update to use real data for clients table
This commit is contained in:
parent
2cfe7ed8e6
commit
464c62d1e5
12 changed files with 1075 additions and 194 deletions
|
|
@ -14,7 +14,21 @@
|
|||
selectionMode="multiple"
|
||||
metaKeySelection="true"
|
||||
dataKey="id"
|
||||
:loading="loading"
|
||||
:loadingIcon="loadingIcon"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="text-center py-6">
|
||||
<i class="pi pi-info-circle text-4xl text-gray-400 mb-2"></i>
|
||||
<p class="text-gray-500">{{ emptyMessage || "No data available" }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #loading>
|
||||
<div class="text-center py-6">
|
||||
<i class="pi pi-spin pi-spinner text-4xl text-blue-500 mb-2"></i>
|
||||
<p class="text-gray-600">{{ loadingMessage || "Loading data. Please wait..." }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<Column
|
||||
v-for="col in columns"
|
||||
:key="col.fieldName"
|
||||
|
|
@ -28,6 +42,7 @@
|
|||
type="text"
|
||||
@input="handleFilterInput(col.fieldName, filterModel.value, filterCallback)"
|
||||
:placeholder="`Search ${col.label}...`"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="col.type === 'status'" #body="slotProps">
|
||||
|
|
@ -57,8 +72,10 @@ import InputText from "primevue/inputtext";
|
|||
import { ref } from "vue";
|
||||
import { FilterMatchMode } from "@primevue/core";
|
||||
import { useFiltersStore } from "../../stores/filters";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
|
||||
const filtersStore = useFiltersStore();
|
||||
const loadingStore = useLoadingStore();
|
||||
|
||||
const props = defineProps({
|
||||
columns: {
|
||||
|
|
@ -79,10 +96,44 @@ const props = defineProps({
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingMessage: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: "pi pi-spinner pi-spin",
|
||||
},
|
||||
// Auto-connect to global loading store
|
||||
useGlobalLoading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["rowClick"]);
|
||||
|
||||
// Computed loading state that considers both prop and global store
|
||||
const loading = computed(() => {
|
||||
if (props.useGlobalLoading) {
|
||||
return (
|
||||
props.loading ||
|
||||
loadingStore.getComponentLoading("dataTable") ||
|
||||
loadingStore.getComponentLoading(props.tableName) ||
|
||||
loadingStore.isAnyLoading
|
||||
);
|
||||
}
|
||||
return props.loading;
|
||||
});
|
||||
|
||||
// Initialize filters in store when component mounts
|
||||
onMounted(() => {
|
||||
filtersStore.initializeTableFilters(props.tableName, props.columns);
|
||||
|
|
@ -97,39 +148,43 @@ const filterRef = computed({
|
|||
},
|
||||
set(newFilters) {
|
||||
// Update store when filters change
|
||||
Object.keys(newFilters).forEach(key => {
|
||||
if (key !== 'global' && newFilters[key]) {
|
||||
Object.keys(newFilters).forEach((key) => {
|
||||
if (key !== "global" && newFilters[key]) {
|
||||
const filter = newFilters[key];
|
||||
filtersStore.updateTableFilter(
|
||||
props.tableName,
|
||||
key,
|
||||
filter.value,
|
||||
filter.matchMode
|
||||
props.tableName,
|
||||
key,
|
||||
filter.value,
|
||||
filter.matchMode,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Watch for filter changes to sync match mode changes
|
||||
watch(filterRef, (newFilters) => {
|
||||
Object.keys(newFilters).forEach(key => {
|
||||
if (key !== 'global' && newFilters[key]) {
|
||||
const filter = newFilters[key];
|
||||
const storeFilter = filtersStore.getTableFilters(props.tableName)[key];
|
||||
|
||||
// Only update if the match mode has actually changed
|
||||
if (storeFilter && storeFilter.matchMode !== filter.matchMode) {
|
||||
filtersStore.updateTableFilter(
|
||||
props.tableName,
|
||||
key,
|
||||
filter.value,
|
||||
filter.matchMode
|
||||
);
|
||||
watch(
|
||||
filterRef,
|
||||
(newFilters) => {
|
||||
Object.keys(newFilters).forEach((key) => {
|
||||
if (key !== "global" && newFilters[key]) {
|
||||
const filter = newFilters[key];
|
||||
const storeFilter = filtersStore.getTableFilters(props.tableName)[key];
|
||||
|
||||
// Only update if the match mode has actually changed
|
||||
if (storeFilter && storeFilter.matchMode !== filter.matchMode) {
|
||||
filtersStore.updateTableFilter(
|
||||
props.tableName,
|
||||
key,
|
||||
filter.value,
|
||||
filter.matchMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { deep: true });
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const selectedRows = ref();
|
||||
|
||||
|
|
@ -138,7 +193,7 @@ const handleFilterInput = (fieldName, value, filterCallback) => {
|
|||
// Get the current filter to preserve the match mode
|
||||
const currentFilter = filterRef.value[fieldName];
|
||||
const matchMode = currentFilter?.matchMode || FilterMatchMode.CONTAINS;
|
||||
|
||||
|
||||
// Update the store with both value and match mode
|
||||
filtersStore.updateTableFilter(props.tableName, fieldName, value, matchMode);
|
||||
// Call the PrimeVue filter callback
|
||||
|
|
@ -160,6 +215,12 @@ const getBadgeColor = (status) => {
|
|||
};
|
||||
console.log("DEBUG: - DataTable props.columns", props.columns);
|
||||
console.log("DEBUG: - DataTable props.data", props.data);
|
||||
|
||||
// Expose loading control methods for parent components
|
||||
defineExpose({
|
||||
startLoading: (message) => loadingStore.setComponentLoading(props.tableName, true, message),
|
||||
stopLoading: () => loadingStore.setComponentLoading(props.tableName, false),
|
||||
isLoading: () => loading.value,
|
||||
});
|
||||
</script>
|
||||
<style lang="">
|
||||
</style>
|
||||
<style lang=""></style>
|
||||
|
|
|
|||
|
|
@ -19,16 +19,15 @@
|
|||
v-model="fieldValues[field.name]"
|
||||
:type="field.format || 'text'"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:readonly="field.readonly"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
:fluid="true"
|
||||
@input="
|
||||
handleFieldChange(
|
||||
field,
|
||||
$event.target ? $event.target.value : $event,
|
||||
)
|
||||
"
|
||||
fluid
|
||||
:maxlength="field.maxLength"
|
||||
:inputmode="field.inputMode"
|
||||
:pattern="field.pattern"
|
||||
@keydown="handleKeyDown(field, $event)"
|
||||
@input="handleTextInput(field, $event)"
|
||||
@blur="
|
||||
handleFieldBlur(
|
||||
field,
|
||||
|
|
@ -59,13 +58,13 @@
|
|||
:id="field.name"
|
||||
v-model="fieldValues[field.name]"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:readonly="field.readonly"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:step="field.step"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
:fluid="true"
|
||||
fluid
|
||||
@input="handleFieldChange(field, $event.value)"
|
||||
@blur="handleFieldBlur(field, fieldValues[field.name])"
|
||||
/>
|
||||
|
|
@ -92,11 +91,11 @@
|
|||
:id="field.name"
|
||||
v-model="fieldValues[field.name]"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:readonly="field.readonly"
|
||||
:rows="field.rows || 3"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
:fluid="true"
|
||||
fluid
|
||||
:autoResize="field.autoResize !== false"
|
||||
@input="
|
||||
handleFieldChange(
|
||||
|
|
@ -134,14 +133,13 @@
|
|||
:id="field.name"
|
||||
v-model="fieldValues[field.name]"
|
||||
:options="field.options"
|
||||
:optionLabel="field.optionLabel || 'label'"
|
||||
:optionValue="field.optionValue || 'value'"
|
||||
:disabled="field.disabled"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:placeholder="field.placeholder"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
:fluid="true"
|
||||
:filter="field.filter !== false"
|
||||
:showClear="field.showClear !== false"
|
||||
fluid
|
||||
appendTo="body"
|
||||
@update:model-value="handleFieldChange(field, $event)"
|
||||
@blur="handleFieldBlur(field, fieldValues[field.name])"
|
||||
/>
|
||||
|
|
@ -168,15 +166,13 @@
|
|||
:id="field.name"
|
||||
v-model="fieldValues[field.name]"
|
||||
:suggestions="field.filteredOptions || field.options || []"
|
||||
:option-label="field.optionLabel"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:placeholder="field.placeholder"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
:fluid="true"
|
||||
:dropdown="field.dropdown !== false"
|
||||
:multiple="field.multiple === true"
|
||||
:force-selection="field.forceSelection === true"
|
||||
dropdown-mode="blank"
|
||||
fluid
|
||||
:dropdown="field.dropdown"
|
||||
:forceSelection="field.forceSelection"
|
||||
appendTo="body"
|
||||
@complete="handleAutocompleteSearch(field, $event)"
|
||||
@update:model-value="handleFieldChange(field, $event)"
|
||||
@blur="handleFieldBlur(field, fieldValues[field.name])"
|
||||
|
|
@ -201,7 +197,7 @@
|
|||
:id="field.name"
|
||||
v-model="fieldValues[field.name]"
|
||||
:binary="true"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
@update:model-value="handleFieldChange(field, $event)"
|
||||
/>
|
||||
|
|
@ -240,7 +236,7 @@
|
|||
v-model="fieldValues[field.name]"
|
||||
:name="field.name"
|
||||
:value="option.value"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
@update:model-value="handleFieldChange(field, $event)"
|
||||
/>
|
||||
|
|
@ -272,13 +268,13 @@
|
|||
:id="field.name"
|
||||
v-model="fieldValues[field.name]"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:readonly="field.readonly"
|
||||
:minDate="field.minDate"
|
||||
:maxDate="field.maxDate"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
:fluid="true"
|
||||
:showIcon="true"
|
||||
fluid
|
||||
showIcon
|
||||
iconDisplay="input"
|
||||
:dateFormat="field.dateFormat || 'dd/mm/yy'"
|
||||
@update:model-value="handleFieldChange(field, $event)"
|
||||
|
|
@ -307,15 +303,15 @@
|
|||
:id="field.name"
|
||||
v-model="fieldValues[field.name]"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:readonly="field.readonly"
|
||||
:minDate="field.minDate"
|
||||
:maxDate="field.maxDate"
|
||||
:invalid="!!getFieldError(field.name)"
|
||||
:fluid="true"
|
||||
:showIcon="true"
|
||||
fluid
|
||||
showIcon
|
||||
iconDisplay="input"
|
||||
:showTime="true"
|
||||
showTime
|
||||
:hourFormat="field.hourFormat || '24'"
|
||||
:dateFormat="field.dateFormat || 'dd/mm/yy'"
|
||||
@update:model-value="handleFieldChange(field, $event)"
|
||||
|
|
@ -344,7 +340,7 @@
|
|||
:id="field.name"
|
||||
v-model="fieldValues[field.name]"
|
||||
mode="basic"
|
||||
:disabled="field.disabled"
|
||||
:disabled="field.disabled || isFormDisabled"
|
||||
:accept="field.accept"
|
||||
:multiple="field.multiple"
|
||||
:invalidFileTypeMessage="`Invalid file type. Accepted types: ${field.accept || 'any'}`"
|
||||
|
|
@ -378,13 +374,15 @@
|
|||
v-if="showSubmitButton"
|
||||
type="submit"
|
||||
:label="submitButtonText"
|
||||
:loading="isSubmitting"
|
||||
:loading="isLoading"
|
||||
:disabled="isFormDisabled"
|
||||
severity="primary"
|
||||
/>
|
||||
<Button
|
||||
v-if="showCancelButton"
|
||||
type="button"
|
||||
:label="cancelButtonText"
|
||||
:disabled="isLoading"
|
||||
severity="secondary"
|
||||
variant="outlined"
|
||||
@click="handleCancel"
|
||||
|
|
@ -408,6 +406,9 @@ import DatePicker from "primevue/datepicker";
|
|||
import FileUpload from "primevue/fileupload";
|
||||
import Button from "primevue/button";
|
||||
import Message from "primevue/message";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
|
||||
const props = defineProps({
|
||||
fields: {
|
||||
|
|
@ -457,6 +458,27 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingMessage: {
|
||||
type: String,
|
||||
default: "Processing...",
|
||||
},
|
||||
disableOnLoading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// Auto-connect to global loading store
|
||||
useGlobalLoading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
formName: {
|
||||
type: String,
|
||||
default: "form",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData", "submit", "cancel", "change", "blur"]);
|
||||
|
|
@ -466,6 +488,23 @@ const internalFormData = reactive({});
|
|||
const formErrors = reactive({});
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
// Computed loading and disabled states
|
||||
const isLoading = computed(() => {
|
||||
if (props.useGlobalLoading) {
|
||||
return (
|
||||
props.loading ||
|
||||
loadingStore.getComponentLoading("form") ||
|
||||
loadingStore.getComponentLoading(props.formName) ||
|
||||
isSubmitting.value
|
||||
);
|
||||
}
|
||||
return props.loading || isSubmitting.value;
|
||||
});
|
||||
|
||||
const isFormDisabled = computed(() => {
|
||||
return props.disableOnLoading && isLoading.value;
|
||||
});
|
||||
|
||||
// Computed property for v-model binding
|
||||
const fieldValues = computed({
|
||||
get() {
|
||||
|
|
@ -605,6 +644,71 @@ const validateField = (field, value) => {
|
|||
return errors.length > 0 ? errors[0] : null;
|
||||
};
|
||||
|
||||
// Handle keydown events for input restrictions
|
||||
const handleKeyDown = (field, event) => {
|
||||
// Check if field has numeric-only restriction
|
||||
if (field.inputMode === "numeric" || field.pattern === "[0-9]*") {
|
||||
const key = event.key;
|
||||
|
||||
// Allow control keys (backspace, delete, tab, escape, enter, arrows, etc.)
|
||||
const allowedKeys = [
|
||||
"Backspace",
|
||||
"Delete",
|
||||
"Tab",
|
||||
"Escape",
|
||||
"Enter",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"Home",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
];
|
||||
|
||||
// Allow Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X, Ctrl+Z
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow allowed control keys
|
||||
if (allowedKeys.includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow numeric keys (0-9)
|
||||
if (!/^[0-9]$/.test(key)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check max length if specified
|
||||
if (field.maxLength && event.target.value.length >= field.maxLength) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle text input with custom formatting
|
||||
const handleTextInput = (field, event) => {
|
||||
let value = event.target ? event.target.value : event;
|
||||
|
||||
// Apply custom input formatting if provided
|
||||
if (field.onInput && typeof field.onInput === "function") {
|
||||
value = field.onInput(value);
|
||||
|
||||
// Update the input value immediately to reflect formatting
|
||||
if (event.target) {
|
||||
event.target.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the standard field change handler
|
||||
handleFieldChange(field, value);
|
||||
};
|
||||
|
||||
// Handle field value changes
|
||||
const handleFieldChange = (field, value) => {
|
||||
// Update form data
|
||||
|
|
@ -693,19 +797,24 @@ const handleAutocompleteSearch = (field, event) => {
|
|||
const getFieldColumnClasses = (field) => {
|
||||
const classes = [];
|
||||
|
||||
// Default column sizes based on field.cols or defaults
|
||||
// Base column size (mobile-first)
|
||||
const cols = field.cols || 12;
|
||||
const sm = field.sm || 12;
|
||||
const md = field.md || 6;
|
||||
const lg = field.lg || 6;
|
||||
classes.push(`col-${cols}`);
|
||||
|
||||
// Convert to CSS Grid or Flexbox classes
|
||||
// This is a basic implementation - you might want to use a CSS framework
|
||||
if (cols === 12) classes.push("col-full");
|
||||
else if (cols === 6) classes.push("col-half");
|
||||
else if (cols === 4) classes.push("col-third");
|
||||
else if (cols === 3) classes.push("col-quarter");
|
||||
else classes.push(`col-${cols}`);
|
||||
// Small breakpoint (sm)
|
||||
if (field.sm && field.sm !== cols) {
|
||||
classes.push(`sm-${field.sm}`);
|
||||
}
|
||||
|
||||
// Medium breakpoint (md)
|
||||
if (field.md && field.md !== cols) {
|
||||
classes.push(`md-${field.md}`);
|
||||
}
|
||||
|
||||
// Large breakpoint (lg)
|
||||
if (field.lg && field.lg !== cols) {
|
||||
classes.push(`lg-${field.lg}`);
|
||||
}
|
||||
|
||||
return classes.join(" ");
|
||||
};
|
||||
|
|
@ -805,6 +914,11 @@ defineExpose({
|
|||
}
|
||||
return true;
|
||||
},
|
||||
// Loading control methods
|
||||
startLoading: (message) =>
|
||||
loadingStore.setComponentLoading(props.formName, true, message || props.loadingMessage),
|
||||
stopLoading: () => loadingStore.setComponentLoading(props.formName, false),
|
||||
isLoading: () => isLoading.value,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -831,19 +945,7 @@ defineExpose({
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Responsive column classes */
|
||||
.col-full {
|
||||
grid-column: span 12;
|
||||
}
|
||||
.col-half {
|
||||
grid-column: span 6;
|
||||
}
|
||||
.col-third {
|
||||
grid-column: span 4;
|
||||
}
|
||||
.col-quarter {
|
||||
grid-column: span 3;
|
||||
}
|
||||
/* Base column classes (mobile-first) */
|
||||
.col-1 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
|
@ -881,14 +983,130 @@ defineExpose({
|
|||
grid-column: span 12;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
/* Small breakpoint (576px and up) */
|
||||
@media (min-width: 576px) {
|
||||
.sm-1 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
.sm-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.sm-3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
.sm-4 {
|
||||
grid-column: span 4;
|
||||
}
|
||||
.sm-5 {
|
||||
grid-column: span 5;
|
||||
}
|
||||
.sm-6 {
|
||||
grid-column: span 6;
|
||||
}
|
||||
.sm-7 {
|
||||
grid-column: span 7;
|
||||
}
|
||||
.sm-8 {
|
||||
grid-column: span 8;
|
||||
}
|
||||
.sm-9 {
|
||||
grid-column: span 9;
|
||||
}
|
||||
.sm-10 {
|
||||
grid-column: span 10;
|
||||
}
|
||||
.sm-11 {
|
||||
grid-column: span 11;
|
||||
}
|
||||
.sm-12 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium breakpoint (768px and up) */
|
||||
@media (min-width: 768px) {
|
||||
.md-1 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
.md-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.md-3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
.md-4 {
|
||||
grid-column: span 4;
|
||||
}
|
||||
.md-5 {
|
||||
grid-column: span 5;
|
||||
}
|
||||
.md-6 {
|
||||
grid-column: span 6;
|
||||
}
|
||||
.md-7 {
|
||||
grid-column: span 7;
|
||||
}
|
||||
.md-8 {
|
||||
grid-column: span 8;
|
||||
}
|
||||
.md-9 {
|
||||
grid-column: span 9;
|
||||
}
|
||||
.md-10 {
|
||||
grid-column: span 10;
|
||||
}
|
||||
.md-11 {
|
||||
grid-column: span 11;
|
||||
}
|
||||
.md-12 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large breakpoint (992px and up) */
|
||||
@media (min-width: 992px) {
|
||||
.lg-1 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
.lg-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.lg-3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
.lg-4 {
|
||||
grid-column: span 4;
|
||||
}
|
||||
.lg-5 {
|
||||
grid-column: span 5;
|
||||
}
|
||||
.lg-6 {
|
||||
grid-column: span 6;
|
||||
}
|
||||
.lg-7 {
|
||||
grid-column: span 7;
|
||||
}
|
||||
.lg-8 {
|
||||
grid-column: span 8;
|
||||
}
|
||||
.lg-9 {
|
||||
grid-column: span 9;
|
||||
}
|
||||
.lg-10 {
|
||||
grid-column: span 10;
|
||||
}
|
||||
.lg-11 {
|
||||
grid-column: span 11;
|
||||
}
|
||||
.lg-12 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive - stack all fields on very small screens */
|
||||
@media (max-width: 575px) {
|
||||
.form-field {
|
||||
grid-column: span 1 !important;
|
||||
grid-column: span 12 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -959,12 +1177,11 @@ defineExpose({
|
|||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Tablet responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.col-half,
|
||||
.col-third,
|
||||
.col-quarter {
|
||||
grid-column: span 6;
|
||||
/* Tablet responsive - let the responsive classes handle the layout */
|
||||
@media (max-width: 767px) {
|
||||
/* Fields without md specified should span full width on tablets */
|
||||
.form-field:not([class*="md-"]) {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
60
frontend/src/components/common/GlobalLoadingOverlay.vue
Normal file
60
frontend/src/components/common/GlobalLoadingOverlay.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="showOverlay"
|
||||
class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-[9999] transition-opacity duration-200"
|
||||
:class="{ 'opacity-100': showOverlay, 'opacity-0 pointer-events-none': !showOverlay }"
|
||||
>
|
||||
<div class="bg-white rounded-lg p-6 shadow-xl max-w-sm w-full mx-4 text-center">
|
||||
<div class="mb-4">
|
||||
<i class="pi pi-spin pi-spinner text-4xl text-blue-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">Loading</h3>
|
||||
<p class="text-gray-600">{{ loadingMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
|
||||
const props = defineProps({
|
||||
// Show overlay only for global loading, not component-specific
|
||||
globalOnly: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// Minimum display time to prevent flashing
|
||||
minDisplayTime: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
});
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
|
||||
const showOverlay = computed(() => {
|
||||
if (props.globalOnly) {
|
||||
return loadingStore.isLoading;
|
||||
}
|
||||
return loadingStore.isAnyLoading;
|
||||
});
|
||||
|
||||
const loadingMessage = computed(() => {
|
||||
return loadingStore.loadingMessage;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Additional styling for better visual appearance */
|
||||
.bg-opacity-30 {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Backdrop blur effect for modern browsers */
|
||||
@supports (backdrop-filter: blur(4px)) {
|
||||
.fixed.inset-0 {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, watchEffect } from "vue";
|
||||
import { ref, reactive, computed, watch } from "vue";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Modal from "@/components/common/Modal.vue";
|
||||
import Form from "@/components/common/Form.vue";
|
||||
|
|
@ -43,11 +43,12 @@ const isVisible = computed(() => modalStore.isModalOpen("createClient"));
|
|||
const customerNames = ref([]);
|
||||
// Form data
|
||||
const formData = reactive({
|
||||
name: "",
|
||||
address: "",
|
||||
customertype: "",
|
||||
customerName: "",
|
||||
addressLine1: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
zipcode: "",
|
||||
pincode: "",
|
||||
city: "",
|
||||
state: "",
|
||||
});
|
||||
|
|
@ -78,12 +79,27 @@ const modalOptions = {
|
|||
// Form field definitions
|
||||
const formFields = computed(() => [
|
||||
{
|
||||
name: "name",
|
||||
name: "customertype",
|
||||
label: "Client Type",
|
||||
type: "select",
|
||||
required: true,
|
||||
placeholder: "Select client type",
|
||||
cols: 12,
|
||||
md: 6,
|
||||
options: [
|
||||
{ label: "Individual", value: "Individual" },
|
||||
{ label: "Company", value: "Company" },
|
||||
],
|
||||
helpText: "Select whether this is an individual or company client",
|
||||
},
|
||||
{
|
||||
name: "customerName",
|
||||
label: "Client Name",
|
||||
type: "autocomplete", // Changed from 'select' to 'autocomplete'
|
||||
required: true,
|
||||
placeholder: "Type or select client name",
|
||||
cols: 12,
|
||||
md: 6,
|
||||
options: customerNames.value, // Direct array of strings
|
||||
forceSelection: false, // Allow custom entries not in the list
|
||||
dropdown: true,
|
||||
|
|
@ -92,7 +108,7 @@ const formFields = computed(() => [
|
|||
// Let the Form component handle filtering automatically
|
||||
},
|
||||
{
|
||||
name: "address",
|
||||
name: "addressLine1",
|
||||
label: "Address",
|
||||
type: "text",
|
||||
required: true,
|
||||
|
|
@ -127,17 +143,24 @@ const formFields = computed(() => [
|
|||
md: 6,
|
||||
},
|
||||
{
|
||||
name: "zipcode",
|
||||
name: "pincode",
|
||||
label: "Zip Code",
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "Enter zip code",
|
||||
placeholder: "Enter 5-digit zip code",
|
||||
cols: 12,
|
||||
md: 4,
|
||||
maxLength: 5,
|
||||
inputMode: "numeric",
|
||||
pattern: "[0-9]*",
|
||||
onChangeOverride: handleZipcodeChange,
|
||||
onInput: (value) => {
|
||||
// Only allow numbers and limit to 5 digits
|
||||
return value.replace(/\D/g, "").substring(0, 5);
|
||||
},
|
||||
validate: (value) => {
|
||||
if (value && !/^\d{5}(-\d{4})?$/.test(value)) {
|
||||
return "Please enter a valid zip code";
|
||||
if (value && !/^\d{5}$/.test(value)) {
|
||||
return "Please enter a valid 5-digit zip code";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
|
@ -148,6 +171,7 @@ const formFields = computed(() => [
|
|||
type: availableCities.value.length > 0 ? "select" : "text",
|
||||
required: true,
|
||||
disabled: false,
|
||||
showClear: availableCities.value.length > 1,
|
||||
placeholder: availableCities.value.length > 0 ? "Select city" : "Enter city name",
|
||||
options: availableCities.value.map((place) => ({
|
||||
label: place["place name"],
|
||||
|
|
@ -194,7 +218,10 @@ const formFields = computed(() => [
|
|||
|
||||
// Handle zipcode change and API lookup
|
||||
async function handleZipcodeChange(value, fieldName, currentFormData) {
|
||||
if (fieldName === "zipcode" && value && value.length >= 5) {
|
||||
if (value.length < 5) {
|
||||
return;
|
||||
}
|
||||
if (fieldName === "pincode" && value && value.length >= 5) {
|
||||
// Only process if it's a valid zipcode format
|
||||
const zipcode = value.replace(/\D/g, "").substring(0, 5);
|
||||
|
||||
|
|
@ -292,23 +319,14 @@ function getStatusIcon(type) {
|
|||
}
|
||||
|
||||
// Handle form submission
|
||||
async function handleSubmit(submittedFormData) {
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
showStatusMessage("Creating client...", "info");
|
||||
|
||||
// Convert form data to the expected format
|
||||
const clientData = {
|
||||
name: submittedFormData.name,
|
||||
address: submittedFormData.address,
|
||||
phone: submittedFormData.phone,
|
||||
email: submittedFormData.email,
|
||||
zipcode: submittedFormData.zipcode,
|
||||
city: submittedFormData.city,
|
||||
state: submittedFormData.state,
|
||||
};
|
||||
|
||||
// Call API to create client
|
||||
const response = await Api.createClient(clientData);
|
||||
const response = await Api.createClient(formData);
|
||||
|
||||
if (response && response.success) {
|
||||
showStatusMessage("Client created successfully!", "success");
|
||||
|
|
@ -366,12 +384,7 @@ watch(isVisible, async () => {
|
|||
try {
|
||||
const names = await Api.getCustomerNames();
|
||||
console.log("Loaded customer names:", names);
|
||||
console.log("Customer names type:", typeof names, Array.isArray(names));
|
||||
console.log("First customer name:", names[0], typeof names[0]);
|
||||
customerNames.value = names;
|
||||
|
||||
// Debug: Let's also set some test data to see if autocomplete works at all
|
||||
console.log("Setting customerNames to:", customerNames.value);
|
||||
} catch (error) {
|
||||
console.error("Error loading customer names:", error);
|
||||
// Set some test data to debug if autocomplete works
|
||||
|
|
@ -406,31 +419,6 @@ watch(isVisible, async () => {
|
|||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Form styling adjustments for PrimeVue components */
|
||||
:deep(.p-inputtext),
|
||||
:deep(.p-dropdown),
|
||||
:deep(.p-autocomplete) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Ensure AutoComplete panel appears above modal */
|
||||
:global(.p-autocomplete-overlay) {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
:global(.p-autocomplete-panel) {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
:deep(.p-button) {
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.p-button:not(.p-button-text)) {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Status message styling */
|
||||
.status-message {
|
||||
padding: 12px 16px;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ import { onMounted, ref } from "vue";
|
|||
import DataTable from "../common/DataTable.vue";
|
||||
import Api from "../../api";
|
||||
import { FilterMatchMode } from "@primevue/core";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
|
||||
const itemCount = ref(0);
|
||||
const page = ref(0);
|
||||
const pageLength = ref(30);
|
||||
const tableData = ref([]);
|
||||
|
||||
const onClick = () => {
|
||||
|
|
@ -35,20 +41,31 @@ const columns = [
|
|||
},
|
||||
{
|
||||
label: "Appt. Scheduled",
|
||||
fieldName: "appointmentScheduled",
|
||||
fieldName: "appointmentStatus",
|
||||
type: "status",
|
||||
sortable: true,
|
||||
},
|
||||
{ label: "Estimate Sent", fieldName: "estimateSent", type: "status", sortable: true },
|
||||
{ label: "Payment Received", fieldName: "paymentReceived", type: "status", sortable: true },
|
||||
{ label: "Estimate Sent", fieldName: "estimateStatus", type: "status", sortable: true },
|
||||
{ label: "Payment Received", fieldName: "paymentStatus", type: "status", sortable: true },
|
||||
{ label: "Job Status", fieldName: "jobStatus", type: "status", sortable: true },
|
||||
];
|
||||
onMounted(async () => {
|
||||
if (tableData.value.length > 0) {
|
||||
return;
|
||||
}
|
||||
let data = await Api.getClientDetails();
|
||||
tableData.value = data;
|
||||
|
||||
try {
|
||||
// Use the loading store to track this API call
|
||||
const data = await loadingStore.withComponentLoading(
|
||||
"clients",
|
||||
() => Api.getClientDetails(),
|
||||
"Loading client data...",
|
||||
);
|
||||
tableData.value = data;
|
||||
} catch (error) {
|
||||
console.error("Error loading client data:", error);
|
||||
// You could also show a toast or other error notification here
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="css"></style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue