import { computed, reactive, ref } from 'vue' import { useFormErrors } from '~/shared/composables/useFormErrors' import { emptyProviderMain, type ProviderMainDraft, type ProviderMainResponse, } from '~/modules/technique/types/providerForm' /** * Workflow de l'ecran « Ajouter un prestataire » (M3 Technique, ERP-141) — * miroir conceptuel de la logique de creation fournisseur (M2), extraite ici en * composable. * * Particularites M3 (cf. spec-front § « Ecran Ajouter ») : * - PAS d'onglet « Information » : le formulaire principal est minimal (Nom + * Categorie + Site). * - Selecteur de site SUR le formulaire principal (RG-3.03, relation directe * `provider.sites`). * - Creation incrementale par onglets (Contact · Adresse · Comptabilite) : * POST principal puis PATCH partiels par groupe de serialisation * (`provider:write:*`, mode strict — spec-back § 2.10). Le contenu des onglets * arrive aux tickets ERP-142 → 144 ; ce composable pose le POST principal et * l'orchestration des onglets. * * Etat 100 % local a l'instance (refs/reactive) — aucune persistance URL. */ /** * Cles des onglets du FLUX DE CREATION. Pas d'onglet « Information » au M3 ; * « Rapports » / « Echanges » n'apparaissent qu'en consultation/modification. * L'onglet « Comptabilite » n'est present que pour les roles qui peuvent le voir * (`technique.providers.accounting.view` — Admin, Compta). */ export function buildProviderCreateTabKeys(canAccountingView: boolean): string[] { return canAccountingView ? ['contact', 'address', 'accounting'] : ['contact', 'address'] } export function useProviderForm() { const api = useApi() const { t } = useI18n() const toast = useToast() const { can } = usePermissions() // Erreurs de validation par champ (ERP-101) du formulaire principal. const mainErrors = useFormErrors() // ── Etat du prestataire cree ──────────────────────────────────────────── const providerId = ref(null) const mainLocked = ref(false) const mainSubmitting = ref(false) const tabSubmitting = ref(false) // ── Formulaire principal ────────────────────────────────────────────────── const main = reactive(emptyProviderMain()) // ── Onglets : ordre + gating progressif ─────────────────────────────────── const canAccountingView = computed(() => can('technique.providers.accounting.view')) const tabKeys = computed(() => buildProviderCreateTabKeys(canAccountingView.value)) // Index du dernier onglet deverrouille (-1 tant que le prestataire n'est pas cree). const unlockedIndex = ref(-1) const activeTab = ref('contact') // Onglets valides (passent en lecture seule). const validated = reactive>({}) function isValidated(key: string): boolean { return validated[key] === true } function tabIndex(key: string): number { return tabKeys.value.indexOf(key) } /** * Validation FRONT du formulaire principal : RG-3.03 (>= 1 site) et RG-3.09 * (>= 1 categorie). Pose les erreurs inline et retourne false si invalide. * Le back reste la couche autoritaire (ERP-101) ; ce pre-check evite un * aller-retour inutile et porte la garantie RG-3.03 cote front. */ function validateMainFront(): boolean { let valid = true if (main.siteIris.length === 0) { mainErrors.setError('sites', t('technique.providers.form.errors.siteRequired')) valid = false } if (main.categoryIris.length === 0) { mainErrors.setError('categories', t('technique.providers.form.errors.categoryRequired')) valid = false } return valid } /** * Payload du POST principal (groupe `provider:write:main`). `companyName` est * omis s'il est vide afin que la 422 porte la violation NotBlank (RG-3.11) sur * le champ plutot qu'une erreur de type. Les relations M2M partent en IRI. */ function buildMainPayload(): Record { const payload: Record = { categories: [...main.categoryIris], sites: [...main.siteIris], } if (main.companyName?.trim()) { payload.companyName = main.companyName } return payload } /** * POST /providers (groupe `provider:write:main`). Pre-check front RG-3.03/3.09, * puis creation. Au succes : verrouille le bloc principal, deverrouille le 1er * onglet et bascule sur « Contact ». Retourne true si cree, false sinon. */ async function submitMain(): Promise { if (mainSubmitting.value) return false mainErrors.clearErrors() if (!validateMainFront()) return false mainSubmitting.value = true try { const created = await api.post('/providers', buildMainPayload(), { headers: { Accept: 'application/ld+json' }, toast: false, }) providerId.value = created.id // Reaffiche la valeur normalisee renvoyee par le serveur (UPPERCASE, RG-3.11). main.companyName = created.companyName ?? main.companyName mainLocked.value = true unlockedIndex.value = 0 activeTab.value = tabKeys.value[0] ?? 'contact' toast.success({ title: t('technique.providers.toast.createSuccess') }) return true } catch (error) { // 409 = doublon de nom (RG-3.10) → erreur inline dediee + toast ; // 422 → mapping inline par champ ; autre → toast de fallback (ERP-101). const status = (error as { response?: { status?: number } })?.response?.status if (status === 409) { const message = t('technique.providers.form.duplicateCompany') mainErrors.setError('companyName', message) toast.error({ title: t('technique.providers.toast.error'), message }) } else { mainErrors.handleApiError(error, { fallbackMessage: t('technique.providers.toast.error') }) } return false } finally { mainSubmitting.value = false } } /** * PATCH partiel du prestataire (mode strict : un seul groupe de serialisation * par appel — spec-back § 2.10). Sert l'onglet Comptabilite a champs scalaires * (ERP-144) ; les onglets Contact/Adresse passent par leurs sous-ressources * (POST/PATCH par ligne, ERP-142/143). No-op tant que le prestataire n'existe pas. */ async function patchProvider(payload: Record): Promise { if (providerId.value === null) return await api.patch(`/providers/${providerId.value}`, payload, { toast: false }) } /** * Marque un onglet valide (passe en lecture seule), deverrouille et avance a * l'onglet suivant. Retourne true si c'etait le dernier onglet du flux * (creation terminee), false sinon. */ function completeTab(key: string): boolean { validated[key] = true const index = tabIndex(key) const next = tabKeys.value[index + 1] if (next === undefined) { return true } unlockedIndex.value = Math.max(unlockedIndex.value, index + 1) activeTab.value = next return false } return { // etat main, providerId, mainLocked, mainSubmitting, tabSubmitting, mainErrors, // onglets canAccountingView, tabKeys, activeTab, unlockedIndex, validated, isValidated, // actions validateMainFront, buildMainPayload, submitMain, patchProvider, completeTab, } }