feat(front) : onglet comptabilite prestataire
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m39s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m21s

This commit is contained in:
2026-06-15 09:51:56 +02:00
parent 3d4ae391fe
commit a8c5166eb2
8 changed files with 793 additions and 5 deletions
@@ -2,15 +2,20 @@ import { computed, reactive, ref, type Ref } from 'vue'
import { useFormErrors } from '~/shared/composables/useFormErrors'
import { mapViolationsToRecord } from '~/shared/utils/api'
import {
emptyProviderAccounting,
emptyProviderAddress,
emptyProviderContact,
emptyProviderMain,
emptyProviderRib,
type ProviderAccountingDraft,
type ProviderAddressFormDraft,
type ProviderAddressResponse,
type ProviderContactFormDraft,
type ProviderContactResponse,
type ProviderMainDraft,
type ProviderMainResponse,
type ProviderRibFormDraft,
type ProviderRibResponse,
} from '~/modules/technique/types/providerForm'
import {
buildProviderContactPayload,
@@ -20,6 +25,12 @@ import {
buildProviderAddressPayload,
isProviderAddressValid,
} from '~/modules/technique/utils/forms/providerAddress'
import {
buildProviderAccountingPayload,
buildProviderRibPayload,
isRibBlank,
isRibComplete,
} from '~/modules/technique/utils/forms/providerAccounting'
/**
* Workflow de l'ecran « Ajouter un prestataire » (M3 Technique, ERP-141) —
@@ -72,6 +83,7 @@ export function useProviderForm() {
// ── Onglets : ordre + gating progressif ───────────────────────────────────
const canAccountingView = computed(() => can('technique.providers.accounting.view'))
const canAccountingManage = computed(() => can('technique.providers.accounting.manage'))
const tabKeys = computed(() => buildProviderCreateTabKeys(canAccountingView.value))
// Index du dernier onglet deverrouille (-1 tant que le prestataire n'est pas cree).
@@ -370,6 +382,130 @@ export function useProviderForm() {
}
}
// ── Onglet Comptabilite (ERP-144) ─────────────────────────────────────────
const accounting = reactive<ProviderAccountingDraft>(emptyProviderAccounting())
const ribs = ref<ProviderRibFormDraft[]>([])
const accountingErrors = useFormErrors()
// Erreurs 422 par ligne de RIB (alignees sur l'index du v-for).
const ribErrors = ref<Record<string, string>[]>([])
// L'onglet est editable seulement avec accounting.manage (sinon lecture seule).
const accountingReadonly = computed(() => isValidated('accounting') || !canAccountingManage.value)
/**
* Met a jour le type de reglement (IRI) en propageant ses RG inter-champs :
* - hors VIREMENT (RG-3.07) : on vide la banque (sans objet) ;
* - LCR (RG-3.08) : on garantit au moins un bloc RIB visible ; hors LCR, on
* purge les erreurs de RIB (les blocs sont conserves mais non persistes).
* `isBankRequired` / `isRibRequired` sont calcules par l'appelant (page) a
* partir du code resolu via les referentiels.
*/
function setPaymentType(iri: string | null, isBankRequired: boolean, isRibRequired: boolean): void {
accounting.paymentTypeIri = iri
if (!isBankRequired) {
accounting.bankIri = null
}
if (isRibRequired) {
if (ribs.value.length === 0) {
ribs.value.push(emptyProviderRib())
}
}
else {
ribErrors.value = []
}
}
// « + RIB » desactive tant que le dernier bloc RIB n'est pas complet (RG-3.08).
const canAddRib = computed(() => {
const last = ribs.value[ribs.value.length - 1]
return last !== undefined && isRibComplete(last)
})
function addRib(): void {
if (canAddRib.value) {
ribs.value.push(emptyProviderRib())
}
}
function removeRib(index: number): void {
ribs.value.splice(index, 1)
ribErrors.value.splice(index, 1)
// Garde au moins un bloc RIB visible (sous LCR).
if (ribs.value.length === 0) {
ribs.value.push(emptyProviderRib())
}
}
/**
* Valide l'onglet Comptabilite : (1) sous LCR, POST/PATCH des RIB d'abord
* (le back valide RG-3.08 sur le PATCH scalaires, les RIB doivent donc exister
* AVANT) ; (2) PATCH des scalaires comptables (groupe provider:write:accounting,
* banque envoyee seulement si VIREMENT — RG-3.07). Erreurs RIB par ligne ;
* erreurs scalaires inline (bank/paymentType). Retourne true si l'onglet a ete
* valide.
*/
async function submitAccounting(
isBankRequired: boolean,
isRibRequired: boolean,
onRibError: (error: unknown) => void,
): Promise<boolean> {
if (providerId.value === null || tabSubmitting.value) {
return false
}
tabSubmitting.value = true
accountingErrors.clearErrors()
try {
// 1) RIB d'abord, uniquement sous LCR. Une amorce vide neuve est sautee
// s'il reste un autre RIB soumettable ; sinon (LCR sans aucun RIB rempli)
// on la soumet pour declencher la 422 NotBlank inline.
if (isRibRequired) {
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
const ribHasError = await submitRows(
ribs.value,
ribErrors,
async (rib) => {
const body = buildProviderRibPayload(rib)
if (rib.id === null) {
const created = await api.post<ProviderRibResponse>(
`/providers/${providerId.value}/ribs`,
body,
{ headers: { Accept: 'application/ld+json' }, toast: false },
)
rib.id = created.id
}
else {
await api.patch(`/provider_ribs/${rib.id}`, body, { toast: false })
}
},
onRibError,
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
)
if (ribHasError) {
return false
}
}
// 2) PATCH des scalaires comptables (erreurs inline sur leurs champs).
try {
await api.patch(
`/providers/${providerId.value}`,
buildProviderAccountingPayload(accounting, isBankRequired),
{ toast: false },
)
}
catch (error) {
accountingErrors.handleApiError(error, { fallbackMessage: t('technique.providers.toast.error') })
return false
}
completeTab('accounting')
return true
}
finally {
tabSubmitting.value = false
}
}
return {
// etat
main,
@@ -380,6 +516,7 @@ export function useProviderForm() {
mainErrors,
// onglets
canAccountingView,
canAccountingManage,
tabKeys,
activeTab,
unlockedIndex,
@@ -399,6 +536,17 @@ export function useProviderForm() {
addAddress,
removeAddress,
submitAddresses,
// comptabilite
accounting,
ribs,
accountingErrors,
ribErrors,
accountingReadonly,
setPaymentType,
canAddRib,
addRib,
removeRib,
submitAccounting,
// actions
validateMainFront,
buildMainPayload,