feat(model-types): add related-items modal and guard category edits
This commit is contained in:
@@ -29,10 +29,65 @@
|
|||||||
:total="total"
|
:total="total"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:offset="offset"
|
:offset="offset"
|
||||||
|
@related="openRelatedModal"
|
||||||
@edit="openEditPage"
|
@edit="openEditPage"
|
||||||
@delete="confirmDelete"
|
@delete="confirmDelete"
|
||||||
@update:offset="onOffsetChange"
|
@update:offset="onOffsetChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<dialog class="modal" :class="{ 'modal-open': relatedModalOpen }">
|
||||||
|
<div class="modal-box max-w-3xl">
|
||||||
|
<h3 class="text-lg font-bold text-base-content">
|
||||||
|
{{ relatedModalTitle }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-base-content/70">
|
||||||
|
{{ relatedModalSubtitle }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 rounded-xl border border-base-200 bg-base-100">
|
||||||
|
<div v-if="relatedLoading" class="flex items-center gap-2 px-4 py-6 text-sm text-info">
|
||||||
|
<span class="loading loading-spinner loading-sm" aria-hidden="true"></span>
|
||||||
|
Chargement des éléments liés…
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="relatedError" class="px-4 py-6 text-sm text-error">
|
||||||
|
{{ relatedError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="relatedItems.length === 0"
|
||||||
|
class="px-4 py-6 text-sm text-base-content/60"
|
||||||
|
>
|
||||||
|
Aucun élément lié à cette catégorie.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul v-else class="max-h-96 divide-y divide-base-200 overflow-y-auto">
|
||||||
|
<li
|
||||||
|
v-for="entry in relatedItems"
|
||||||
|
:key="entry.id"
|
||||||
|
class="px-2 py-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full flex-col gap-1 rounded-lg px-2 py-2 text-left hover:bg-base-200 focus:bg-base-200 focus:outline-none"
|
||||||
|
@click="openRelatedEdit(entry)"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-base-content">{{ entry.name }}</span>
|
||||||
|
<span v-if="entry.reference" class="text-xs text-base-content/60">
|
||||||
|
Référence: {{ entry.reference }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button type="button" class="btn" @click="closeRelatedModal">
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -41,6 +96,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } 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 {
|
import {
|
||||||
deleteModelType,
|
deleteModelType,
|
||||||
listModelTypes,
|
listModelTypes,
|
||||||
@@ -82,6 +138,7 @@ let activeController: AbortController | null = null;
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { showError, showSuccess } = useToast();
|
const { showError, showSuccess } = useToast();
|
||||||
|
const { get } = useApi();
|
||||||
|
|
||||||
const headingText = computed(() => props.heading);
|
const headingText = computed(() => props.heading);
|
||||||
const descriptionText = computed(
|
const descriptionText = computed(
|
||||||
@@ -257,6 +314,165 @@ const confirmDelete = async (item: ModelType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RelatedEntry = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
reference?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedModalOpen = ref(false);
|
||||||
|
const relatedLoading = ref(false);
|
||||||
|
const relatedError = ref<string | null>(null);
|
||||||
|
const relatedItems = ref<RelatedEntry[]>([]);
|
||||||
|
const relatedType = ref<ModelType | null>(null);
|
||||||
|
|
||||||
|
const relatedCategoryLabels: Record<
|
||||||
|
ModelCategory,
|
||||||
|
{ plural: string; singular: string }
|
||||||
|
> = {
|
||||||
|
COMPONENT: { plural: "composants", singular: "composant" },
|
||||||
|
PIECE: { plural: "pièces", singular: "pièce" },
|
||||||
|
PRODUCT: { plural: "produits", singular: "produit" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedModalTitle = computed(() => {
|
||||||
|
const current = relatedType.value;
|
||||||
|
if (!current) {
|
||||||
|
return "Éléments liés";
|
||||||
|
}
|
||||||
|
return `Éléments liés à « ${current.name} »`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const relatedModalSubtitle = computed(() => {
|
||||||
|
const current = relatedType.value;
|
||||||
|
if (!current) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const labels =
|
||||||
|
relatedCategoryLabels[current.category] ?? relatedCategoryLabels.COMPONENT;
|
||||||
|
const count = relatedItems.value.length;
|
||||||
|
if (relatedLoading.value) {
|
||||||
|
return `Chargement des ${labels.plural}…`;
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
return `Aucun ${labels.singular} lié.`;
|
||||||
|
}
|
||||||
|
if (count === 1) {
|
||||||
|
return `1 ${labels.singular} lié.`;
|
||||||
|
}
|
||||||
|
return `${count} ${labels.plural} liés.`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const extractCollection = (payload: any): any[] => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member;
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.["hydra:member"])) {
|
||||||
|
return payload["hydra:member"];
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.items)) {
|
||||||
|
return payload.items;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildModelTypeIri = (id: string) => `/api/model_types/${id}`;
|
||||||
|
|
||||||
|
const resolveRelatedConfig = (category: ModelCategory) => {
|
||||||
|
if (category === "COMPONENT") {
|
||||||
|
return { endpoint: "/composants", filterKey: "typeComposant" };
|
||||||
|
}
|
||||||
|
if (category === "PIECE") {
|
||||||
|
return { endpoint: "/pieces", filterKey: "typePiece" };
|
||||||
|
}
|
||||||
|
return { endpoint: "/products", filterKey: "typeProduct" };
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveRelatedEditBasePath = (category: ModelCategory) => {
|
||||||
|
if (category === "COMPONENT") {
|
||||||
|
return "/component";
|
||||||
|
}
|
||||||
|
if (category === "PIECE") {
|
||||||
|
return "/pieces";
|
||||||
|
}
|
||||||
|
return "/product";
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapRelatedEntry = (item: any): RelatedEntry | null => {
|
||||||
|
if (!item || typeof item !== "object" || typeof item.id !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const name =
|
||||||
|
typeof item.name === "string" && item.name.trim()
|
||||||
|
? item.name
|
||||||
|
: "Sans nom";
|
||||||
|
const reference =
|
||||||
|
typeof item.reference === "string" && item.reference.trim()
|
||||||
|
? item.reference
|
||||||
|
: typeof item.code === "string" && item.code.trim()
|
||||||
|
? item.code
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name,
|
||||||
|
reference,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRelatedItems = async (item: ModelType) => {
|
||||||
|
const { endpoint, filterKey } = resolveRelatedConfig(item.category);
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("itemsPerPage", "200");
|
||||||
|
params.set(filterKey, buildModelTypeIri(item.id));
|
||||||
|
params.set("order[name]", "asc");
|
||||||
|
|
||||||
|
relatedLoading.value = true;
|
||||||
|
relatedError.value = null;
|
||||||
|
relatedItems.value = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await get(`${endpoint}?${params.toString()}`);
|
||||||
|
if (!result.success) {
|
||||||
|
relatedError.value =
|
||||||
|
result.error ?? "Impossible de charger les éléments liés.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collection = extractCollection(result.data);
|
||||||
|
relatedItems.value = collection
|
||||||
|
.map(mapRelatedEntry)
|
||||||
|
.filter((entry): entry is RelatedEntry => Boolean(entry));
|
||||||
|
} catch (error) {
|
||||||
|
relatedError.value = extractErrorMessage(error);
|
||||||
|
} finally {
|
||||||
|
relatedLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openRelatedModal = (item: ModelType) => {
|
||||||
|
relatedType.value = item;
|
||||||
|
relatedModalOpen.value = true;
|
||||||
|
void loadRelatedItems(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openRelatedEdit = (entry: RelatedEntry) => {
|
||||||
|
const current = relatedType.value;
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const basePath = resolveRelatedEditBasePath(current.category);
|
||||||
|
relatedModalOpen.value = false;
|
||||||
|
router.push(`${basePath}/${entry.id}/edit`).catch(() => {
|
||||||
|
showError("Navigation impossible vers la fiche d'édition.");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeRelatedModal = () => {
|
||||||
|
relatedModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => searchInput.value,
|
() => searchInput.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
|
|||||||
@@ -108,6 +108,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="disableSubmit"
|
||||||
|
class="alert alert-warning"
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<span>{{ disableSubmitMessage }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer class="flex flex-col gap-3 border-t border-base-300 pt-4 sm:flex-row sm:justify-end">
|
<footer class="flex flex-col gap-3 border-t border-base-300 pt-4 sm:flex-row sm:justify-end">
|
||||||
<button type="button" class="btn btn-ghost" :disabled="saving" @click="emit('cancel')">
|
<button type="button" class="btn btn-ghost" :disabled="saving" @click="emit('cancel')">
|
||||||
Annuler
|
Annuler
|
||||||
@@ -150,6 +159,8 @@ const props = withDefaults(defineProps<{
|
|||||||
structureLoading?: boolean
|
structureLoading?: boolean
|
||||||
allowComponentSubcomponents?: boolean
|
allowComponentSubcomponents?: boolean
|
||||||
componentSubcomponentMaxDepth?: number
|
componentSubcomponentMaxDepth?: number
|
||||||
|
disableSubmit?: boolean
|
||||||
|
disableSubmitMessage?: string
|
||||||
}>(), {
|
}>(), {
|
||||||
initialData: null,
|
initialData: null,
|
||||||
saving: false,
|
saving: false,
|
||||||
@@ -157,6 +168,8 @@ const props = withDefaults(defineProps<{
|
|||||||
structureLoading: false,
|
structureLoading: false,
|
||||||
allowComponentSubcomponents: true,
|
allowComponentSubcomponents: true,
|
||||||
componentSubcomponentMaxDepth: 1,
|
componentSubcomponentMaxDepth: 1,
|
||||||
|
disableSubmit: false,
|
||||||
|
disableSubmitMessage: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -173,6 +186,12 @@ const componentSubcomponentMaxDepth = computed(() =>
|
|||||||
? props.componentSubcomponentMaxDepth
|
? props.componentSubcomponentMaxDepth
|
||||||
: 1,
|
: 1,
|
||||||
)
|
)
|
||||||
|
const disableSubmit = computed(() => props.disableSubmit === true)
|
||||||
|
const disableSubmitMessage = computed(() =>
|
||||||
|
(props.disableSubmitMessage && props.disableSubmitMessage.trim())
|
||||||
|
? props.disableSubmitMessage
|
||||||
|
: 'Cette catégorie ne peut pas être modifiée car des éléments y sont déjà liés.',
|
||||||
|
)
|
||||||
|
|
||||||
const form = reactive<ModelTypePayload>({
|
const form = reactive<ModelTypePayload>({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -248,7 +267,7 @@ const resetForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submitLabel = computed(() => (props.mode === 'edit' ? 'Enregistrer' : 'Créer'))
|
const submitLabel = computed(() => (props.mode === 'edit' ? 'Enregistrer' : 'Créer'))
|
||||||
const isSubmitDisabled = computed(() => saving.value || structureLoading.value)
|
const isSubmitDisabled = computed(() => saving.value || structureLoading.value || disableSubmit.value)
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
errors.name = undefined
|
errors.name = undefined
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<tr class="text-base-content/70">
|
<tr class="text-base-content/70">
|
||||||
<th scope="col">Nom</th>
|
<th scope="col">Nom</th>
|
||||||
<th scope="col">Notes</th>
|
<th scope="col">Notes</th>
|
||||||
<th scope="col" class="w-32 text-right">Actions</th>
|
<th scope="col" class="w-48 text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -45,6 +45,9 @@
|
|||||||
<span v-else class="text-base-content/50">—</span>
|
<span v-else class="text-base-content/50">—</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right space-x-2">
|
<td class="text-right space-x-2">
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" @click="emit('related', item)">
|
||||||
|
Liés
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-ghost btn-sm" @click="emit('edit', item)">
|
<button type="button" class="btn btn-ghost btn-sm" @click="emit('edit', item)">
|
||||||
Éditer
|
Éditer
|
||||||
</button>
|
</button>
|
||||||
@@ -72,6 +75,9 @@
|
|||||||
<p class="mt-3 text-sm text-base-content/80" v-if="item.notes">{{ item.notes }}</p>
|
<p class="mt-3 text-sm text-base-content/80" v-if="item.notes">{{ item.notes }}</p>
|
||||||
<p class="mt-3 text-sm text-base-content/50" v-else>Pas de notes</p>
|
<p class="mt-3 text-sm text-base-content/50" v-else>Pas de notes</p>
|
||||||
<footer class="mt-4 flex flex-wrap items-center gap-2 justify-end">
|
<footer class="mt-4 flex flex-wrap items-center gap-2 justify-end">
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" @click="emit('related', item)">
|
||||||
|
Liés
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-ghost btn-sm" @click="emit('edit', item)">
|
<button type="button" class="btn btn-ghost btn-sm" @click="emit('edit', item)">
|
||||||
Éditer
|
Éditer
|
||||||
</button>
|
</button>
|
||||||
@@ -123,6 +129,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
(e: 'related', item: ModelType): void;
|
||||||
(e: 'edit', item: ModelType): void;
|
(e: 'edit', item: ModelType): void;
|
||||||
(e: 'delete', item: ModelType): void;
|
(e: 'delete', item: ModelType): void;
|
||||||
(e: 'update:offset', offset: number): void;
|
(e: 'update:offset', offset: number): void;
|
||||||
|
|||||||
101
app/composables/useCategoryEditGuard.ts
Normal file
101
app/composables/useCategoryEditGuard.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import { useToast } from '~/composables/useToast'
|
||||||
|
|
||||||
|
type GuardLabels = {
|
||||||
|
singular: string
|
||||||
|
plural: string
|
||||||
|
verifying: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GuardConfig = {
|
||||||
|
endpoint: string
|
||||||
|
filterKey: string
|
||||||
|
labels: GuardLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractTotal = (payload: any, fallbackLength: number) => {
|
||||||
|
if (typeof payload?.totalItems === 'number') {
|
||||||
|
return payload.totalItems
|
||||||
|
}
|
||||||
|
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||||
|
return payload['hydra:totalItems']
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member.length
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member'].length
|
||||||
|
}
|
||||||
|
return fallbackLength
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCategoryEditGuard (config: GuardConfig) {
|
||||||
|
const { get } = useApi()
|
||||||
|
const { showError } = useToast()
|
||||||
|
|
||||||
|
const linkedCount = ref(0)
|
||||||
|
const linkedLoading = ref(false)
|
||||||
|
|
||||||
|
const loadLinkedCount = async (modelTypeId: string) => {
|
||||||
|
linkedLoading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', '1')
|
||||||
|
params.set(config.filterKey, `/api/model_types/${modelTypeId}`)
|
||||||
|
|
||||||
|
const result = await get(`${config.endpoint}?${params.toString()}`)
|
||||||
|
if (!result.success) {
|
||||||
|
linkedCount.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackLength = Array.isArray(result.data?.member)
|
||||||
|
? result.data.member.length
|
||||||
|
: Array.isArray(result.data?.['hydra:member'])
|
||||||
|
? result.data['hydra:member'].length
|
||||||
|
: 0
|
||||||
|
|
||||||
|
linkedCount.value = extractTotal(result.data, fallbackLength)
|
||||||
|
} catch (error) {
|
||||||
|
linkedCount.value = 0
|
||||||
|
} finally {
|
||||||
|
linkedLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSubmitBlocked = computed(
|
||||||
|
() => linkedLoading.value || linkedCount.value > 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const submitBlockMessage = computed(() => {
|
||||||
|
if (linkedLoading.value) {
|
||||||
|
return config.labels.verifying
|
||||||
|
}
|
||||||
|
if (linkedCount.value <= 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (linkedCount.value === 1) {
|
||||||
|
return `Modification bloquée : 1 ${config.labels.singular} est déjà lié à cette catégorie.`
|
||||||
|
}
|
||||||
|
return `Modification bloquée : ${linkedCount.value} ${config.labels.plural} sont déjà liés à cette catégorie.`
|
||||||
|
})
|
||||||
|
|
||||||
|
const guardSubmitOrNotify = () => {
|
||||||
|
if (!isSubmitBlocked.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
showError(submitBlockMessage.value || 'Modification bloquée pour cette catégorie.')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
linkedCount,
|
||||||
|
linkedLoading,
|
||||||
|
isSubmitBlocked,
|
||||||
|
submitBlockMessage,
|
||||||
|
loadLinkedCount,
|
||||||
|
guardSubmitOrNotify,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
:initial-data="initialData"
|
:initial-data="initialData"
|
||||||
:lock-category="true"
|
:lock-category="true"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
|
:disable-submit="isSubmitBlocked"
|
||||||
|
:disable-submit-message="submitBlockMessage"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
/>
|
/>
|
||||||
@@ -38,6 +40,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useHead, useRoute, useRouter } from '#imports'
|
import { useHead, useRoute, useRouter } from '#imports'
|
||||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||||
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
||||||
|
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -48,6 +51,21 @@ const loading = ref(true)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const initialData = ref<Partial<ModelTypePayload> | null>(null)
|
const initialData = ref<Partial<ModelTypePayload> | null>(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
isSubmitBlocked,
|
||||||
|
submitBlockMessage,
|
||||||
|
loadLinkedCount,
|
||||||
|
guardSubmitOrNotify,
|
||||||
|
} = useCategoryEditGuard({
|
||||||
|
endpoint: '/composants',
|
||||||
|
filterKey: 'typeComposant',
|
||||||
|
labels: {
|
||||||
|
singular: 'composant',
|
||||||
|
plural: 'composants',
|
||||||
|
verifying: 'Vérification des composants liés en cours…',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const title = computed(() =>
|
const title = computed(() =>
|
||||||
initialData.value?.name
|
initialData.value?.name
|
||||||
? `Modifier « ${initialData.value.name} »`
|
? `Modifier « ${initialData.value.name} »`
|
||||||
@@ -88,6 +106,8 @@ const loadCategory = async () => {
|
|||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: response.structure ?? undefined,
|
structure: response.structure ?? undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await loadLinkedCount(id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(normalizeError(error))
|
showError(normalizeError(error))
|
||||||
await navigateBackToList()
|
await navigateBackToList()
|
||||||
@@ -101,6 +121,9 @@ const handleCancel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
|
const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
|
||||||
|
if (guardSubmitOrNotify()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const id = String(route.params.id)
|
const id = String(route.params.id)
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
:initial-data="initialData"
|
:initial-data="initialData"
|
||||||
:lock-category="true"
|
:lock-category="true"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
|
:disable-submit="isSubmitBlocked"
|
||||||
|
:disable-submit-message="submitBlockMessage"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
/>
|
/>
|
||||||
@@ -38,6 +40,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useHead, useRoute, useRouter } from '#imports'
|
import { useHead, useRoute, useRouter } from '#imports'
|
||||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||||
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
||||||
|
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -48,6 +51,21 @@ const loading = ref(true)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const initialData = ref<Partial<ModelTypePayload> | null>(null)
|
const initialData = ref<Partial<ModelTypePayload> | null>(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
isSubmitBlocked,
|
||||||
|
submitBlockMessage,
|
||||||
|
loadLinkedCount,
|
||||||
|
guardSubmitOrNotify,
|
||||||
|
} = useCategoryEditGuard({
|
||||||
|
endpoint: '/pieces',
|
||||||
|
filterKey: 'typePiece',
|
||||||
|
labels: {
|
||||||
|
singular: 'pièce',
|
||||||
|
plural: 'pièces',
|
||||||
|
verifying: 'Vérification des pièces liées en cours…',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const title = computed(() =>
|
const title = computed(() =>
|
||||||
initialData.value?.name ? `Modifier « ${initialData.value.name} »` : 'Modifier une catégorie de pièce',
|
initialData.value?.name ? `Modifier « ${initialData.value.name} »` : 'Modifier une catégorie de pièce',
|
||||||
)
|
)
|
||||||
@@ -86,6 +104,8 @@ const loadCategory = async () => {
|
|||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: response.structure ?? undefined,
|
structure: response.structure ?? undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await loadLinkedCount(id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(normalizeError(error))
|
showError(normalizeError(error))
|
||||||
await navigateBackToList()
|
await navigateBackToList()
|
||||||
@@ -99,6 +119,9 @@ const handleCancel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
|
const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
|
||||||
|
if (guardSubmitOrNotify()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const id = String(route.params.id)
|
const id = String(route.params.id)
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
:initial-data="initialData"
|
:initial-data="initialData"
|
||||||
:lock-category="true"
|
:lock-category="true"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
|
:disable-submit="isSubmitBlocked"
|
||||||
|
:disable-submit-message="submitBlockMessage"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
/>
|
/>
|
||||||
@@ -38,6 +40,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useHead, useRoute, useRouter } from '#imports'
|
import { useHead, useRoute, useRouter } from '#imports'
|
||||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||||
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
||||||
|
import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -48,6 +51,21 @@ const loading = ref(true)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const initialData = ref<Partial<ModelTypePayload> | null>(null)
|
const initialData = ref<Partial<ModelTypePayload> | null>(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
isSubmitBlocked,
|
||||||
|
submitBlockMessage,
|
||||||
|
loadLinkedCount,
|
||||||
|
guardSubmitOrNotify,
|
||||||
|
} = useCategoryEditGuard({
|
||||||
|
endpoint: '/products',
|
||||||
|
filterKey: 'typeProduct',
|
||||||
|
labels: {
|
||||||
|
singular: 'produit',
|
||||||
|
plural: 'produits',
|
||||||
|
verifying: 'Vérification des produits liés en cours…',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const title = computed(() =>
|
const title = computed(() =>
|
||||||
initialData.value?.name ? `Modifier « ${initialData.value.name} »` : 'Modifier une catégorie de produit',
|
initialData.value?.name ? `Modifier « ${initialData.value.name} »` : 'Modifier une catégorie de produit',
|
||||||
)
|
)
|
||||||
@@ -86,6 +104,8 @@ const loadCategory = async () => {
|
|||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: response.structure ?? undefined,
|
structure: response.structure ?? undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await loadLinkedCount(id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(normalizeError(error))
|
showError(normalizeError(error))
|
||||||
await navigateBackToList()
|
await navigateBackToList()
|
||||||
@@ -99,6 +119,9 @@ const handleCancel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
|
const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
|
||||||
|
if (guardSubmitOrNotify()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const id = String(route.params.id)
|
const id = String(route.params.id)
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user