feat(front) : onglet comptabilite prestataire (ERP-144) (#106)
Auto Tag Develop / tag (push) Successful in 8s
Auto Tag Develop / tag (push) Successful in 8s
Empilée sur ERP-143 (#105). ## Périmètre ERP-144 Onglet **Comptabilité** de l'écran `/providers/new` — gated par permission + blocs RIB conditionnels. - Champs (`Malio*`) : SIREN / Numéro de compte / Mode de TVA (`/api/tva_modes`) / N° de TVA / Délai (`/api/payment_delays`) / Type de règlement (`/api/payment_types`) / Banque (`/api/banks`). - **RG-3.07** : Banque visible **et** obligatoire **seulement si** Type = `VIREMENT` (affichage conditionnel + payload `bank` forcé à null sinon). - **RG-3.08** : blocs RIB (Libellé/BIC/IBAN) affichés et requis si Type = `LCR` ; « + RIB » gated (dernier RIB complet) / Supprimer (modal). À la validation, **POST des RIB AVANT** le PATCH des scalaires (le back valide RG-3.08 sur le PATCH). - **Gating** : onglet présent uniquement si `technique.providers.accounting.view` ; **éditable** uniquement si `.manage` (sinon lecture seule). Masqué pour Bureau/Commerciale. - « Valider » → PATCH `/api/providers/{id}` (groupe `provider:write:accounting`) + sous-ressource RIBs (`/providers/{id}/ribs` + `/provider_ribs/{id}`). Erreurs 422 inline (scalaires) et par ligne (RIB). - `useProviderReferentials.loadAccounting()` (chargé seulement si l'onglet est accessible). Helpers purs `utils/forms/providerAccounting.ts`. - i18n `technique.providers.form.accounting` + `confirmDelete.rib`. > NB : les placeholders **Rapports / Échanges** relèvent des écrans Consultation/Modification (ERP-145) — le flux de **création** ne porte que 3 onglets (Contact/Adresse/Comptabilité), conformément à la spec. ## Conformité - `useApi()` only ; `Malio*` only ; pas de masque email ; aucun texte FR en dur ; pas d'import inter-module (helpers ré-implémentés côté Technique, règle ABSOLUE n°1). ## Vérifications - Vitest : 454/454 (18 nouveaux : helpers compta RG-3.07/3.08, workflow VIREMENT/LCR, ordre RIB→scalaires, 422 inline + par ligne, lecture seule sans manage). - ESLint : OK. - `nuxi typecheck` : 0 erreur sur les fichiers source du ticket. - Golden path navigateur : page compile, onglet Comptabilité visible (gating accounting.view OK pour admin). Contenu de l'onglet gaté derrière le déverrouillage des 3 onglets (multiselect `Malio` non pilotable en a11y) — couvert par les tests unitaires + typecheck. Reviewed-on: #106 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #106.
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user