Merge branch 'fix/filtres-listes' into develop

This commit is contained in:
Matthieu
2026-02-11 16:50:39 +01:00
20 changed files with 259 additions and 119 deletions

View File

@@ -92,11 +92,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { computed, onBeforeUnmount, onMounted, ref, watch, type Ref } from "vue";
import { useHead, useRouter } from "#imports"; import { useHead, useRouter } from "#imports";
import ModelTypesToolbar from "~/components/model-types/Toolbar.vue"; import ModelTypesToolbar from "~/components/model-types/Toolbar.vue";
import ModelTypesTable from "~/components/model-types/Table.vue"; import ModelTypesTable from "~/components/model-types/Table.vue";
import { useApi } from "~/composables/useApi"; import { useApi } from "~/composables/useApi";
import { useUrlState } from "~/composables/useUrlState";
import { extractCollection } from "~/shared/utils/apiHelpers"; import { extractCollection } from "~/shared/utils/apiHelpers";
import { import {
deleteModelType, deleteModelType,
@@ -125,11 +126,28 @@ const props = withDefaults(
const selectedCategory = ref<ModelCategory>(props.category); const selectedCategory = ref<ModelCategory>(props.category);
const searchInput = ref(""); const searchInput = ref("");
const searchTerm = ref("");
const sort = ref<"name" | "createdAt">("name"); // State synced with URL query params (preserved on back/forward navigation)
const dir = ref<"asc" | "desc">("asc"); const urlState = useUrlState({
const limit = ref(20); q: { default: '' },
const offset = ref(0); sort: { default: 'name' },
dir: { default: 'asc' },
limit: { default: 20, type: 'number' },
offset: { default: 0, type: 'number' },
}, {
onRestore: () => {
searchInput.value = urlState.q.value;
refresh();
},
});
const searchTerm = urlState.q;
const sort = urlState.sort as Ref<'name' | 'createdAt'>;
const dir = urlState.dir as Ref<'asc' | 'desc'>;
const limit = urlState.limit;
const offset = urlState.offset;
// Initialize searchInput from URL (for direct navigation with ?q=...)
searchInput.value = searchTerm.value;
const items = ref<ModelType[]>([]); const items = ref<ModelType[]>([]);
const total = ref(0); const total = ref(0);

View File

@@ -49,11 +49,12 @@ export function useDocuments() {
const loadFromEndpoint = async ( const loadFromEndpoint = async (
endpoint: string, endpoint: string,
{ updateStore = false }: { updateStore?: boolean } = {}, { updateStore = false, itemsPerPage }: { updateStore?: boolean; itemsPerPage?: number } = {},
): Promise<DocumentResult> => { ): Promise<DocumentResult> => {
loading.value = true loading.value = true
try { try {
const result = await get(endpoint) const url = itemsPerPage ? `${endpoint}${endpoint.includes('?') ? '&' : '?'}itemsPerPage=${itemsPerPage}` : endpoint
const result = await get(url)
if (result.success) { if (result.success) {
const data = extractCollection(result.data) const data = extractCollection(result.data)
if (updateStore) { if (updateStore) {
@@ -76,9 +77,9 @@ export function useDocuments() {
} }
const loadDocuments = async ( const loadDocuments = async (
options: { updateStore?: boolean } = {}, options: { updateStore?: boolean; itemsPerPage?: number } = {},
): Promise<DocumentResult> => { ): Promise<DocumentResult> => {
return loadFromEndpoint('/documents', { updateStore: options.updateStore ?? true }) return loadFromEndpoint('/documents', { updateStore: options.updateStore ?? true, itemsPerPage: options.itemsPerPage })
} }
const loadDocumentsBySite = async ( const loadDocumentsBySite = async (

View File

@@ -0,0 +1,116 @@
import { ref, watch, nextTick, type Ref } from 'vue'
import { useRoute, useRouter } from '#imports'
interface ParamDef<T extends string | number = string | number> {
default: T
type?: 'string' | 'number'
/** Debounce URL writes (ms). Default: 0 (immediate). */
debounce?: number
}
type ParamDefs = Record<string, ParamDef>
type InferRef<D extends ParamDef> = D['default'] extends number ? Ref<number> : Ref<string>
type StateRefs<T extends ParamDefs> = {
[K in keyof T]: InferRef<T[K]>
}
interface UseUrlStateOptions {
/** Called when state is restored from URL (back/forward navigation). */
onRestore?: () => void
}
export function useUrlState<T extends ParamDefs>(
params: T,
options?: UseUrlStateOptions,
): StateRefs<T> {
const route = useRoute()
const router = useRouter()
const keys = Object.keys(params) as (keyof T & string)[]
const refs: Record<string, Ref<string | number>> = {}
const timers: Record<string, ReturnType<typeof setTimeout> | null> = {}
for (const key of keys) {
refs[key] = ref(parseValue(route.query[key], params[key]!))
timers[key] = null
}
let isProgrammatic = false
const buildQuery = (): Record<string, string> => {
const q: Record<string, string> = {}
for (const key of keys) {
const val = refs[key]!.value
if (val !== params[key]!.default) {
q[key] = String(val)
}
}
return q
}
const pushToUrl = () => {
if (isProgrammatic) return
isProgrammatic = true
const query = buildQuery()
router
.replace({ path: route.path, query })
.catch(() => {})
.finally(() => {
nextTick(() => {
isProgrammatic = false
})
})
}
for (const key of keys) {
const ms = params[key]!.debounce ?? 0
watch(refs[key]!, () => {
if (isProgrammatic) return
if (ms > 0) {
if (timers[key]) clearTimeout(timers[key]!)
timers[key] = setTimeout(pushToUrl, ms)
} else {
pushToUrl()
}
})
}
watch(
() => ({ ...route.query }),
(newQuery) => {
if (isProgrammatic) return
isProgrammatic = true
let changed = false
for (const key of keys) {
const parsed = parseValue(newQuery[key], params[key]!)
if (refs[key]!.value !== parsed) {
refs[key]!.value = parsed
changed = true
}
}
nextTick(() => {
isProgrammatic = false
if (changed && options?.onRestore) {
options.onRestore()
}
})
},
)
return refs as StateRefs<T>
}
function parseValue(
raw: unknown,
def: ParamDef,
): string | number {
const str = typeof raw === 'string' ? raw : null
if (str === null) return def.default
if (def.type === 'number' || typeof def.default === 'number') {
const n = Number(str)
return Number.isFinite(n) ? n : def.default
}
return str
}

View File

@@ -176,11 +176,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted } from 'vue'
import { useComposants } from '~/composables/useComposants' import { useComposants } from '~/composables/useComposants'
import { useComponentTypes } from '~/composables/useComponentTypes' import { useComponentTypes } from '~/composables/useComponentTypes'
import { useToast } from '~/composables/useToast' import { useToast } from '~/composables/useToast'
import { usePersistedSort } from '~/composables/usePersistedSort' import { useUrlState } from '~/composables/useUrlState'
import DocumentThumbnail from '~/components/DocumentThumbnail.vue' import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
import Pagination from '~/components/common/Pagination.vue' import Pagination from '~/components/common/Pagination.vue'
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview' import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
@@ -190,15 +190,28 @@ const { composants, total, loadComposants, loading: loadingComposantsRef, delete
const { componentTypes, loadComponentTypes } = useComponentTypes() const { componentTypes, loadComponentTypes } = useComponentTypes()
const loadingComposants = computed(() => loadingComposantsRef.value) const loadingComposants = computed(() => loadingComposantsRef.value)
// Pagination state // State synced with URL query params (preserved on back/forward navigation)
const currentPage = ref(1) const {
const itemsPerPage = ref(30) page: currentPage,
perPage: itemsPerPage,
q: searchTerm,
sort: sortField,
dir: sortDirection,
} = useUrlState({
page: { default: 1, type: 'number' },
perPage: { default: 20, type: 'number' },
q: { default: '', debounce: 300 },
sort: { default: 'name' },
dir: { default: 'asc' },
}, {
onRestore: () => fetchComposants(),
})
const composantsTotal = computed(() => total.value) const composantsTotal = computed(() => total.value)
const composantsOnPage = computed(() => composants.value.length) const composantsOnPage = computed(() => composants.value.length)
const totalPages = computed(() => Math.ceil(composantsTotal.value / itemsPerPage.value) || 1) const totalPages = computed(() => Math.ceil(composantsTotal.value / itemsPerPage.value) || 1)
// Search state with debounce // Search debounce for API calls
const searchTerm = ref('')
let searchTimeout: ReturnType<typeof setTimeout> | null = null let searchTimeout: ReturnType<typeof setTimeout> | null = null
const debouncedSearch = () => { const debouncedSearch = () => {
@@ -211,12 +224,6 @@ const debouncedSearch = () => {
}, 300) }, 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) => {
@@ -234,7 +241,8 @@ const fetchComposants = async () => {
page: currentPage.value, page: currentPage.value,
itemsPerPage: itemsPerPage.value, itemsPerPage: itemsPerPage.value,
orderBy: sortField.value, orderBy: sortField.value,
orderDir: sortDirection.value orderDir: sortDirection.value as 'asc' | 'desc',
force: true
}) })
} }

View File

@@ -8,9 +8,9 @@
Ajustez le squelette et les métadonnées de cette catégorie de composant. Les modifications seront appliquées lors des prochaines créations de composants. Ajustez le squelette et les métadonnées de cette catégorie de composant. Les modifications seront appliquées lors des prochaines créations de composants.
</p> </p>
</div> </div>
<NuxtLink class="btn btn-ghost" to="/component-category"> <button type="button" class="btn btn-ghost" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
</header> </header>

View File

@@ -8,9 +8,9 @@
Configurez le squelette canonique qui sera appliqué lors de la création des composants appartenant à cette catégorie. Configurez le squelette canonique qui sera appliqué lors de la création des composants appartenant à cette catégorie.
</p> </p>
</div> </div>
<NuxtLink class="btn btn-ghost" to="/component-category"> <button type="button" class="btn btn-ghost" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
</header> </header>

View File

@@ -19,9 +19,9 @@
</p> </p>
</div> </div>
</div> </div>
<NuxtLink to="/component-catalog" class="btn btn-primary mt-6"> <button type="button" class="btn btn-primary mt-6" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
<section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto"> <section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto">
@@ -33,9 +33,9 @@
Mettez à jour les informations du composant et ses champs personnalisés. Mettez à jour les informations du composant et ses champs personnalisés.
</p> </p>
</div> </div>
<NuxtLink to="/component-catalog" class="btn btn-ghost btn-sm md:btn-md self-start"> <button type="button" class="btn btn-ghost btn-sm md:btn-md self-start" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</header> </header>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">

View File

@@ -7,9 +7,9 @@
Sélectionnez la catégorie cible puis complétez les informations du composant. Sélectionnez la catégorie cible puis complétez les informations du composant.
</p> </p>
</div> </div>
<NuxtLink to="/component-catalog" class="btn btn-ghost btn-sm md:btn-md self-start"> <button type="button" class="btn btn-ghost btn-sm md:btn-md self-start" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</header> </header>
<section class="card border border-base-200 bg-base-100 shadow-sm"> <section class="card border border-base-200 bg-base-100 shadow-sm">

View File

@@ -132,6 +132,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useDocuments } from '~/composables/useDocuments' import { useDocuments } from '~/composables/useDocuments'
import { useUrlState } from '~/composables/useUrlState'
import { getFileIcon } from '~/utils/fileIcons' import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument } from '~/utils/documentPreview' import { canPreviewDocument } from '~/utils/documentPreview'
import { formatFrenchDate } from '~/utils/date' import { formatFrenchDate } from '~/utils/date'
@@ -140,13 +141,15 @@ import IconLucideFileSearch from '~icons/lucide/file-search'
const { documents, loading, loadDocuments } = useDocuments() const { documents, loading, loadDocuments } = useDocuments()
const searchTerm = ref('') const { q: searchTerm, filter: attachmentFilter } = useUrlState({
const attachmentFilter = ref('all') q: { default: '', debounce: 300 },
filter: { default: 'all' },
})
const previewDocument = ref(null) const previewDocument = ref(null)
const previewVisible = ref(false) const previewVisible = ref(false)
onMounted(() => { onMounted(() => {
loadDocuments() loadDocuments({ itemsPerPage: 200 })
}) })
const filteredDocuments = computed(() => { const filteredDocuments = computed(() => {
@@ -156,10 +159,10 @@ const filteredDocuments = computed(() => {
return documents.value.filter((document) => { return documents.value.filter((document) => {
const matchesFilter = const matchesFilter =
filter === 'all' || filter === 'all' ||
(filter === 'site' && document.siteId) || (filter === 'site' && document.site) ||
(filter === 'machine' && document.machineId) || (filter === 'machine' && document.machine) ||
(filter === 'composant' && document.composantId) || (filter === 'composant' && document.composant) ||
(filter === 'piece' && document.pieceId) (filter === 'piece' && document.piece)
if (!matchesFilter) { return false } if (!matchesFilter) { return false }

View File

@@ -8,9 +8,9 @@
Mettez à jour la structure et les champs personnalisés de cette catégorie de pièce pour préparer les futures créations. Mettez à jour la structure et les champs personnalisés de cette catégorie de pièce pour préparer les futures créations.
</p> </p>
</div> </div>
<NuxtLink class="btn btn-ghost" to="/piece-category"> <button type="button" class="btn btn-ghost" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
</header> </header>

View File

@@ -8,9 +8,9 @@
Définissez les champs personnalisés et le squelette appliqué lors de la création des pièces de cette catégorie. Définissez les champs personnalisés et le squelette appliqué lors de la création des pièces de cette catégorie.
</p> </p>
</div> </div>
<NuxtLink class="btn btn-ghost" to="/piece-category"> <button type="button" class="btn btn-ghost" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
</header> </header>

View File

@@ -198,11 +198,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted } 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 { useUrlState } from '~/composables/useUrlState'
import DocumentThumbnail from '~/components/DocumentThumbnail.vue' import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
import Pagination from '~/components/common/Pagination.vue' import Pagination from '~/components/common/Pagination.vue'
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview' import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
@@ -212,15 +212,28 @@ const { pieces, total, loadPieces, loading: loadingPiecesRef, deletePiece } = us
const { pieceTypes, loadPieceTypes } = usePieceTypes() const { pieceTypes, loadPieceTypes } = usePieceTypes()
const loadingPieces = computed(() => loadingPiecesRef.value) const loadingPieces = computed(() => loadingPiecesRef.value)
// Pagination state // State synced with URL query params (preserved on back/forward navigation)
const currentPage = ref(1) const {
const itemsPerPage = ref(30) page: currentPage,
perPage: itemsPerPage,
q: searchTerm,
sort: sortField,
dir: sortDirection,
} = useUrlState({
page: { default: 1, type: 'number' },
perPage: { default: 20, type: 'number' },
q: { default: '', debounce: 300 },
sort: { default: 'name' },
dir: { default: 'asc' },
}, {
onRestore: () => fetchPieces(),
})
const piecesTotal = computed(() => total.value) const piecesTotal = computed(() => total.value)
const piecesOnPage = computed(() => pieces.value.length) const piecesOnPage = computed(() => pieces.value.length)
const totalPages = computed(() => Math.ceil(piecesTotal.value / itemsPerPage.value) || 1) const totalPages = computed(() => Math.ceil(piecesTotal.value / itemsPerPage.value) || 1)
// Search state with debounce // Search debounce for API calls
const searchTerm = ref('')
let searchTimeout: ReturnType<typeof setTimeout> | null = null let searchTimeout: ReturnType<typeof setTimeout> | null = null
const debouncedSearch = () => { const debouncedSearch = () => {
@@ -233,12 +246,6 @@ const debouncedSearch = () => {
}, 300) }, 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) => {
@@ -256,7 +263,8 @@ const fetchPieces = async () => {
page: currentPage.value, page: currentPage.value,
itemsPerPage: itemsPerPage.value, itemsPerPage: itemsPerPage.value,
orderBy: sortField.value, orderBy: sortField.value,
orderDir: sortDirection.value orderDir: sortDirection.value as 'asc' | 'desc',
force: true
}) })
} }

View File

@@ -19,9 +19,9 @@
</p> </p>
</div> </div>
</div> </div>
<NuxtLink to="/pieces-catalog" class="btn btn-primary mt-6"> <button type="button" class="btn btn-primary mt-6" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
<section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto"> <section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto">
@@ -33,9 +33,9 @@
Ajustez les informations de la pièce et ses champs personnalisés. Ajustez les informations de la pièce et ses champs personnalisés.
</p> </p>
</div> </div>
<NuxtLink to="/pieces-catalog" class="btn btn-ghost btn-sm md:btn-md self-start"> <button type="button" class="btn btn-ghost btn-sm md:btn-md self-start" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</header> </header>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">

View File

@@ -7,9 +7,9 @@
Choisissez la catégorie adaptée puis renseignez toutes les informations de votre pièce. Choisissez la catégorie adaptée puis renseignez toutes les informations de votre pièce.
</p> </p>
</div> </div>
<NuxtLink to="/pieces-catalog" class="btn btn-ghost btn-sm md:btn-md self-start"> <button type="button" class="btn btn-ghost btn-sm md:btn-md self-start" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</header> </header>
<section class="card border border-base-200 bg-base-100 shadow-sm"> <section class="card border border-base-200 bg-base-100 shadow-sm">

View File

@@ -170,12 +170,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted } from 'vue'
import { useHead } from '#imports' import { useHead } from '#imports'
import { useProducts } from '~/composables/useProducts' import { useProducts } from '~/composables/useProducts'
import { useProductTypes } from '~/composables/useProductTypes' import { useProductTypes } from '~/composables/useProductTypes'
import { useToast } from '~/composables/useToast' import { useToast } from '~/composables/useToast'
import { usePersistedSort } from '~/composables/usePersistedSort' import { useUrlState } from '~/composables/useUrlState'
import DocumentThumbnail from '~/components/DocumentThumbnail.vue' import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview' import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
@@ -195,11 +195,11 @@ const {
const { productTypes, loadProductTypes } = useProductTypes() const { productTypes, loadProductTypes } = useProductTypes()
const toast = useToast() const toast = useToast()
const searchTerm = ref('') const { q: searchTerm, sort: sortField, dir: sortDirection } = useUrlState({
const { sortField, sortDirection } = usePersistedSort<'name' | 'createdAt', 'asc' | 'desc'>( q: { default: '', debounce: 300 },
'product-catalog', sort: { default: 'name' },
{ field: 'name', direction: 'asc' }, dir: { default: 'asc' },
) })
// Enrichir les produits avec les types de produits complets // Enrichir les produits avec les types de produits complets
const normalizedProducts = computed(() => { const normalizedProducts = computed(() => {
@@ -388,7 +388,7 @@ const resolvePreviewAlt = (product: Record<string, any>) => {
} }
const reload = async () => { const reload = async () => {
await loadProducts({ force: true }) await loadProducts({ itemsPerPage: 200, force: true })
} }
const { confirm } = useConfirm() const { confirm } = useConfirm()
@@ -409,7 +409,7 @@ const confirmDelete = async (product: Record<string, any>) => {
onMounted(async () => { onMounted(async () => {
await Promise.all([ await Promise.all([
loadProducts(), loadProducts({ itemsPerPage: 200, force: true }),
loadProductTypes() loadProductTypes()
]) ])
}) })

View File

@@ -8,9 +8,9 @@
Mettez à jour la structure et les champs personnalisés de cette catégorie de produit pour préparer les futures créations. Mettez à jour la structure et les champs personnalisés de cette catégorie de produit pour préparer les futures créations.
</p> </p>
</div> </div>
<NuxtLink class="btn btn-ghost" to="/product-category"> <button type="button" class="btn btn-ghost" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
</header> </header>

View File

@@ -8,9 +8,9 @@
Définissez les champs personnalisés et le squelette appliqué lors de la création des produits de cette catégorie. Définissez les champs personnalisés et le squelette appliqué lors de la création des produits de cette catégorie.
</p> </p>
</div> </div>
<NuxtLink class="btn btn-ghost" to="/product-category"> <button type="button" class="btn btn-ghost" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
</header> </header>

View File

@@ -19,9 +19,9 @@
</p> </p>
</div> </div>
</div> </div>
<NuxtLink to="/product-catalog" class="btn btn-primary mt-6"> <button type="button" class="btn btn-primary mt-6" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</div> </div>
<section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-4xl mx-auto"> <section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-4xl mx-auto">
@@ -33,9 +33,9 @@
Mettez à jour les informations du produit et ses champs personnalisés. Mettez à jour les informations du produit et ses champs personnalisés.
</p> </p>
</div> </div>
<NuxtLink to="/product-catalog" class="btn btn-ghost btn-sm md:btn-md self-start"> <button type="button" class="btn btn-ghost btn-sm md:btn-md self-start" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</header> </header>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">

View File

@@ -7,9 +7,9 @@
Sélectionnez la catégorie cible puis renseignez toutes les informations de votre produit catalogue. Sélectionnez la catégorie cible puis renseignez toutes les informations de votre produit catalogue.
</p> </p>
</div> </div>
<NuxtLink to="/product-catalog" class="btn btn-ghost btn-sm md:btn-md self-start"> <button type="button" class="btn btn-ghost btn-sm md:btn-md self-start" @click="$router.back()">
Retour au catalogue Retour au catalogue
</NuxtLink> </button>
</header> </header>
<section class="card border border-base-200 bg-base-100 shadow-sm"> <section class="card border border-base-200 bg-base-100 shadow-sm">

View File

@@ -132,28 +132,19 @@ export async function listModelTypes(params: ModelTypeListParams = {}, opts: { s
if (params.category) { if (params.category) {
query.category = params.category; query.category = params.category;
} }
if (params.sort) {
query.sort = params.sort;
}
if (params.dir) {
query.dir = params.dir;
}
const hasCategoryFilter = Boolean(params.category);
const effectiveLimit = typeof params.limit === 'number' ? params.limit : undefined;
const effectiveOffset = typeof params.offset === 'number' ? params.offset : 0;
if (hasCategoryFilter) { // Sort: API Platform OrderFilter uses order[field]=direction
// Fetch enough items to allow client-side category filtering + pagination. const sortField = params.sort || 'name';
query.itemsPerPage = Math.max(effectiveLimit ?? 200, 200); const sortDir = params.dir || 'asc';
query.offset = 0; query[`order[${sortField}]`] = sortDir;
} else {
if (typeof params.limit === 'number') { // Pagination: API Platform uses page + itemsPerPage
query.itemsPerPage = params.limit; const effectiveLimit = typeof params.limit === 'number' ? params.limit : 20;
} const effectiveOffset = typeof params.offset === 'number' ? params.offset : 0;
if (typeof params.offset === 'number') { const page = Math.floor(effectiveOffset / effectiveLimit) + 1;
query.offset = params.offset;
} query.itemsPerPage = effectiveLimit;
} query.page = page;
const payload = await requestFetch<Record<string, any>>(ENDPOINT, createOptions({ const payload = await requestFetch<Record<string, any>>(ENDPOINT, createOptions({
method: 'GET', method: 'GET',
@@ -168,25 +159,20 @@ export async function listModelTypes(params: ModelTypeListParams = {}, opts: { s
: Array.isArray(payload?.items) : Array.isArray(payload?.items)
? payload.items ? payload.items
: []; : [];
const filteredItems = params.category
? rawItems.filter((item: any) => item?.category === params.category) const total = typeof payload?.totalItems === 'number'
: rawItems; ? payload.totalItems
const total = params.category : typeof payload?.['hydra:totalItems'] === 'number'
? filteredItems.length ? payload['hydra:totalItems']
: typeof payload?.totalItems === 'number' : rawItems.length;
? payload.totalItems
: Array.isArray(payload?.items) const items = rawItems.map(normalizeModelType);
? payload.items.length
: rawItems.length;
const items = (params.category && typeof effectiveLimit === 'number'
? filteredItems.slice(effectiveOffset, effectiveOffset + effectiveLimit)
: filteredItems).map(normalizeModelType);
return { return {
items, items,
total, total,
offset: effectiveOffset, offset: effectiveOffset,
limit: typeof effectiveLimit === 'number' ? effectiveLimit : items.length, limit: effectiveLimit,
} satisfies ModelTypeListResponse; } satisfies ModelTypeListResponse;
} }