complete Complete User Request Submission section RAD-1
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import Pagination from './Pagination.vue';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Toolbar from 'primevue/toolbar';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
pagination: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
filterLoader: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'selection-change', 'page-change', 'search-change', 'filter-change', 'apply-filters', 'clear-filters']);
|
||||
|
||||
const localSelection = ref(props.modelValue);
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
localSelection.value = newValue;
|
||||
}, { immediate: true });
|
||||
|
||||
const handleSelectionChange = (event) => {
|
||||
localSelection.value = event;
|
||||
emit('update:modelValue', event);
|
||||
emit('selection-change', event);
|
||||
};
|
||||
|
||||
const localSearchValue = ref(props.searchValue);
|
||||
|
||||
watch(() => props.searchValue, (newValue) => {
|
||||
localSearchValue.value = newValue;
|
||||
});
|
||||
|
||||
let searchDebounceTimer = null;
|
||||
|
||||
const handleSearchInput = () => {
|
||||
if (searchDebounceTimer) {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
}
|
||||
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
emit('search-change', localSearchValue.value);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const localFilters = ref({ ...props.filters });
|
||||
const dynamicOptions = ref({});
|
||||
const loadingOptions = ref({});
|
||||
const searchDebounceTimers = ref({});
|
||||
const searchValues = ref({});
|
||||
|
||||
onMounted(() => {
|
||||
props.columns.forEach(column => {
|
||||
if (column.filter?.dynamic && props.filters[column.key]) {
|
||||
const filterValue = props.filters[column.key];
|
||||
searchValues.value[column.key] = filterValue.toString();
|
||||
loadDynamicOptions(column, true, filterValue.toString(), true);
|
||||
}
|
||||
else if (column.filter?.dynamic) {
|
||||
searchValues.value[column.key] = '';
|
||||
loadDynamicOptions(column, true, '', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
watch(() => props.filters, (newValue) => {
|
||||
Object.keys(newValue).forEach(key => {
|
||||
if (newValue[key] !== undefined && newValue[key] !== null) {
|
||||
localFilters.value[key] = newValue[key];
|
||||
}
|
||||
});
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
const handleFilterChange = (columnKey, value) => {
|
||||
localFilters.value[columnKey] = value;
|
||||
emit('filter-change', { ...localFilters.value });
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
localFilters.value = {};
|
||||
dynamicOptions.value = {};
|
||||
searchValues.value = {};
|
||||
emit('clear-filters');
|
||||
};
|
||||
|
||||
const loadDynamicOptions = async (column, forceReload = false, searchTerm = '', shouldSelectValue = false) => {
|
||||
if (!column.filter?.dynamic || !column.filter?.url) return;
|
||||
|
||||
const columnKey = column.key;
|
||||
loadingOptions.value[columnKey] = true;
|
||||
|
||||
try {
|
||||
let url = column.filter.url;
|
||||
if (searchTerm) {
|
||||
url += `?search=${encodeURIComponent(searchTerm)}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
dynamicOptions.value[columnKey] = data.data;
|
||||
|
||||
if (shouldSelectValue && props.filters[columnKey]) {
|
||||
const filterValue = props.filters[columnKey];
|
||||
const foundOption = data.data.find(option => option.value == filterValue);
|
||||
if (foundOption) {
|
||||
localFilters.value[columnKey] = foundOption.value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dynamicOptions.value[columnKey] = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
dynamicOptions.value[columnKey] = [];
|
||||
} finally {
|
||||
loadingOptions.value[columnKey] = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleFilterSearch = (column, searchTerm) => {
|
||||
if (!column.filter?.dynamic || !column.filter?.search) return;
|
||||
|
||||
const columnKey = column.key;
|
||||
searchValues.value[columnKey] = searchTerm;
|
||||
|
||||
if (searchDebounceTimers.value[columnKey]) {
|
||||
clearTimeout(searchDebounceTimers.value[columnKey]);
|
||||
}
|
||||
|
||||
searchDebounceTimers.value[columnKey] = setTimeout(() => {
|
||||
loadDynamicOptions(column, true, searchTerm, false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const renderFilter = (column) => {
|
||||
if (!column.filter) return null;
|
||||
|
||||
const filterConfig = column.filter;
|
||||
const currentValue = localFilters.value[column.key] || null;
|
||||
const columnKey = column.key;
|
||||
|
||||
let options = [];
|
||||
if (filterConfig.dynamic) {
|
||||
options = dynamicOptions.value[columnKey] || [];
|
||||
} else if (filterConfig.options) {
|
||||
options = filterConfig.options;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'dropdown') {
|
||||
return {
|
||||
type: 'dropdown',
|
||||
options: loadingOptions.value[columnKey] ? [{ label: 'در حال پردازش...', value: null, disabled: true }] : options,
|
||||
value: currentValue,
|
||||
placeholder: filterConfig.placeholder || 'انتخاب کنید...',
|
||||
loading: loadingOptions.value[columnKey] || false,
|
||||
dynamic: filterConfig.dynamic || false,
|
||||
search: filterConfig.search || false,
|
||||
searchValue: searchValues.value[columnKey] || ''
|
||||
};
|
||||
} else if (filterConfig.type === 'multiselect') {
|
||||
return {
|
||||
type: 'multiselect',
|
||||
options: loadingOptions.value[columnKey] ? [{ label: 'در حال پردازش...', value: null, disabled: true }] : options,
|
||||
value: currentValue || [],
|
||||
placeholder: filterConfig.placeholder || 'انتخاب کنید...',
|
||||
loading: loadingOptions.value[columnKey] || false,
|
||||
dynamic: filterConfig.dynamic || false,
|
||||
search: filterConfig.search || false,
|
||||
searchValue: searchValues.value[columnKey] || ''
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderColumnContent = (column, data) => {
|
||||
let content = '';
|
||||
if (column.render && typeof column.render === 'function') {
|
||||
content = column.render(data);
|
||||
} else {
|
||||
content = data[column.key];
|
||||
}
|
||||
|
||||
const defaultStyle = '!font-medium !text-sm';
|
||||
const customStyle = column.style || defaultStyle;
|
||||
|
||||
return `<span class="${customStyle} text-right wrap-break-word text-sm">${content}</span>`;
|
||||
};
|
||||
|
||||
const getColumnStyle = (column) => {
|
||||
const baseStyle = { minWidth: '120px' };
|
||||
|
||||
if (column.width) {
|
||||
baseStyle.width = column.width + 'px';
|
||||
baseStyle.minWidth = column.width + 'px';
|
||||
baseStyle.maxWidth = column.width + 'px';
|
||||
}
|
||||
|
||||
return baseStyle;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table-generic">
|
||||
<div v-if="searchable" class="p-4 pb-2">
|
||||
<Toolbar class="">
|
||||
<template #start>
|
||||
<div class="grid grid-cols-2a max-sm:grid-cols-1 justify-start gap-2">
|
||||
<div class="grid grid-cols-1 items-center justify-start">
|
||||
<InputText
|
||||
size="small"
|
||||
class="placeholder:!font-light placeholder:!text-xs !h-10"
|
||||
placeholder="جستجو..."
|
||||
type="text"
|
||||
v-model="localSearchValue"
|
||||
@input="handleSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="columns.some(col => col.filter)" class="">
|
||||
<div class="flex flex-wrap max-sm:!flex-col gap-2 py-2">
|
||||
<div
|
||||
v-for="column in columns.filter(col => col.filter)"
|
||||
:key="`filter-${column.key}`"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
|
||||
<Dropdown
|
||||
v-if="renderFilter(column)?.type === 'dropdown'"
|
||||
:modelValue="renderFilter(column).value"
|
||||
:options="renderFilter(column).options"
|
||||
:placeholder="renderFilter(column).loading ? 'در حال بارگذاری...' : renderFilter(column).placeholder"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
size="small"
|
||||
:pt="{label : '!text-[12px] !pt-2.5' , option : '!text-[12px] !font-normal'}"
|
||||
class="min-w-[130px] max-sm:!w-full max-w-[200px] !text-[12px] !h-10"
|
||||
:loading="renderFilter(column).loading"
|
||||
:disabled="renderFilter(column).loading"
|
||||
:filter="renderFilter(column).search"
|
||||
:filterValue="renderFilter(column).searchValue"
|
||||
:filterPlaceholder="renderFilter(column).search ? 'جستجو...' : ''"
|
||||
emptyMessage="داده ای یافت نشد"
|
||||
:filterMatchModeOptions="[]"
|
||||
:filterFunction="() => true"
|
||||
@update:modelValue="(value) => handleFilterChange(column.key, value)"
|
||||
@filter="(event) => handleFilterSearch(column, event.value)"
|
||||
showClear
|
||||
/>
|
||||
|
||||
<MultiSelect
|
||||
v-else-if="renderFilter(column)?.type === 'multiselect'"
|
||||
:modelValue="renderFilter(column).value"
|
||||
:options="renderFilter(column).options"
|
||||
:placeholder="renderFilter(column).loading ? 'در حال بارگذاری...' : renderFilter(column).placeholder"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
size="small"
|
||||
:pt="{label : '!text-[12px] !pt-2.5' , option : '!text-[12px] !font-normal'}"
|
||||
class="min-w-[150px] max-sm:!w-full max-w-[200px] !text-[12px] !h-10"
|
||||
:loading="renderFilter(column).loading"
|
||||
:disabled="renderFilter(column).loading"
|
||||
:filter="renderFilter(column).search"
|
||||
:filterValue="renderFilter(column).searchValue"
|
||||
:filterPlaceholder="renderFilter(column).search ? 'جستجو...' : ''"
|
||||
emptyMessage="داده ای یافت نشد"
|
||||
:filterMatchModeOptions="[]"
|
||||
:filterFunction="() => true"
|
||||
@update:modelValue="(value) => handleFilterChange(column.key, value)"
|
||||
@filter="(event) => handleFilterSearch(column, event.value)"
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
:label="filterLoader ? 'در حال پردازش...' : 'اعمال فیلتر'"
|
||||
size="small"
|
||||
severity="primary"
|
||||
class="!text-xs h-10"
|
||||
:loading="filterLoader"
|
||||
:disabled="filterLoader"
|
||||
@click="$emit('apply-filters')"
|
||||
/>
|
||||
<Button
|
||||
label="پاک کردن"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
class="!text-xs h-10"
|
||||
outlined
|
||||
@click="clearAllFilters"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="localSelection && localSelection.length > 0" class="mr-2 !h-9 grid grid-cols-1">
|
||||
<slot name="bulk-action" :selection="localSelection"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<slot name="toolbar-actions"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
:value="data"
|
||||
dataKey="id"
|
||||
v-model:selection="localSelection"
|
||||
:showGridlines="false"
|
||||
:stripedRows="false"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="flex justify-center py-4 items-center h-full">
|
||||
<span class="text-center text-sm">هیچ دادهای پیدا نشده!</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column
|
||||
v-if="selectable"
|
||||
selectionMode="multiple"
|
||||
headerStyle="width: 4rem"
|
||||
/>
|
||||
|
||||
<Column
|
||||
v-for="column in columns"
|
||||
:key="column.key"
|
||||
:field="column.key"
|
||||
:header="column.title"
|
||||
:sortable="column.hasSort === true"
|
||||
:style="getColumnStyle(column)"
|
||||
>
|
||||
<template #body="{ data }">
|
||||
<slot v-if="column.slot" :name="`column-${column.key}`" :data="data" :column="column">
|
||||
<div v-html="renderColumnContent(column, data)"></div>
|
||||
</slot>
|
||||
<div v-else v-html="renderColumnContent(column, data)"></div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<Pagination
|
||||
v-if="pagination && pagination.total"
|
||||
:total="pagination.total"
|
||||
:current-page="pagination.current_page"
|
||||
:perPage="pagination.per_page"
|
||||
:lastPage="pagination.last_page"
|
||||
:links="pagination.links"
|
||||
@page-change="(page) => emit('page-change', page)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-generic {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user