From 6f1bac381d65f1dbe228c1055bf922154c4e95d3 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 4 Mar 2026 16:05:00 +0100 Subject: [PATCH] refacto(tables) : composant DataTable global + migration de toutes les tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouveau composant DataTable réutilisable avec tri par en-têtes, pagination, filtres colonnes - Nouveau composable useDataTable (sort/page/search/perPage/columnFilters + persistance URL) - Migration des 9 tables : constructeurs, comments, admin, pieces-catalog, component-catalog, product-catalog, documents, activity-log, ManagementView (catégories) - Filtres "Type de" server-side (ipartial) pour pièces, composants, produits Co-Authored-By: Claude Opus 4.6 --- app/components/common/DataTable.vue | 308 ++++++++ app/components/model-types/ManagementView.vue | 703 +++++++++--------- app/composables/useComments.ts | 8 +- app/composables/useComposants.ts | 8 +- app/composables/useDataTable.ts | 186 +++++ app/composables/usePieces.ts | 8 +- app/composables/useProducts.ts | 8 +- app/pages/activity-log.vue | 260 +++---- app/pages/admin/index.vue | 201 ++--- app/pages/comments.vue | 319 +++----- app/pages/component-catalog.vue | 406 ++++------ app/pages/constructeurs.vue | 157 ++-- app/pages/documents.vue | 316 +++----- app/pages/pieces-catalog.vue | 512 +++++-------- app/pages/product-catalog.vue | 442 +++++------ app/shared/types/dataTable.ts | 46 ++ 16 files changed, 1945 insertions(+), 1943 deletions(-) create mode 100644 app/components/common/DataTable.vue create mode 100644 app/composables/useDataTable.ts create mode 100644 app/shared/types/dataTable.ts diff --git a/app/components/common/DataTable.vue b/app/components/common/DataTable.vue new file mode 100644 index 0000000..24e0405 --- /dev/null +++ b/app/components/common/DataTable.vue @@ -0,0 +1,308 @@ + + + diff --git a/app/components/model-types/ManagementView.vue b/app/components/model-types/ManagementView.vue index f87f4e3..3d933eb 100644 --- a/app/components/model-types/ManagementView.vue +++ b/app/components/model-types/ManagementView.vue @@ -9,35 +9,95 @@

- + - + :sort="currentSort" + :pagination="paginationState" + :show-per-page="true" + row-key="id" + empty-message="Aucune catégorie trouvée." + no-results-message="Aucune catégorie ne correspond à votre recherche." + @sort="handleSort" + @update:current-page="handlePageChange" + @update:per-page="handlePerPageChange" + > + + + + + + + +
- +
@@ -103,44 +163,46 @@ diff --git a/app/composables/useComments.ts b/app/composables/useComments.ts index e477be3..a581087 100644 --- a/app/composables/useComments.ts +++ b/app/composables/useComments.ts @@ -68,15 +68,21 @@ export function useComments() { const fetchAllComments = async (options: { status?: string entityType?: string + entityName?: string page?: number itemsPerPage?: number + orderBy?: string + orderDir?: string } = {}): Promise => { loading.value = true try { const params = new URLSearchParams() if (options.status) params.set('status', options.status) if (options.entityType) params.set('entityType', options.entityType) - params.set('order[createdAt]', 'desc') + if (options.entityName) params.set('entityName', options.entityName) + const sortField = options.orderBy || 'createdAt' + const sortDir = options.orderDir || 'desc' + params.set(`order[${sortField}]`, sortDir) params.set('itemsPerPage', String(options.itemsPerPage || 30)) params.set('page', String(options.page || 1)) diff --git a/app/composables/useComposants.ts b/app/composables/useComposants.ts index e5de946..006e000 100644 --- a/app/composables/useComposants.ts +++ b/app/composables/useComposants.ts @@ -41,6 +41,7 @@ interface LoadComposantsOptions { itemsPerPage?: number orderBy?: string orderDir?: 'asc' | 'desc' + typeName?: string force?: boolean } @@ -107,10 +108,11 @@ export function useComposants() { itemsPerPage = 30, orderBy = 'name', orderDir = 'asc', + typeName, force = false, } = options - if (!force && loaded.value && !search && page === 1) { + if (!force && loaded.value && !search && !typeName && page === 1) { return { success: true, data: { items: composants.value, total: total.value, page, itemsPerPage }, @@ -135,6 +137,10 @@ export function useComposants() { params.set('name', search.trim()) } + if (typeName && typeName.trim()) { + params.set('typeComposant.name', typeName.trim()) + } + params.set(`order[${orderBy}]`, orderDir) const result = await get(`/composants?${params.toString()}`) diff --git a/app/composables/useDataTable.ts b/app/composables/useDataTable.ts new file mode 100644 index 0000000..08d8414 --- /dev/null +++ b/app/composables/useDataTable.ts @@ -0,0 +1,186 @@ +import { ref, computed, type Ref, type ComputedRef } from 'vue' +import { useUrlState } from './useUrlState' +import type { DataTableSort, DataTablePagination, DataTableColumnFilters, SortDirection } from '~/shared/types/dataTable' + +export interface UseDataTableDeps { + /** Called whenever sort/page/search/perPage/filter changes. The composable does NOT fetch data itself. */ + fetchData: () => void | Promise +} + +export interface UseDataTableOptions { + /** Default sort field */ + defaultSort?: string + /** Default sort direction */ + defaultDirection?: SortDirection + /** Default items per page */ + defaultPerPage?: number + /** Available per-page options */ + perPageOptions?: number[] + /** Search debounce in ms. Default: 300 */ + searchDebounceMs?: number + /** Whether to persist state to URL. Default: true */ + persistToUrl?: boolean + /** Extra URL state params for page-specific filters */ + extraParams?: Record +} + +export interface UseDataTableReturn { + searchTerm: Ref + sortField: Ref + sortDirection: Ref + currentPage: Ref + itemsPerPage: Ref + columnFilters: Ref + filters: Record> + sort: ComputedRef + pagination: (total: Ref, pageItems: Ref) => ComputedRef + handleSort: (newSort: DataTableSort) => void + handlePageChange: (page: number) => void + handlePerPageChange: (perPage: number) => void + handleFilterChange: () => void + handleColumnFiltersChange: (filters: DataTableColumnFilters) => void + debouncedSearch: () => void + refresh: () => void + perPageOptions: number[] +} + +export function useDataTable( + deps: UseDataTableDeps, + options: UseDataTableOptions = {}, +): UseDataTableReturn { + const { + defaultSort = 'name', + defaultDirection = 'asc', + defaultPerPage = 20, + perPageOptions = [20, 50, 100], + searchDebounceMs = 300, + persistToUrl = true, + extraParams = {}, + } = options + + let searchTerm: Ref + let sortField: Ref + let sortDirection: Ref + let currentPage: Ref + let itemsPerPage: Ref + const filters: Record> = {} + + if (persistToUrl) { + const paramDefs: Record = { + page: { default: 1, type: 'number' }, + perPage: { default: defaultPerPage, type: 'number' }, + q: { default: '', debounce: searchDebounceMs }, + sort: { default: defaultSort }, + dir: { default: defaultDirection }, + ...extraParams, + } + + const state = useUrlState(paramDefs, { + onRestore: () => deps.fetchData(), + }) + + searchTerm = state.q as Ref + sortField = state.sort as Ref + sortDirection = state.dir as unknown as Ref + currentPage = state.page as unknown as Ref + itemsPerPage = state.perPage as unknown as Ref + + for (const key of Object.keys(extraParams)) { + filters[key] = (state as Record>)[key]! + } + } + else { + searchTerm = ref('') + sortField = ref(defaultSort) + sortDirection = ref(defaultDirection) as Ref + currentPage = ref(1) + itemsPerPage = ref(defaultPerPage) + + for (const [key, def] of Object.entries(extraParams)) { + filters[key] = ref(def.default) + } + } + + // Search debounce + let searchTimeout: ReturnType | null = null + + const debouncedSearch = () => { + if (searchTimeout) clearTimeout(searchTimeout) + searchTimeout = setTimeout(() => { + currentPage.value = 1 + deps.fetchData() + }, searchDebounceMs) + } + + // Sort + const sort = computed(() => ({ + field: sortField.value, + direction: sortDirection.value, + })) + + const handleSort = (newSort: DataTableSort) => { + sortField.value = newSort.field + sortDirection.value = newSort.direction + currentPage.value = 1 + deps.fetchData() + } + + // Pagination + const handlePageChange = (page: number) => { + currentPage.value = page + deps.fetchData() + } + + const handlePerPageChange = (perPage: number) => { + itemsPerPage.value = perPage + currentPage.value = 1 + deps.fetchData() + } + + // Column filters + const columnFilters = ref({}) + + const handleColumnFiltersChange = (newFilters: DataTableColumnFilters) => { + columnFilters.value = newFilters + currentPage.value = 1 + deps.fetchData() + } + + // Generic filter change handler (resets page and refetches) + const handleFilterChange = () => { + currentPage.value = 1 + deps.fetchData() + } + + const pagination = (total: Ref, pageItems: Ref): ComputedRef => + computed(() => ({ + currentPage: currentPage.value, + totalPages: Math.ceil(total.value / itemsPerPage.value) || 1, + totalItems: total.value, + pageItems: pageItems.value, + perPageOptions, + perPage: itemsPerPage.value, + })) + + const refresh = () => deps.fetchData() + + return { + searchTerm, + sortField, + sortDirection, + currentPage, + itemsPerPage, + columnFilters, + filters, + sort, + pagination, + handleSort, + handlePageChange, + handlePerPageChange, + handleFilterChange, + handleColumnFiltersChange, + debouncedSearch, + refresh, + perPageOptions, + } +} diff --git a/app/composables/usePieces.ts b/app/composables/usePieces.ts index 0cdced6..048ba02 100644 --- a/app/composables/usePieces.ts +++ b/app/composables/usePieces.ts @@ -42,6 +42,7 @@ interface LoadPiecesOptions { itemsPerPage?: number orderBy?: string orderDir?: 'asc' | 'desc' + typeName?: string force?: boolean } @@ -117,10 +118,11 @@ export function usePieces() { itemsPerPage = 30, orderBy = 'name', orderDir = 'asc', + typeName, force = false, } = options - if (!force && loaded.value && !search && page === 1) { + if (!force && loaded.value && !search && !typeName && page === 1) { return { success: true, data: { items: pieces.value, total: total.value, page, itemsPerPage }, @@ -145,6 +147,10 @@ export function usePieces() { params.set('name', search.trim()) } + if (typeName && typeName.trim()) { + params.set('typePiece.name', typeName.trim()) + } + params.set(`order[${orderBy}]`, orderDir) const result = await get(`/pieces?${params.toString()}`) diff --git a/app/composables/useProducts.ts b/app/composables/useProducts.ts index f295c94..1d56a54 100644 --- a/app/composables/useProducts.ts +++ b/app/composables/useProducts.ts @@ -40,6 +40,7 @@ interface LoadProductsOptions { itemsPerPage?: number orderBy?: string orderDir?: 'asc' | 'desc' + typeName?: string force?: boolean } @@ -116,10 +117,11 @@ export function useProducts() { itemsPerPage = 30, orderBy = 'name', orderDir = 'asc', + typeName, force = false, } = options - if (!force && loaded.value && !search && page === 1) { + if (!force && loaded.value && !search && !typeName && page === 1) { return { success: true, data: { items: products.value, total: total.value, page, itemsPerPage }, @@ -144,6 +146,10 @@ export function useProducts() { params.set('name', search.trim()) } + if (typeName && typeName.trim()) { + params.set('typeProduct.name', typeName.trim()) + } + params.set(`order[${orderBy}]`, orderDir) const result = await get(`/products?${params.toString()}`) diff --git a/app/pages/activity-log.vue b/app/pages/activity-log.vue index 2a0c220..d1b27d8 100644 --- a/app/pages/activity-log.vue +++ b/app/pages/activity-log.vue @@ -9,8 +9,24 @@
-
-
+ + -
-
diff --git a/app/pages/comments.vue b/app/pages/comments.vue index 6ab6c83..0033d4f 100644 --- a/app/pages/comments.vue +++ b/app/pages/comments.vue @@ -11,9 +11,22 @@
- -
-
+ + -
- - -
-
+ -

- {{ comments.length }} / {{ total }} résultat{{ total > 1 ? 's' : '' }} -

-
- - -
-
- - -

- Aucun commentaire trouvé. -

- - - diff --git a/app/pages/constructeurs.vue b/app/pages/constructeurs.vue index d9624a1..5c3d44b 100644 --- a/app/pages/constructeurs.vue +++ b/app/pages/constructeurs.vue @@ -17,73 +17,48 @@
-
-
- - -
-
- - -
-
+ + -
- - Chargement des fournisseurs... -
+ -
- Aucun fournisseur trouvé. -
+ -
- - - - - - - - - - - - - - - - - -
NomEmailTéléphone - Actions -
{{ constructeur.name }}{{ constructeur.email || '—' }}{{ formatPhoneDisplay(constructeur.phone) }} -
- - -
-
-
+ +
@@ -118,6 +93,7 @@ - - diff --git a/app/pages/documents.vue b/app/pages/documents.vue index 7ac5df2..f57a16b 100644 --- a/app/pages/documents.vue +++ b/app/pages/documents.vue @@ -3,22 +3,34 @@
-
-
+ + -
- - + -
- - + + + + + -
-
diff --git a/app/pages/product-catalog.vue b/app/pages/product-catalog.vue index 8363e4a..8cd50b6 100644 --- a/app/pages/product-catalog.vue +++ b/app/pages/product-catalog.vue @@ -19,51 +19,8 @@
-
-
- -
- - -
-
- - -
-
-

- {{ filteredCount }} / {{ totalCount }} résultat{{ filteredCount > 1 ? 's' : '' }} -

-
- -
-
-
@@ -75,96 +32,107 @@
-

- Chargement du catalogue… -

+ + -

- Aucun produit n'a encore été enregistré. -

+ -

- Aucun produit ne correspond à votre recherche. -

+ -
- - - - - - - - - - - - - - - - - - - - - - - -
AperçuNomRéférenceType de produitFournisseursPrix indicatifActions
- - {{ row.product.name }}{{ row.product.reference || '—' }} - - {{ row.product.typeProduct.name }} - - {{ row.product.typeProduct?.name || '—' }} - -
- - {{ supplier }} - - - +{{ row.suppliers.overflow }} - -
- -
- {{ formatPrice(row.product.supplierPrice) }} - - - Modifier - - -
-
+ + + + + + + + + +
@@ -173,24 +141,22 @@ diff --git a/app/shared/types/dataTable.ts b/app/shared/types/dataTable.ts new file mode 100644 index 0000000..55b3cb0 --- /dev/null +++ b/app/shared/types/dataTable.ts @@ -0,0 +1,46 @@ +export type SortDirection = 'asc' | 'desc' + +export interface DataTableColumn { + /** Unique key — used as slot name prefix (`#cell-{key}`) and default data accessor (`row[key]`) */ + key: string + /** Display label for the column header */ + label: string + /** Whether clicking this column header triggers sorting. Default: false */ + sortable?: boolean + /** Sort key sent to the API (may differ from `key`). Falls back to `key` if omitted. */ + sortKey?: string + /** CSS class applied to both and */ + class?: string + /** CSS class applied only to */ + headerClass?: string + /** Width hint (e.g. 'w-24', 'min-w-[10rem]') */ + width?: string + /** Text alignment: 'left' (default), 'center', 'right' */ + align?: 'left' | 'center' | 'right' + /** Hide on mobile (adds 'hidden sm:table-cell') */ + hiddenMobile?: boolean + /** Whether this column has a filter input under the header */ + filterable?: boolean + /** Placeholder for the filter input */ + filterPlaceholder?: string +} + +export type DataTableColumnFilters = Record + +export interface DataTableSort { + field: string + direction: SortDirection +} + +export interface DataTablePagination { + currentPage: number + totalPages: number + /** Total items matching current filters */ + totalItems: number + /** Items displayed on current page */ + pageItems: number + /** Available per-page options. If omitted, per-page selector is hidden. */ + perPageOptions?: number[] + /** Currently selected items per page */ + perPage?: number +}