refactor : merge Inventory_frontend submodule into frontend/ directory
Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
221
frontend/app/composables/useDataTable.ts
Normal file
221
frontend/app/composables/useDataTable.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { ref, computed, watch, 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<void>
|
||||
}
|
||||
|
||||
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<string, { default: string | number; type?: 'string' | 'number' }>
|
||||
/** Column filter keys to persist in URL (prefixed with `f.` in query string) */
|
||||
columnFilterKeys?: string[]
|
||||
}
|
||||
|
||||
export interface UseDataTableReturn {
|
||||
searchTerm: Ref<string>
|
||||
sortField: Ref<string>
|
||||
sortDirection: Ref<SortDirection>
|
||||
currentPage: Ref<number>
|
||||
itemsPerPage: Ref<number>
|
||||
columnFilters: Ref<DataTableColumnFilters>
|
||||
filters: Record<string, Ref<string | number>>
|
||||
sort: ComputedRef<DataTableSort>
|
||||
pagination: (total: Ref<number>, pageItems: Ref<number>) => ComputedRef<DataTablePagination>
|
||||
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 = {},
|
||||
columnFilterKeys = [],
|
||||
} = options
|
||||
|
||||
let searchTerm: Ref<string>
|
||||
let sortField: Ref<string>
|
||||
let sortDirection: Ref<SortDirection>
|
||||
let currentPage: Ref<number>
|
||||
let itemsPerPage: Ref<number>
|
||||
const filters: Record<string, Ref<string | number>> = {}
|
||||
const columnFilterRefs: Record<string, Ref<string>> = {}
|
||||
|
||||
if (persistToUrl) {
|
||||
const paramDefs: Record<string, { default: string | number; type?: 'string' | 'number'; debounce?: number }> = {
|
||||
page: { default: 1, type: 'number' },
|
||||
perPage: { default: defaultPerPage, type: 'number' },
|
||||
q: { default: '', debounce: searchDebounceMs },
|
||||
sort: { default: defaultSort },
|
||||
dir: { default: defaultDirection },
|
||||
...extraParams,
|
||||
}
|
||||
|
||||
for (const key of columnFilterKeys) {
|
||||
paramDefs[`f.${key}`] = { default: '', debounce: 300 }
|
||||
}
|
||||
|
||||
const state = useUrlState(paramDefs, {
|
||||
onRestore: () => deps.fetchData(),
|
||||
})
|
||||
|
||||
searchTerm = state.q as Ref<string>
|
||||
sortField = state.sort as Ref<string>
|
||||
sortDirection = state.dir as unknown as Ref<SortDirection>
|
||||
currentPage = state.page as unknown as Ref<number>
|
||||
itemsPerPage = state.perPage as unknown as Ref<number>
|
||||
|
||||
for (const key of Object.keys(extraParams)) {
|
||||
filters[key] = (state as Record<string, Ref<string | number>>)[key]!
|
||||
}
|
||||
|
||||
for (const key of columnFilterKeys) {
|
||||
columnFilterRefs[key] = (state as Record<string, Ref<string>>)[`f.${key}`]!
|
||||
}
|
||||
}
|
||||
else {
|
||||
searchTerm = ref('')
|
||||
sortField = ref(defaultSort)
|
||||
sortDirection = ref(defaultDirection) as Ref<SortDirection>
|
||||
currentPage = ref(1)
|
||||
itemsPerPage = ref(defaultPerPage)
|
||||
|
||||
for (const [key, def] of Object.entries(extraParams)) {
|
||||
filters[key] = ref(def.default)
|
||||
}
|
||||
}
|
||||
|
||||
// Search debounce
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const debouncedSearch = () => {
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
currentPage.value = 1
|
||||
deps.fetchData()
|
||||
}, searchDebounceMs)
|
||||
}
|
||||
|
||||
// Sort
|
||||
const sort = computed<DataTableSort>(() => ({
|
||||
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 — seed from URL-persisted refs
|
||||
const initialColumnFilters: DataTableColumnFilters = {}
|
||||
for (const [key, r] of Object.entries(columnFilterRefs)) {
|
||||
if (r.value) initialColumnFilters[key] = r.value
|
||||
}
|
||||
const columnFilters = ref<DataTableColumnFilters>(initialColumnFilters)
|
||||
|
||||
// Sync columnFilters → URL refs
|
||||
if (persistToUrl && columnFilterKeys.length > 0) {
|
||||
watch(columnFilters, (val) => {
|
||||
for (const key of columnFilterKeys) {
|
||||
columnFilterRefs[key]!.value = val[key] || ''
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// Sync URL refs → columnFilters (back/forward navigation)
|
||||
for (const key of columnFilterKeys) {
|
||||
watch(columnFilterRefs[key]!, (urlVal) => {
|
||||
const current = columnFilters.value[key] || ''
|
||||
if (current !== urlVal) {
|
||||
columnFilters.value = { ...columnFilters.value, [key]: urlVal }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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<number>, pageItems: Ref<number>): ComputedRef<DataTablePagination> =>
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user