feat(ui): ajoute la pagination et la recherche serveur
This commit is contained in:
128
app/components/common/Pagination.vue
Normal file
128
app/components/common/Pagination.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="totalPages > 1" class="flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
@click="goToPage(1)"
|
||||||
|
>
|
||||||
|
<IconLucideChevronFirst class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
@click="goToPage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
<IconLucideChevronLeft class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<template v-for="page in visiblePages" :key="page">
|
||||||
|
<span v-if="page === 'ellipsis-start' || page === 'ellipsis-end'" class="px-2">...</span>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="page === currentPage ? 'btn-primary' : 'btn-ghost'"
|
||||||
|
@click="goToPage(page)"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
:disabled="currentPage >= totalPages"
|
||||||
|
@click="goToPage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
<IconLucideChevronRight class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
:disabled="currentPage >= totalPages"
|
||||||
|
@click="goToPage(totalPages)"
|
||||||
|
>
|
||||||
|
<IconLucideChevronLast class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import IconLucideChevronFirst from '~icons/lucide/chevrons-left'
|
||||||
|
import IconLucideChevronLeft from '~icons/lucide/chevron-left'
|
||||||
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
||||||
|
import IconLucideChevronLast from '~icons/lucide/chevrons-right'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
currentPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
totalPages: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
maxVisiblePages: {
|
||||||
|
type: Number,
|
||||||
|
default: 5
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:currentPage'])
|
||||||
|
|
||||||
|
const visiblePages = computed(() => {
|
||||||
|
const pages = []
|
||||||
|
const total = props.totalPages
|
||||||
|
const current = props.currentPage
|
||||||
|
const maxVisible = props.maxVisiblePages
|
||||||
|
|
||||||
|
if (total <= maxVisible + 2) {
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show first page
|
||||||
|
pages.push(1)
|
||||||
|
|
||||||
|
const half = Math.floor(maxVisible / 2)
|
||||||
|
let start = Math.max(2, current - half)
|
||||||
|
let end = Math.min(total - 1, current + half)
|
||||||
|
|
||||||
|
// Adjust if near start
|
||||||
|
if (current <= half + 1) {
|
||||||
|
end = maxVisible
|
||||||
|
}
|
||||||
|
// Adjust if near end
|
||||||
|
if (current >= total - half) {
|
||||||
|
start = total - maxVisible + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start > 2) {
|
||||||
|
pages.push('ellipsis-start')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end < total - 1) {
|
||||||
|
pages.push('ellipsis-end')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show last page
|
||||||
|
pages.push(total)
|
||||||
|
|
||||||
|
return pages
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToPage = (page) => {
|
||||||
|
if (page >= 1 && page <= props.totalPages && page !== props.currentPage) {
|
||||||
|
emit('update:currentPage', page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -6,6 +6,7 @@ import { useConstructeurs } from './useConstructeurs'
|
|||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const composants = ref([])
|
const composants = ref([])
|
||||||
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const extractCollection = (payload) => {
|
const extractCollection = (payload) => {
|
||||||
@@ -24,6 +25,16 @@ const extractCollection = (payload) => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractTotal = (payload, fallbackLength) => {
|
||||||
|
if (typeof payload?.totalItems === 'number') {
|
||||||
|
return payload.totalItems
|
||||||
|
}
|
||||||
|
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||||
|
return payload['hydra:totalItems']
|
||||||
|
}
|
||||||
|
return fallbackLength
|
||||||
|
}
|
||||||
|
|
||||||
export function useComposants () {
|
export function useComposants () {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess, showError, showInfo } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
@@ -65,18 +76,56 @@ export function useComposants () {
|
|||||||
return composant
|
return composant
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadComposants = async () => {
|
/**
|
||||||
|
* Load composants with pagination and search support
|
||||||
|
* @param {Object} options - Query options
|
||||||
|
* @param {string} [options.search] - Search term for name/reference
|
||||||
|
* @param {number} [options.page=1] - Current page (1-based)
|
||||||
|
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||||
|
* @param {string} [options.orderBy='name'] - Field to order by
|
||||||
|
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||||
|
*/
|
||||||
|
const loadComposants = async (options = {}) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await get('/composants')
|
const {
|
||||||
|
search = '',
|
||||||
|
page = 1,
|
||||||
|
itemsPerPage = 30,
|
||||||
|
orderBy = 'name',
|
||||||
|
orderDir = 'asc'
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
|
params.set('page', String(page))
|
||||||
|
|
||||||
|
if (search && search.trim()) {
|
||||||
|
params.set('name', search.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
|
const result = await get(`/composants?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = extractCollection(result.data)
|
const items = extractCollection(result.data)
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
composants.value = enrichedItems
|
composants.value = enrichedItems
|
||||||
showInfo(`Chargement de ${composants.value.length} composant(s) réussi`)
|
total.value = extractTotal(result.data, items.length)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
items: enrichedItems,
|
||||||
|
total: total.value,
|
||||||
|
page,
|
||||||
|
itemsPerPage
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du chargement des composants:', error)
|
console.error('Erreur lors du chargement des composants:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -89,7 +138,8 @@ export function useComposants () {
|
|||||||
const result = await post('/composants', normalizedPayload)
|
const result = await post('/composants', normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
composants.value.push(enriched)
|
composants.value.unshift(enriched)
|
||||||
|
total.value += 1
|
||||||
const displayName = result.data?.name
|
const displayName = result.data?.name
|
||||||
|| composantData?.definition?.name
|
|| composantData?.definition?.name
|
||||||
|| composantData?.name
|
|| composantData?.name
|
||||||
@@ -134,6 +184,7 @@ export function useComposants () {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
const deletedComposant = composants.value.find(comp => comp.id === id)
|
const deletedComposant = composants.value.find(comp => comp.id === id)
|
||||||
composants.value = composants.value.filter(comp => comp.id !== id)
|
composants.value = composants.value.filter(comp => comp.id !== id)
|
||||||
|
total.value = Math.max(0, total.value - 1)
|
||||||
showSuccess(`Composant "${deletedComposant?.name || 'inconnu'}" supprimé avec succès`)
|
showSuccess(`Composant "${deletedComposant?.name || 'inconnu'}" supprimé avec succès`)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -150,6 +201,7 @@ export function useComposants () {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
composants,
|
composants,
|
||||||
|
total,
|
||||||
loading,
|
loading,
|
||||||
loadComposants,
|
loadComposants,
|
||||||
createComposant,
|
createComposant,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useConstructeurs } from './useConstructeurs'
|
|||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const pieces = ref([])
|
const pieces = ref([])
|
||||||
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const extractCollection = (payload) => {
|
const extractCollection = (payload) => {
|
||||||
@@ -24,6 +25,16 @@ const extractCollection = (payload) => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractTotal = (payload, fallbackLength) => {
|
||||||
|
if (typeof payload?.totalItems === 'number') {
|
||||||
|
return payload.totalItems
|
||||||
|
}
|
||||||
|
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||||
|
return payload['hydra:totalItems']
|
||||||
|
}
|
||||||
|
return fallbackLength
|
||||||
|
}
|
||||||
|
|
||||||
export function usePieces () {
|
export function usePieces () {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess, showError, showInfo } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
@@ -65,18 +76,58 @@ export function usePieces () {
|
|||||||
return piece
|
return piece
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadPieces = async () => {
|
/**
|
||||||
|
* Load pieces with pagination and search support
|
||||||
|
* @param {Object} options - Query options
|
||||||
|
* @param {string} [options.search] - Search term for name/reference
|
||||||
|
* @param {number} [options.page=1] - Current page (1-based)
|
||||||
|
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||||
|
* @param {string} [options.orderBy='name'] - Field to order by
|
||||||
|
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||||
|
*/
|
||||||
|
const loadPieces = async (options = {}) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await get('/pieces')
|
const {
|
||||||
|
search = '',
|
||||||
|
page = 1,
|
||||||
|
itemsPerPage = 30,
|
||||||
|
orderBy = 'name',
|
||||||
|
orderDir = 'asc'
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
|
params.set('page', String(page))
|
||||||
|
|
||||||
|
if (search && search.trim()) {
|
||||||
|
// API Platform uses property filters
|
||||||
|
params.set('name', search.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Platform OrderFilter syntax: order[field]=direction
|
||||||
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
|
const result = await get(`/pieces?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = extractCollection(result.data)
|
const items = extractCollection(result.data)
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
pieces.value = enrichedItems
|
pieces.value = enrichedItems
|
||||||
showInfo(`Chargement de ${pieces.value.length} pièce(s) réussi`)
|
total.value = extractTotal(result.data, items.length)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
items: enrichedItems,
|
||||||
|
total: total.value,
|
||||||
|
page,
|
||||||
|
itemsPerPage
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du chargement des pièces:', error)
|
console.error('Erreur lors du chargement des pièces:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -89,7 +140,8 @@ export function usePieces () {
|
|||||||
const result = await post('/pieces', normalizedPayload)
|
const result = await post('/pieces', normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
pieces.value.push(enriched)
|
pieces.value.unshift(enriched)
|
||||||
|
total.value += 1
|
||||||
const displayName = result.data?.name
|
const displayName = result.data?.name
|
||||||
|| pieceData?.definition?.name
|
|| pieceData?.definition?.name
|
||||||
|| pieceData?.name
|
|| pieceData?.name
|
||||||
@@ -134,6 +186,7 @@ export function usePieces () {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
const deletedPiece = pieces.value.find(piece => piece.id === id)
|
const deletedPiece = pieces.value.find(piece => piece.id === id)
|
||||||
pieces.value = pieces.value.filter(piece => piece.id !== id)
|
pieces.value = pieces.value.filter(piece => piece.id !== id)
|
||||||
|
total.value = Math.max(0, total.value - 1)
|
||||||
showSuccess(`Pièce "${deletedPiece?.name || 'inconnu'}" supprimée avec succès`)
|
showSuccess(`Pièce "${deletedPiece?.name || 'inconnu'}" supprimée avec succès`)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -150,6 +203,7 @@ export function usePieces () {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
pieces,
|
pieces,
|
||||||
|
total,
|
||||||
loading,
|
loading,
|
||||||
loadPieces,
|
loadPieces,
|
||||||
createPiece,
|
createPiece,
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ const extractCollection = (payload) => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractTotal = (payload, fallbackLength) => {
|
||||||
|
if (typeof payload?.totalItems === 'number') {
|
||||||
|
return payload.totalItems
|
||||||
|
}
|
||||||
|
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||||
|
return payload['hydra:totalItems']
|
||||||
|
}
|
||||||
|
return fallbackLength
|
||||||
|
}
|
||||||
|
|
||||||
export function useProducts () {
|
export function useProducts () {
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
@@ -77,32 +87,62 @@ export function useProducts () {
|
|||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load products with pagination and search support
|
||||||
|
* @param {Object} options - Query options
|
||||||
|
* @param {string} [options.search] - Search term for name/reference
|
||||||
|
* @param {number} [options.page=1] - Current page (1-based)
|
||||||
|
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||||
|
* @param {string} [options.orderBy='name'] - Field to order by
|
||||||
|
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||||
|
* @param {boolean} [options.force=false] - Force reload even if already loaded
|
||||||
|
*/
|
||||||
const loadProducts = async (options = {}) => {
|
const loadProducts = async (options = {}) => {
|
||||||
|
const {
|
||||||
|
search = '',
|
||||||
|
page = 1,
|
||||||
|
itemsPerPage = 30,
|
||||||
|
orderBy = 'name',
|
||||||
|
orderDir = 'asc',
|
||||||
|
force = false
|
||||||
|
} = options
|
||||||
|
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { items: products.value, total: total.value },
|
data: { items: products.value, total: total.value, page, itemsPerPage },
|
||||||
}
|
|
||||||
}
|
|
||||||
if (loaded.value && !options.force) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: { items: products.value, total: total.value },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const result = await get('/products?itemsPerPage=100')
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
|
params.set('page', String(page))
|
||||||
|
|
||||||
|
if (search && search.trim()) {
|
||||||
|
params.set('name', search.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
|
const result = await get(`/products?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = extractCollection(result.data)
|
const items = extractCollection(result.data)
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
products.value = enrichedItems
|
products.value = enrichedItems
|
||||||
total.value = typeof result.data?.totalItems === 'number'
|
total.value = extractTotal(result.data, items.length)
|
||||||
? result.data.totalItems
|
|
||||||
: items.length
|
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
items: enrichedItems,
|
||||||
|
total: total.value,
|
||||||
|
page,
|
||||||
|
itemsPerPage
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
error.value = result.error
|
error.value = result.error
|
||||||
showError(`Impossible de charger les produits: ${result.error}`)
|
showError(`Impossible de charger les produits: ${result.error}`)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered input-sm w-full mt-1"
|
class="input input-bordered input-sm w-full mt-1"
|
||||||
placeholder="Nom ou référence…"
|
placeholder="Nom ou référence…"
|
||||||
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
id="component-catalog-sort"
|
id="component-catalog-sort"
|
||||||
v-model="sortField"
|
v-model="sortField"
|
||||||
class="select select-bordered select-sm"
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleSortChange"
|
||||||
>
|
>
|
||||||
<option value="name">Nom</option>
|
<option value="name">Nom</option>
|
||||||
<option value="createdAt">Date de création</option>
|
<option value="createdAt">Date de création</option>
|
||||||
@@ -64,14 +66,33 @@
|
|||||||
id="component-catalog-dir"
|
id="component-catalog-dir"
|
||||||
v-model="sortDirection"
|
v-model="sortDirection"
|
||||||
class="select select-bordered select-sm"
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleSortChange"
|
||||||
>
|
>
|
||||||
<option value="asc">Ascendant</option>
|
<option value="asc">Ascendant</option>
|
||||||
<option value="desc">Descendant</option>
|
<option value="desc">Descendant</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label
|
||||||
|
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
||||||
|
for="component-catalog-per-page"
|
||||||
|
>
|
||||||
|
Par page
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="component-catalog-per-page"
|
||||||
|
v-model.number="itemsPerPage"
|
||||||
|
class="select select-bordered select-sm"
|
||||||
|
@change="handlePerPageChange"
|
||||||
|
>
|
||||||
|
<option :value="20">20</option>
|
||||||
|
<option :value="50">50</option>
|
||||||
|
<option :value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-base-content/50 lg:text-right">
|
<p class="text-xs text-base-content/50 lg:text-right">
|
||||||
{{ visibleComposants.length }} / {{ composantsTotal }} résultat{{ visibleComposants.length > 1 ? 's' : '' }}
|
{{ composantsOnPage }} / {{ composantsTotal }} résultat{{ composantsTotal > 1 ? 's' : '' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,54 +104,62 @@
|
|||||||
Aucun composant n'a encore été créé.
|
Aucun composant n'a encore été créé.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-else-if="!visibleComposants.length" class="text-sm text-base-content/70">
|
<p v-else-if="!composantsList.length" class="text-sm text-base-content/70">
|
||||||
Aucun composant ne correspond à votre recherche.
|
Aucun composant ne correspond à votre recherche.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-else class="overflow-x-auto">
|
<template v-else>
|
||||||
<table class="table table-sm md:table-md">
|
<div class="overflow-x-auto">
|
||||||
<thead>
|
<table class="table table-sm md:table-md">
|
||||||
<tr>
|
<thead>
|
||||||
<th class="w-24">Aperçu</th>
|
<tr>
|
||||||
<th>Nom</th>
|
<th class="w-24">Aperçu</th>
|
||||||
<th>Référence</th>
|
<th>Nom</th>
|
||||||
<th>Type de composant</th>
|
<th>Référence</th>
|
||||||
<th>Actions</th>
|
<th>Type de composant</th>
|
||||||
</tr>
|
<th>Actions</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr v-for="component in visibleComposants" :key="component.id">
|
<tbody>
|
||||||
<td class="align-middle">
|
<tr v-for="component in composantsList" :key="component.id">
|
||||||
<DocumentThumbnail
|
<td class="align-middle">
|
||||||
:document="resolvePrimaryDocument(component)"
|
<DocumentThumbnail
|
||||||
:alt="resolvePreviewAlt(component)"
|
:document="resolvePrimaryDocument(component)"
|
||||||
/>
|
:alt="resolvePreviewAlt(component)"
|
||||||
</td>
|
/>
|
||||||
<td>{{ component.name || 'Composant sans nom' }}</td>
|
</td>
|
||||||
<td>{{ component.reference || '—' }}</td>
|
<td>{{ component.name || 'Composant sans nom' }}</td>
|
||||||
<td>{{ resolveComponentType(component) }}</td>
|
<td>{{ component.reference || '—' }}</td>
|
||||||
<td>
|
<td>{{ resolveComponentType(component) }}</td>
|
||||||
<div class="flex items-center gap-2">
|
<td>
|
||||||
<NuxtLink
|
<div class="flex items-center gap-2">
|
||||||
:to="`/component/${component.id}/edit`"
|
<NuxtLink
|
||||||
class="btn btn-ghost btn-xs"
|
:to="`/component/${component.id}/edit`"
|
||||||
>
|
class="btn btn-ghost btn-xs"
|
||||||
Modifier
|
>
|
||||||
</NuxtLink>
|
Modifier
|
||||||
<button
|
</NuxtLink>
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-error btn-xs"
|
type="button"
|
||||||
:disabled="loadingComposants"
|
class="btn btn-error btn-xs"
|
||||||
@click="handleDeleteComponent(component)"
|
:disabled="loadingComposants"
|
||||||
>
|
@click="handleDeleteComponent(component)"
|
||||||
Supprimer
|
>
|
||||||
</button>
|
Supprimer
|
||||||
</div>
|
</button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
:current-page="currentPage"
|
||||||
|
:total-pages="totalPages"
|
||||||
|
@update:current-page="handlePageChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -144,13 +173,41 @@ import { useComponentTypes } from '~/composables/useComponentTypes'
|
|||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { usePersistedSort } from '~/composables/usePersistedSort'
|
import { usePersistedSort } from '~/composables/usePersistedSort'
|
||||||
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
||||||
|
import Pagination from '~/components/common/Pagination.vue'
|
||||||
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
const { composants, loadComposants, loading: loadingComposantsRef, deleteComposant } = useComposants()
|
const { composants, total, loadComposants, loading: loadingComposantsRef, deleteComposant } = useComposants()
|
||||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||||
const loadingComposants = computed(() => loadingComposantsRef.value)
|
const loadingComposants = computed(() => loadingComposantsRef.value)
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const itemsPerPage = ref(30)
|
||||||
|
const composantsTotal = computed(() => total.value)
|
||||||
|
const composantsOnPage = computed(() => composants.value.length)
|
||||||
|
const totalPages = computed(() => Math.ceil(composantsTotal.value / itemsPerPage.value) || 1)
|
||||||
|
|
||||||
|
// Search state with debounce
|
||||||
|
const searchTerm = ref('')
|
||||||
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const debouncedSearch = () => {
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchComposants()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort state
|
||||||
|
const { sortField, sortDirection } = usePersistedSort<'name' | 'createdAt', 'asc' | 'desc'>(
|
||||||
|
'component-catalog',
|
||||||
|
{ field: 'name', direction: 'asc' },
|
||||||
|
)
|
||||||
|
|
||||||
// Enrichir les composants avec les types de composants complets
|
// Enrichir les composants avec les types de composants complets
|
||||||
const composantsList = computed(() => {
|
const composantsList = computed(() => {
|
||||||
return (composants.value || []).map((composant) => {
|
return (composants.value || []).map((composant) => {
|
||||||
@@ -161,13 +218,31 @@ const composantsList = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const composantsTotal = computed(() => composantsList.value.length)
|
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const fetchComposants = async () => {
|
||||||
const { sortField, sortDirection } = usePersistedSort<'name' | 'createdAt', 'asc' | 'desc'>(
|
await loadComposants({
|
||||||
'component-catalog',
|
search: searchTerm.value,
|
||||||
{ field: 'name', direction: 'asc' },
|
page: currentPage.value,
|
||||||
)
|
itemsPerPage: itemsPerPage.value,
|
||||||
|
orderBy: sortField.value,
|
||||||
|
orderDir: sortDirection.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
currentPage.value = page
|
||||||
|
fetchComposants()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSortChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchComposants()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePerPageChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchComposants()
|
||||||
|
}
|
||||||
|
|
||||||
const resolvePrimaryDocument = (component: Record<string, any>) => {
|
const resolvePrimaryDocument = (component: Record<string, any>) => {
|
||||||
const documents = Array.isArray(component?.documents) ? component.documents : []
|
const documents = Array.isArray(component?.documents) ? component.documents : []
|
||||||
@@ -230,58 +305,6 @@ const resolveDeleteGuard = (component: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveComparableName = (component: Record<string, any>) => {
|
|
||||||
const toComparable = (value?: string | null) =>
|
|
||||||
(value ?? '').toString().trim().toLowerCase()
|
|
||||||
|
|
||||||
return (
|
|
||||||
toComparable(component?.name) ||
|
|
||||||
toComparable(component?.reference) ||
|
|
||||||
toComparable(component?.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveComparableDate = (component: Record<string, any>) => {
|
|
||||||
const raw = component?.createdAt ?? component?.created_at ?? null
|
|
||||||
if (!raw) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
const parsed = new Date(raw).getTime()
|
|
||||||
return Number.isNaN(parsed) ? 0 : parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleComposants = computed(() => {
|
|
||||||
const term = searchTerm.value.trim().toLowerCase()
|
|
||||||
const source = composantsList.value || []
|
|
||||||
|
|
||||||
const filtered = term
|
|
||||||
? source.filter((component) => {
|
|
||||||
const name = (component?.name || '').toLowerCase()
|
|
||||||
const reference = (component?.reference || '').toLowerCase()
|
|
||||||
return (
|
|
||||||
name.includes(term) ||
|
|
||||||
reference.includes(term)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
: [...source]
|
|
||||||
|
|
||||||
const direction = sortDirection.value === 'asc' ? 1 : -1
|
|
||||||
|
|
||||||
return filtered.sort((a, b) => {
|
|
||||||
if (sortField.value === 'name') {
|
|
||||||
return (
|
|
||||||
resolveComparableName(a).localeCompare(
|
|
||||||
resolveComparableName(b),
|
|
||||||
'fr',
|
|
||||||
{ sensitivity: 'base' }
|
|
||||||
) * direction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (resolveComparableDate(a) - resolveComparableDate(b)) * direction
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleDeleteComponent = async (component: Record<string, any>) => {
|
const handleDeleteComponent = async (component: Record<string, any>) => {
|
||||||
const { blockingReasons, hasCustomFields } = resolveDeleteGuard(component)
|
const { blockingReasons, hasCustomFields } = resolveDeleteGuard(component)
|
||||||
|
|
||||||
@@ -310,11 +333,13 @@ const handleDeleteComponent = async (component: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await deleteComposant(component.id)
|
await deleteComposant(component.id)
|
||||||
|
// Reload current page after deletion
|
||||||
|
fetchComposants()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadComposants(),
|
fetchComposants(),
|
||||||
loadComponentTypes()
|
loadComponentTypes()
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered input-sm w-full mt-1"
|
class="input input-bordered input-sm w-full mt-1"
|
||||||
placeholder="Nom ou référence…"
|
placeholder="Nom ou référence…"
|
||||||
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
id="piece-catalog-sort"
|
id="piece-catalog-sort"
|
||||||
v-model="sortField"
|
v-model="sortField"
|
||||||
class="select select-bordered select-sm"
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleSortChange"
|
||||||
>
|
>
|
||||||
<option value="name">Nom</option>
|
<option value="name">Nom</option>
|
||||||
<option value="createdAt">Date de création</option>
|
<option value="createdAt">Date de création</option>
|
||||||
@@ -63,14 +65,33 @@
|
|||||||
id="piece-catalog-dir"
|
id="piece-catalog-dir"
|
||||||
v-model="sortDirection"
|
v-model="sortDirection"
|
||||||
class="select select-bordered select-sm"
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleSortChange"
|
||||||
>
|
>
|
||||||
<option value="asc">Ascendant</option>
|
<option value="asc">Ascendant</option>
|
||||||
<option value="desc">Descendant</option>
|
<option value="desc">Descendant</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label
|
||||||
|
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
||||||
|
for="piece-catalog-per-page"
|
||||||
|
>
|
||||||
|
Par page
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="piece-catalog-per-page"
|
||||||
|
v-model.number="itemsPerPage"
|
||||||
|
class="select select-bordered select-sm"
|
||||||
|
@change="handlePerPageChange"
|
||||||
|
>
|
||||||
|
<option :value="20">20</option>
|
||||||
|
<option :value="50">50</option>
|
||||||
|
<option :value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-base-content/50 lg:text-right">
|
<p class="text-xs text-base-content/50 lg:text-right">
|
||||||
{{ visiblePieces.length }} / {{ piecesTotal }} résultat{{ visiblePieces.length > 1 ? 's' : '' }}
|
{{ piecesOnPage }} / {{ piecesTotal }} résultat{{ piecesTotal > 1 ? 's' : '' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -82,77 +103,85 @@
|
|||||||
Aucune pièce n'a encore été créée.
|
Aucune pièce n'a encore été créée.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-else-if="!visiblePieces.length" class="text-sm text-base-content/70">
|
<p v-else-if="!piecesList.length" class="text-sm text-base-content/70">
|
||||||
Aucune pièce ne correspond à votre recherche.
|
Aucune pièce ne correspond à votre recherche.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-else class="overflow-x-auto">
|
<template v-else>
|
||||||
<table class="table table-sm md:table-md">
|
<div class="overflow-x-auto">
|
||||||
<thead>
|
<table class="table table-sm md:table-md">
|
||||||
<tr>
|
<thead>
|
||||||
<th class="w-24">Aperçu</th>
|
<tr>
|
||||||
<th>Nom</th>
|
<th class="w-24">Aperçu</th>
|
||||||
<th>Référence</th>
|
<th>Nom</th>
|
||||||
<th>Fournisseurs</th>
|
<th>Référence</th>
|
||||||
<th>Type de pièce</th>
|
<th>Fournisseurs</th>
|
||||||
<th>Actions</th>
|
<th>Type de pièce</th>
|
||||||
</tr>
|
<th>Actions</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr v-for="row in pieceRows" :key="row.piece.id">
|
<tbody>
|
||||||
<td class="align-middle">
|
<tr v-for="row in pieceRows" :key="row.piece.id">
|
||||||
<DocumentThumbnail
|
<td class="align-middle">
|
||||||
:document="resolvePrimaryDocument(row.piece)"
|
<DocumentThumbnail
|
||||||
:alt="resolvePreviewAlt(row.piece)"
|
:document="resolvePrimaryDocument(row.piece)"
|
||||||
/>
|
:alt="resolvePreviewAlt(row.piece)"
|
||||||
</td>
|
/>
|
||||||
<td>{{ row.piece.name || 'Pièce sans nom' }}</td>
|
</td>
|
||||||
<td>{{ row.piece.reference || '—' }}</td>
|
<td>{{ row.piece.name || 'Pièce sans nom' }}</td>
|
||||||
<td>
|
<td>{{ row.piece.reference || '—' }}</td>
|
||||||
<div
|
<td>
|
||||||
v-if="row.suppliers.visible.length"
|
<div
|
||||||
class="flex max-w-[14rem] flex-wrap items-center gap-1"
|
v-if="row.suppliers.visible.length"
|
||||||
:title="row.suppliers.tooltip"
|
class="flex max-w-[14rem] flex-wrap items-center gap-1"
|
||||||
>
|
:title="row.suppliers.tooltip"
|
||||||
<span
|
|
||||||
v-for="supplier in row.suppliers.visible"
|
|
||||||
:key="supplier"
|
|
||||||
class="badge badge-ghost badge-sm whitespace-nowrap"
|
|
||||||
>
|
>
|
||||||
{{ supplier }}
|
<span
|
||||||
</span>
|
v-for="supplier in row.suppliers.visible"
|
||||||
<span
|
:key="supplier"
|
||||||
v-if="row.suppliers.overflow"
|
class="badge badge-ghost badge-sm whitespace-nowrap"
|
||||||
class="badge badge-outline badge-sm"
|
>
|
||||||
>
|
{{ supplier }}
|
||||||
+{{ row.suppliers.overflow }}
|
</span>
|
||||||
</span>
|
<span
|
||||||
</div>
|
v-if="row.suppliers.overflow"
|
||||||
<span v-else>—</span>
|
class="badge badge-outline badge-sm"
|
||||||
</td>
|
>
|
||||||
<td>{{ resolvePieceType(row.piece) }}</td>
|
+{{ row.suppliers.overflow }}
|
||||||
<td>
|
</span>
|
||||||
<div class="flex items-center gap-2">
|
</div>
|
||||||
<NuxtLink
|
<span v-else>—</span>
|
||||||
:to="`/pieces/${row.piece.id}/edit`"
|
</td>
|
||||||
class="btn btn-ghost btn-xs"
|
<td>{{ resolvePieceType(row.piece) }}</td>
|
||||||
>
|
<td>
|
||||||
Modifier
|
<div class="flex items-center gap-2">
|
||||||
</NuxtLink>
|
<NuxtLink
|
||||||
<button
|
:to="`/pieces/${row.piece.id}/edit`"
|
||||||
type="button"
|
class="btn btn-ghost btn-xs"
|
||||||
class="btn btn-error btn-xs"
|
>
|
||||||
:disabled="loadingPieces"
|
Modifier
|
||||||
@click="handleDeletePiece(row.piece)"
|
</NuxtLink>
|
||||||
>
|
<button
|
||||||
Supprimer
|
type="button"
|
||||||
</button>
|
class="btn btn-error btn-xs"
|
||||||
</div>
|
:disabled="loadingPieces"
|
||||||
</td>
|
@click="handleDeletePiece(row.piece)"
|
||||||
</tr>
|
>
|
||||||
</tbody>
|
Supprimer
|
||||||
</table>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
:current-page="currentPage"
|
||||||
|
:total-pages="totalPages"
|
||||||
|
@update:current-page="handlePageChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -160,19 +189,47 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { usePieces } from '~/composables/usePieces'
|
import { usePieces } from '~/composables/usePieces'
|
||||||
import { usePieceTypes } from '~/composables/usePieceTypes'
|
import { usePieceTypes } from '~/composables/usePieceTypes'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { usePersistedSort } from '~/composables/usePersistedSort'
|
import { usePersistedSort } from '~/composables/usePersistedSort'
|
||||||
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
||||||
|
import Pagination from '~/components/common/Pagination.vue'
|
||||||
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
const { pieces, loadPieces, loading: loadingPiecesRef, deletePiece } = usePieces()
|
const { pieces, total, loadPieces, loading: loadingPiecesRef, deletePiece } = usePieces()
|
||||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||||
const loadingPieces = computed(() => loadingPiecesRef.value)
|
const loadingPieces = computed(() => loadingPiecesRef.value)
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const itemsPerPage = ref(30)
|
||||||
|
const piecesTotal = computed(() => total.value)
|
||||||
|
const piecesOnPage = computed(() => pieces.value.length)
|
||||||
|
const totalPages = computed(() => Math.ceil(piecesTotal.value / itemsPerPage.value) || 1)
|
||||||
|
|
||||||
|
// Search state with debounce
|
||||||
|
const searchTerm = ref('')
|
||||||
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const debouncedSearch = () => {
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchPieces()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort state
|
||||||
|
const { sortField, sortDirection } = usePersistedSort<'name' | 'createdAt', 'asc' | 'desc'>(
|
||||||
|
'pieces-catalog',
|
||||||
|
{ field: 'name', direction: 'asc' },
|
||||||
|
)
|
||||||
|
|
||||||
// Enrichir les pièces avec les types de pièces complets
|
// Enrichir les pièces avec les types de pièces complets
|
||||||
const piecesList = computed(() => {
|
const piecesList = computed(() => {
|
||||||
return (pieces.value || []).map((piece) => {
|
return (pieces.value || []).map((piece) => {
|
||||||
@@ -183,13 +240,31 @@ const piecesList = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const piecesTotal = computed(() => piecesList.value.length)
|
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const fetchPieces = async () => {
|
||||||
const { sortField, sortDirection } = usePersistedSort<'name' | 'createdAt', 'asc' | 'desc'>(
|
await loadPieces({
|
||||||
'pieces-catalog',
|
search: searchTerm.value,
|
||||||
{ field: 'name', direction: 'asc' },
|
page: currentPage.value,
|
||||||
)
|
itemsPerPage: itemsPerPage.value,
|
||||||
|
orderBy: sortField.value,
|
||||||
|
orderDir: sortDirection.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
currentPage.value = page
|
||||||
|
fetchPieces()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSortChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchPieces()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePerPageChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchPieces()
|
||||||
|
}
|
||||||
|
|
||||||
const resolvePrimaryDocument = (piece: Record<string, any>) => {
|
const resolvePrimaryDocument = (piece: Record<string, any>) => {
|
||||||
const documents = Array.isArray(piece?.documents) ? piece.documents : []
|
const documents = Array.isArray(piece?.documents) ? piece.documents : []
|
||||||
@@ -337,60 +412,8 @@ const resolveDeleteGuard = (piece: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveComparableName = (piece: Record<string, any>) => {
|
|
||||||
const normalise = (value?: string | null) =>
|
|
||||||
(value ?? '').toString().trim().toLowerCase()
|
|
||||||
|
|
||||||
return (
|
|
||||||
normalise(piece?.name) ||
|
|
||||||
normalise(piece?.reference) ||
|
|
||||||
normalise(piece?.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveComparableDate = (piece: Record<string, any>) => {
|
|
||||||
const raw = piece?.createdAt ?? piece?.created_at ?? null
|
|
||||||
if (!raw) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
const timestamp = new Date(raw).getTime()
|
|
||||||
return Number.isNaN(timestamp) ? 0 : timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
const visiblePieces = computed(() => {
|
|
||||||
const term = searchTerm.value.trim().toLowerCase()
|
|
||||||
const source = piecesList.value || []
|
|
||||||
|
|
||||||
const filtered = term
|
|
||||||
? source.filter((piece) => {
|
|
||||||
const name = (piece?.name || '').toLowerCase()
|
|
||||||
const reference = (piece?.reference || '').toLowerCase()
|
|
||||||
return (
|
|
||||||
name.includes(term) ||
|
|
||||||
reference.includes(term)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
: [...source]
|
|
||||||
|
|
||||||
const direction = sortDirection.value === 'asc' ? 1 : -1
|
|
||||||
|
|
||||||
return filtered.sort((a, b) => {
|
|
||||||
if (sortField.value === 'name') {
|
|
||||||
return (
|
|
||||||
resolveComparableName(a).localeCompare(
|
|
||||||
resolveComparableName(b),
|
|
||||||
'fr',
|
|
||||||
{ sensitivity: 'base' }
|
|
||||||
) * direction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (resolveComparableDate(a) - resolveComparableDate(b)) * direction
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const pieceRows = computed(() =>
|
const pieceRows = computed(() =>
|
||||||
visiblePieces.value.map((piece) => ({
|
piecesList.value.map((piece) => ({
|
||||||
piece,
|
piece,
|
||||||
suppliers: buildPieceSuppliersDisplay(piece),
|
suppliers: buildPieceSuppliersDisplay(piece),
|
||||||
})),
|
})),
|
||||||
@@ -425,11 +448,13 @@ const handleDeletePiece = async (piece: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await deletePiece(piece.id)
|
await deletePiece(piece.id)
|
||||||
|
// Reload current page after deletion
|
||||||
|
fetchPieces()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadPieces(),
|
fetchPieces(),
|
||||||
loadPieceTypes()
|
loadPieceTypes()
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user