frontend: remove legacy model-types page
This commit is contained in:
@@ -1,286 +0,0 @@
|
|||||||
<template>
|
|
||||||
<main class="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
|
|
||||||
<header class="space-y-2">
|
|
||||||
<p class="text-sm uppercase tracking-wide text-primary">Administration</p>
|
|
||||||
<h1 class="text-3xl font-bold text-base-content">{{ heading }}</h1>
|
|
||||||
<p class="text-base text-base-content/70">
|
|
||||||
Gérez les catégories utilisées pour structurer les catalogues de composants et de pièces. Ajoutez, modifiez ou supprimez des entrées avec tri, recherche et pagination.
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<ModelTypesToolbar
|
|
||||||
:category="category"
|
|
||||||
:search="searchInput"
|
|
||||||
:sort="sort"
|
|
||||||
:dir="dir"
|
|
||||||
:loading="loading || saving"
|
|
||||||
@update:category="onCategoryChange"
|
|
||||||
@update:search="onSearchInput"
|
|
||||||
@update:sort="onSortChange"
|
|
||||||
@update:dir="onDirChange"
|
|
||||||
@create="openCreateModal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ModelTypesTable
|
|
||||||
:items="items"
|
|
||||||
:loading="loading"
|
|
||||||
:total="total"
|
|
||||||
:limit="limit"
|
|
||||||
:offset="offset"
|
|
||||||
@edit="openEditModal"
|
|
||||||
@delete="confirmDelete"
|
|
||||||
@update:offset="onOffsetChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditModal
|
|
||||||
:visible="isModalOpen"
|
|
||||||
:mode="modalMode"
|
|
||||||
:initial-category="category"
|
|
||||||
:initial-data="modalInitialData"
|
|
||||||
:saving="saving"
|
|
||||||
@close="closeModal"
|
|
||||||
@save="handleSave"
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
||||||
import { useHead } from '#imports';
|
|
||||||
import ModelTypesToolbar from '~/components/model-types/Toolbar.vue';
|
|
||||||
import ModelTypesTable from '~/components/model-types/Table.vue';
|
|
||||||
import EditModal from '~/components/model-types/EditModal.vue';
|
|
||||||
import {
|
|
||||||
createModelType,
|
|
||||||
deleteModelType,
|
|
||||||
listModelTypes,
|
|
||||||
updateModelType,
|
|
||||||
type ModelCategory,
|
|
||||||
type ModelType,
|
|
||||||
type ModelTypeListResponse,
|
|
||||||
type ModelTypePayload,
|
|
||||||
} from '~/services/modelTypes';
|
|
||||||
import { useToast } from '~/composables/useToast';
|
|
||||||
|
|
||||||
const category = ref<ModelCategory>('COMPONENT');
|
|
||||||
const searchInput = ref('');
|
|
||||||
const searchTerm = ref('');
|
|
||||||
const sort = ref<'name' | 'code' | 'createdAt'>('createdAt');
|
|
||||||
const dir = ref<'asc' | 'desc'>('desc');
|
|
||||||
const limit = ref(20);
|
|
||||||
const offset = ref(0);
|
|
||||||
|
|
||||||
const items = ref<ModelType[]>([]);
|
|
||||||
const total = ref(0);
|
|
||||||
const loading = ref(false);
|
|
||||||
const saving = ref(false);
|
|
||||||
|
|
||||||
const isModalOpen = ref(false);
|
|
||||||
const modalMode = ref<'create' | 'edit'>('create');
|
|
||||||
const modalInitialData = ref<Partial<ModelTypePayload> | null>(null);
|
|
||||||
const editingId = ref<string | null>(null);
|
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let activeController: AbortController | null = null;
|
|
||||||
|
|
||||||
const { showError, showSuccess } = useToast();
|
|
||||||
|
|
||||||
const heading = computed(() => (category.value === 'COMPONENT' ? 'Catégorie de composant' : 'Catégorie de pièce'));
|
|
||||||
|
|
||||||
useHead(() => ({
|
|
||||||
title: heading.value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const extractErrorMessage = (error: unknown) => {
|
|
||||||
if (error && typeof error === 'object') {
|
|
||||||
const maybeFetchError = error as { data?: any; statusMessage?: string; message?: string };
|
|
||||||
if (maybeFetchError.data) {
|
|
||||||
const data = maybeFetchError.data;
|
|
||||||
if (typeof data.message === 'string') {
|
|
||||||
return data.message;
|
|
||||||
}
|
|
||||||
if (Array.isArray(data.message) && data.message.length > 0) {
|
|
||||||
return data.message[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof maybeFetchError.statusMessage === 'string') {
|
|
||||||
return maybeFetchError.statusMessage;
|
|
||||||
}
|
|
||||||
if (typeof maybeFetchError.message === 'string') {
|
|
||||||
return maybeFetchError.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'Une erreur est survenue lors de la communication avec le serveur.';
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = async ({ resetOffset = false }: { resetOffset?: boolean } = {}) => {
|
|
||||||
if (resetOffset) {
|
|
||||||
offset.value = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeController) {
|
|
||||||
activeController.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
activeController = controller;
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response: ModelTypeListResponse = await listModelTypes(
|
|
||||||
{
|
|
||||||
q: searchTerm.value || undefined,
|
|
||||||
category: category.value,
|
|
||||||
sort: sort.value,
|
|
||||||
dir: dir.value,
|
|
||||||
limit: limit.value,
|
|
||||||
offset: offset.value,
|
|
||||||
},
|
|
||||||
{ signal: controller.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
items.value = response.items;
|
|
||||||
total.value = response.total;
|
|
||||||
offset.value = response.offset;
|
|
||||||
limit.value = response.limit;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error?.name === 'AbortError') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showError(extractErrorMessage(error));
|
|
||||||
} finally {
|
|
||||||
if (activeController === controller) {
|
|
||||||
loading.value = false;
|
|
||||||
activeController = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSearchInput = (value: string) => {
|
|
||||||
searchInput.value = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCategoryChange = (value: ModelCategory) => {
|
|
||||||
if (category.value !== value) {
|
|
||||||
category.value = value;
|
|
||||||
refresh({ resetOffset: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSortChange = (value: 'name' | 'code' | 'createdAt') => {
|
|
||||||
if (sort.value !== value) {
|
|
||||||
sort.value = value;
|
|
||||||
refresh({ resetOffset: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDirChange = (value: 'asc' | 'desc') => {
|
|
||||||
if (dir.value !== value) {
|
|
||||||
dir.value = value;
|
|
||||||
refresh({ resetOffset: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOffsetChange = (value: number) => {
|
|
||||||
const nextOffset = Math.max(0, value);
|
|
||||||
if (nextOffset !== offset.value) {
|
|
||||||
offset.value = nextOffset;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openCreateModal = () => {
|
|
||||||
modalMode.value = 'create';
|
|
||||||
modalInitialData.value = null;
|
|
||||||
editingId.value = null;
|
|
||||||
isModalOpen.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openEditModal = (item: ModelType) => {
|
|
||||||
modalMode.value = 'edit';
|
|
||||||
editingId.value = item.id;
|
|
||||||
modalInitialData.value = {
|
|
||||||
name: item.name,
|
|
||||||
code: item.code,
|
|
||||||
category: item.category,
|
|
||||||
notes: item.notes ?? item.description ?? '',
|
|
||||||
};
|
|
||||||
isModalOpen.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
isModalOpen.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async (payload: ModelTypePayload) => {
|
|
||||||
saving.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const enrichedPayload = {
|
|
||||||
...payload,
|
|
||||||
description: payload.notes ?? null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (modalMode.value === 'create') {
|
|
||||||
await createModelType(enrichedPayload);
|
|
||||||
showSuccess('Type de modèle créé avec succès.');
|
|
||||||
} else if (modalMode.value === 'edit' && editingId.value) {
|
|
||||||
await updateModelType(editingId.value, enrichedPayload);
|
|
||||||
showSuccess('Type de modèle mis à jour avec succès.');
|
|
||||||
}
|
|
||||||
|
|
||||||
isModalOpen.value = false;
|
|
||||||
await refresh();
|
|
||||||
} catch (error) {
|
|
||||||
showError(extractErrorMessage(error));
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = async (item: ModelType) => {
|
|
||||||
const confirmed = window.confirm('Supprimer ce type ? Cette action est irréversible.');
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteModelType(item.id);
|
|
||||||
showSuccess(`Type « ${item.name} » supprimé avec succès.`);
|
|
||||||
|
|
||||||
if (items.value.length === 1 && offset.value >= limit.value) {
|
|
||||||
offset.value = Math.max(0, offset.value - limit.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
await refresh();
|
|
||||||
} catch (error) {
|
|
||||||
showError(extractErrorMessage(error));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => searchInput.value,
|
|
||||||
(value) => {
|
|
||||||
if (debounceTimer) {
|
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
}
|
|
||||||
debounceTimer = setTimeout(() => {
|
|
||||||
searchTerm.value = value.trim();
|
|
||||||
refresh({ resetOffset: true });
|
|
||||||
}, 300);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (debounceTimer) {
|
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
}
|
|
||||||
if (activeController) {
|
|
||||||
activeController.abort();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
Reference in New Issue
Block a user