feat : creation du composant datatable (WIP)
This commit is contained in:
@@ -1,21 +1,60 @@
|
||||
<template>
|
||||
<div class="mt-6">
|
||||
<table class="min-w-full border border-slate-300">
|
||||
<thead class="bg-slate-100 uppercase tracking-wide">
|
||||
<div class="mt-6 mx-[6px]">
|
||||
<table class="w-full border border-slate-300 table-fixed">
|
||||
<thead class="bg-slate-100 capitalize tracking-wide">
|
||||
<tr>
|
||||
<th
|
||||
v-for="column in normalizedColumns"
|
||||
:key="column.key"
|
||||
class="border border-slate-300 px-3 py-2 text-left"
|
||||
class="border border-slate-300 px-2 py-1"
|
||||
>
|
||||
<span>{{ column.label }}</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<UiSelect
|
||||
v-if="column.isSearchable && column.type === 'selectTypeReception'"
|
||||
v-model="searchValues[column.key]"
|
||||
:placeholder="column.label"
|
||||
select-class="w-full !text-sm !py-1"
|
||||
:options="[
|
||||
{ value: '__all__', label: 'Tous' },
|
||||
...receptionTypes.map((type) => ({
|
||||
value: type.label,
|
||||
label: type.label
|
||||
}))
|
||||
]"
|
||||
/>
|
||||
<UiSelect
|
||||
v-else-if="column.isSearchable && column.type === 'selectTypeShipment'"
|
||||
v-model="searchValues[column.key]"
|
||||
:placeholder="column.label"
|
||||
select-class="w-full !text-sm !py-1"
|
||||
:options="[
|
||||
{ value: '__all__', label: 'Tous' },
|
||||
...shipmentTypes.map((type) => ({
|
||||
value: type.label,
|
||||
label: type.label
|
||||
}))
|
||||
]"
|
||||
/>
|
||||
<div v-else-if="column.isSearchable" class="relative">
|
||||
<UiTextInput
|
||||
v-model="searchValues[column.key]"
|
||||
:placeholder="column.label"
|
||||
input-class="min-w-full !text-sm !py-1 !pr-7"
|
||||
/>
|
||||
<Icon
|
||||
name="gg:search"
|
||||
class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<span v-else>{{ column.label }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="loading">
|
||||
<td
|
||||
class="border border-slate-300 px-3 py-2 text-left text-slate-500"
|
||||
class="border border-slate-300 px-2 py-2 whitespace-pre-line"
|
||||
:colspan="normalizedColumns.length || 1"
|
||||
>
|
||||
Chargement...
|
||||
@@ -32,6 +71,7 @@
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="(row, rowIndex) in displayedRows"
|
||||
class="hover:bg-primary-500 hover:bg-opacity-15"
|
||||
:key="rowIndex"
|
||||
:class="props.rowClickable ? 'cursor-pointer' : ''"
|
||||
@click="props.rowClickable ? onRowClick(row) : null"
|
||||
@@ -39,7 +79,7 @@
|
||||
<td
|
||||
v-for="column in normalizedColumns"
|
||||
:key="column.key"
|
||||
class="border border-slate-300 px-2 py-2 whitespace-pre-line"
|
||||
class="border border-slate-300 px-2 py-2 whitespace-pre-line "
|
||||
>
|
||||
{{ formatColumnValue(row, column) }}
|
||||
</td>
|
||||
@@ -89,14 +129,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {Row,ColumnConfig, AnyCollection, PaginationItem }from '~/services/datatable'
|
||||
import {useApi} from "~/composables/useApi";
|
||||
import {Row, ColumnConfig, AnyCollection, PaginationItem} from '~/services/dto/datatable-data'
|
||||
import {useApi} from '~/composables/useApi'
|
||||
import type {ReceptionTypeData} from '~/services/dto/reception-type-data'
|
||||
import {getReceptionTypeList} from '~/services/reception-type'
|
||||
import type {ShipmentTypeData} from "~/services/dto/shipment-data";
|
||||
import {getShipmentTypeList} from "~/services/shipment-type";
|
||||
|
||||
const api = useApi()
|
||||
const receptionTypes = ref<ReceptionTypeData[]>([])
|
||||
const shipmentTypes = ref<ShipmentTypeData[]>([])
|
||||
const loading = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const rows = ref<Row[]>([])
|
||||
const total = ref(0)
|
||||
const searchValues = reactive<Record<string, string>>({})
|
||||
const isNestedMode = computed(() => Boolean(props.responsePath))
|
||||
const effectiveTotal = computed(() => total.value)
|
||||
const emit = defineEmits<{
|
||||
@@ -118,7 +165,6 @@ const props = withDefaults(defineProps<{
|
||||
itemsPerPage: 10,
|
||||
rowClickable: true
|
||||
})
|
||||
|
||||
const displayedRows = computed<Row[]>(() => {
|
||||
if (!isNestedMode.value) return rows.value
|
||||
|
||||
@@ -126,13 +172,19 @@ const displayedRows = computed<Row[]>(() => {
|
||||
const endIndex = startIndex + props.itemsPerPage
|
||||
return rows.value.slice(startIndex, endIndex)
|
||||
})
|
||||
onMounted(async () => {
|
||||
receptionTypes.value = await getReceptionTypeList()
|
||||
shipmentTypes.value = await getShipmentTypeList()
|
||||
|
||||
})
|
||||
const normalizedColumns = computed(() => {
|
||||
if (props.columns.length > 0) {
|
||||
return props.columns.map((column) => ({
|
||||
key: column.key,
|
||||
label: column.label ?? column.key,
|
||||
format: column.format
|
||||
format: column.format,
|
||||
isSearchable: column.isSearchable ?? false,
|
||||
type: column.type
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -148,7 +200,7 @@ const normalizedColumns = computed(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(effectiveTotal.value / props.itemsPerPage)),)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(effectiveTotal.value / props.itemsPerPage)))
|
||||
|
||||
function getVisiblePages(page: number, lastPage: number): number[] {
|
||||
const candidates = new Set([1, page - 1, page, page + 1, lastPage])
|
||||
@@ -170,6 +222,7 @@ function insertEllipses(sortedPages: number[]): PaginationItem[] {
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
const paginationItems = computed<PaginationItem[]>(() => {
|
||||
const pages = getVisiblePages(currentPage.value, totalPages.value)
|
||||
return insertEllipses(pages)
|
||||
@@ -193,7 +246,21 @@ watch(
|
||||
}
|
||||
await loadPage()
|
||||
},
|
||||
{ immediate: true }
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout>
|
||||
|
||||
watch(
|
||||
() => ({...searchValues}),
|
||||
() => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
currentPage.value = 1
|
||||
if (!isNestedMode.value) loadPage()
|
||||
}, 750)
|
||||
},
|
||||
{deep: true}
|
||||
)
|
||||
|
||||
watch(
|
||||
@@ -211,9 +278,54 @@ watch(
|
||||
currentPage.value = totalPages.value
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
function buildDateInterval(value: string): { after: string; before: string } | null {
|
||||
const trimmed = value.trim()
|
||||
|
||||
// YYYY
|
||||
if (/^\d{4}$/.test(trimmed)) {
|
||||
const year = Number(trimmed)
|
||||
return {
|
||||
after: `${year}-01-01`,
|
||||
before: `${year + 1}-01-01`
|
||||
}
|
||||
}
|
||||
|
||||
// YYYY-MM
|
||||
if (/^\d{4}-\d{2}$/.test(trimmed)) {
|
||||
const [year, month] = trimmed.split('-').map(Number)
|
||||
|
||||
const nextMonth = month === 12 ? 1 : month + 1
|
||||
const nextYear = month === 12 ? year + 1 : year
|
||||
|
||||
return {
|
||||
after: `${year}-${String(month).padStart(2, '0')}-01`,
|
||||
before: `${nextYear}-${String(nextMonth).padStart(2, '0')}-01`
|
||||
}
|
||||
}
|
||||
|
||||
// YYYY-MM-DD
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
||||
const date = new Date(`${trimmed}T00:00:00`)
|
||||
const nextDay = new Date(date)
|
||||
nextDay.setDate(date.getDate() + 1)
|
||||
|
||||
const yyyy = nextDay.getFullYear()
|
||||
const mm = String(nextDay.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(nextDay.getDate()).padStart(2, '0')
|
||||
|
||||
return {
|
||||
after: trimmed,
|
||||
before: `${yyyy}-${mm}-${dd}`
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
// Construit la requête, charge les données et normalise la réponse, puis met à jour rows et total
|
||||
async function loadPage(): Promise<void> {
|
||||
if (!props.url) {
|
||||
@@ -236,11 +348,36 @@ async function loadPage(): Promise<void> {
|
||||
total.value = rows.value.length
|
||||
return
|
||||
}
|
||||
const searchQuery: Record<string, string> = {}
|
||||
|
||||
for (const column of normalizedColumns.value) {
|
||||
if (!column.isSearchable) continue
|
||||
|
||||
const rawValue = searchValues[column.key] ?? ''
|
||||
const raw = rawValue === '__all__' ? '' : rawValue.trim()
|
||||
if (!raw) continue
|
||||
|
||||
const paramBase = column.key
|
||||
|
||||
if (column.type === 'date') {
|
||||
const interval = buildDateInterval(raw)
|
||||
|
||||
if (interval) {
|
||||
searchQuery[`${paramBase}[after]`] = interval.after
|
||||
searchQuery[`${paramBase}[before]`] = interval.before
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
searchQuery[paramBase] = raw
|
||||
}
|
||||
|
||||
const requestQuery: Record<string, unknown> = {
|
||||
...props.query,
|
||||
...searchQuery,
|
||||
page: currentPage.value,
|
||||
itemsPerPage: props.itemsPerPage
|
||||
itemsPerPage: props.itemsPerPage,
|
||||
}
|
||||
|
||||
const response = await api.get<AnyCollection<Row> | Row[]>(props.url, requestQuery, {
|
||||
|
||||
Reference in New Issue
Block a user