[ERP-64] Page Consultation client (lecture seule + Modifier / Archiver) (#49)
Auto Tag Develop / tag (push) Successful in 9s
Auto Tag Develop / tag (push) Successful in 9s
## ERP-64 — Page Consultation client (lecture seule)
Route **`/clients/[id]`** : consultation client en lecture seule, porte vers Modification + actions Archiver / Restaurer.
### Périmètre (front uniquement)
- **`useClient(id)`** : charge le détail (embed contacts / adresses / ribs), `archive()` / `restore()` via `PATCH { isArchived }` **seul**, puis **refetch complet** (la réponse du PATCH ne porte pas l'embed). Le **409** de conflit d'homonyme à la restauration (RG-1.23) est propagé → toast dédié.
- **Page** : formulaire principal + **8 onglets** readonly en **navigation libre** (4 actifs + 4 placeholders). Onglet **Comptabilité** visible **uniquement avec `accounting.view`**.
- **Boutons** : **Modifier** si `manage` OU `accounting.manage` ; **Archiver** si `archive` et client actif ; **Restaurer** si `archive` et client archivé.
- Téléphones affichés formatés `XX XX XX XX XX`.
- Réutilise `ClientContactBlock` / `ClientAddressBlock` / `TabPlaceholderBlank` (ERP-63) en mode `readonly`.
### Libellés issus de l'embed (role-independant)
`GET /api/categories` et `/api/sites` renvoient **403 pour les rôles métier non-admin**. La page lit donc tous les libellés (catégories, sites, référentiels comptables) **directement dans le payload embarqué** — affichage correct pour tous les rôles, sans dépendre d'un `GET` de référentiel.
### Correctifs `ClientAddressBlock` (lecture seule)
- la **ville** courante est toujours présente dans les options (sinon `MalioSelect` n'affiche rien) ;
- la **rue** s'affiche en champ texte readonly (`MalioInputAutocomplete` ne réaffiche pas sa valeur liée).
### Pas de changement back
L'embed `GET /api/clients/{id}` (contacts/adresses/ribs + sites + codes catégories, gating `accounting.view`, 409 restauration) **était déjà livré par ERP-62 (#44)** — vérifié sur l'API réelle et couvert par `ClientApiTest::testGetDetailEmbedsSubCollections`, `ClientReadGroupContextBuilderTest`, `ClientArchiveTest::testRestoreConflictReturns409`.
### Tests
- Vitest : **+29 tests** (mapping payload→brouillons, options embed, permissions, archive/restore/409). Suite complète **158 OK**.
- `nuxi typecheck` : 0 erreur sur les fichiers ajoutés.
- Golden path navigateur (admin + commerciale) : readonly complet, onglet Compta + RIBs selon `accounting.view`, boutons selon rôle, bascule Archiver ↔ Restaurer.
### ⚠️ À investiguer (hors périmètre)
Le 403 sur `/categories` et `/sites` impacte aussi `useClientReferentials.loadCommon()` (un `Promise.all` qui rejette en entier) → potentiellement le **formulaire de création ERP-63 cassé pour la Commerciale** (impossible de choisir catégories/sites). À confirmer dans un ticket dédié.
Reviewed-on: #49
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #49.
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- En-tete : retour repertoire + nom du client + actions (Modifier / Archiver|Restaurer). -->
|
||||
<div class="flex items-center gap-3">
|
||||
<MalioButtonIcon
|
||||
icon="mdi:arrow-left-bold"
|
||||
icon-size="24"
|
||||
variant="ghost"
|
||||
v-bind="{ ariaLabel: t('commercial.clients.consultation.back') }"
|
||||
@click="goBack"
|
||||
/>
|
||||
<h1 class="text-[32px] font-bold text-m-primary">{{ headerTitle }}</h1>
|
||||
|
||||
<!-- gap-12 = 48px : meme espacement que Ajouter / Filtres du repertoire. -->
|
||||
<div class="ml-auto flex items-center gap-12">
|
||||
<MalioButton
|
||||
v-if="canEdit"
|
||||
variant="secondary"
|
||||
icon-name="mdi:pencil-outline"
|
||||
icon-position="left"
|
||||
:label="t('commercial.clients.action.edit')"
|
||||
@click="goEdit"
|
||||
/>
|
||||
<MalioButton
|
||||
v-if="showArchive"
|
||||
variant="secondary"
|
||||
icon-name="mdi:archive-arrow-down-outline"
|
||||
icon-position="left"
|
||||
:label="t('commercial.clients.action.archive')"
|
||||
@click="askToggleArchive"
|
||||
/>
|
||||
<MalioButton
|
||||
v-if="showRestore"
|
||||
variant="secondary"
|
||||
icon-name="mdi:archive-arrow-up-outline"
|
||||
icon-position="left"
|
||||
:label="t('commercial.clients.action.restore')"
|
||||
@click="askToggleArchive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Etats de chargement / introuvable. -->
|
||||
<p v-if="loading" class="mt-12 text-center text-black/60">{{ t('commercial.clients.consultation.loading') }}</p>
|
||||
<p v-else-if="error" class="mt-12 text-center text-m-danger">{{ t('commercial.clients.consultation.notFound') }}</p>
|
||||
|
||||
<template v-else-if="client">
|
||||
<!-- ── Formulaire principal (lecture seule) ──────────────────────── -->
|
||||
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
:model-value="client.companyName"
|
||||
:label="t('commercial.clients.form.main.companyName')"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="client.lastName"
|
||||
:label="t('commercial.clients.form.main.lastName')"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="client.firstName"
|
||||
:label="t('commercial.clients.form.main.firstName')"
|
||||
readonly
|
||||
/>
|
||||
<MalioSelectCheckbox
|
||||
:model-value="categoryIris"
|
||||
:options="mainCategoryOptions"
|
||||
:label="t('commercial.clients.form.main.categories')"
|
||||
:display-tag="true"
|
||||
disabled
|
||||
/>
|
||||
<MalioInputPhone
|
||||
v-for="(phone, index) in mainPhones"
|
||||
:key="index"
|
||||
:model-value="phone"
|
||||
:label="index === 0 ? t('commercial.clients.form.main.phonePrimary') : t('commercial.clients.form.main.phoneSecondary')"
|
||||
:mask="PHONE_MASK"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputEmail
|
||||
:model-value="client.email"
|
||||
:label="t('commercial.clients.form.main.email')"
|
||||
readonly
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="relation.type"
|
||||
:model-value="relation.type"
|
||||
:options="relationOptions"
|
||||
:label="t('commercial.clients.form.main.relation')"
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="relation.type"
|
||||
:model-value="relation.name"
|
||||
:label="relation.type === 'distributeur' ? t('commercial.clients.form.main.distributorName') : t('commercial.clients.form.main.brokerName')"
|
||||
readonly
|
||||
/>
|
||||
<MalioCheckbox
|
||||
:model-value="client.triageService === true"
|
||||
:label="t('commercial.clients.form.main.triageService')"
|
||||
group-class="self-center"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ── Onglets (navigation libre, tout en lecture seule) ─────────── -->
|
||||
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
||||
<!-- Onglet Information -->
|
||||
<template #information>
|
||||
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
||||
<MalioInputTextArea
|
||||
:model-value="information.description"
|
||||
:label="t('commercial.clients.form.information.description')"
|
||||
resize="none"
|
||||
group-class="row-span-2 pt-1"
|
||||
text-input="h-full text-lg"
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="information.competitors"
|
||||
:label="t('commercial.clients.form.information.competitors')"
|
||||
readonly
|
||||
/>
|
||||
<MalioDate
|
||||
:model-value="information.foundedAt"
|
||||
:label="t('commercial.clients.form.information.foundedAt')"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="information.employeesCount"
|
||||
:label="t('commercial.clients.form.information.employeesCount')"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputAmount
|
||||
:model-value="information.revenueAmount"
|
||||
:label="t('commercial.clients.form.information.revenueAmount')"
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="information.directorName"
|
||||
:label="t('commercial.clients.form.information.directorName')"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputAmount
|
||||
:model-value="information.profitAmount"
|
||||
:label="t('commercial.clients.form.information.profitAmount')"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Onglet Contact -->
|
||||
<template #contact>
|
||||
<div class="mt-12 flex flex-col gap-6">
|
||||
<ClientContactBlock
|
||||
v-for="(contact, index) in contacts"
|
||||
:key="contact.id ?? index"
|
||||
:model-value="contact"
|
||||
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
||||
readonly
|
||||
/>
|
||||
<p v-if="contacts.length === 0" class="text-center text-black/60">
|
||||
{{ t('commercial.clients.consultation.emptyContacts') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Onglet Adresse -->
|
||||
<template #address>
|
||||
<div class="mt-12 flex flex-col gap-6">
|
||||
<ClientAddressBlock
|
||||
v-for="(view, index) in addressViews"
|
||||
:key="view.draft.id ?? index"
|
||||
:model-value="view.draft"
|
||||
:title="t('commercial.clients.form.address.title', { n: index + 1 })"
|
||||
:category-options="view.categoryOptions"
|
||||
:site-options="view.siteOptions"
|
||||
:contact-options="contactOptions"
|
||||
:country-options="countryOptions"
|
||||
readonly
|
||||
/>
|
||||
<p v-if="addressViews.length === 0" class="text-center text-black/60">
|
||||
{{ t('commercial.clients.consultation.emptyAddresses') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
||||
<template v-if="canAccountingView" #accounting>
|
||||
<div class="mt-12 flex flex-col gap-6">
|
||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
||||
<div class="grid grid-cols-3 gap-x-[80px] gap-y-5">
|
||||
<MalioInputText
|
||||
:model-value="accounting.siren"
|
||||
:label="t('commercial.clients.form.accounting.siren')"
|
||||
:mask="SIREN_MASK"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="accounting.accountNumber"
|
||||
:label="t('commercial.clients.form.accounting.accountNumber')"
|
||||
readonly
|
||||
/>
|
||||
<MalioSelect
|
||||
:model-value="accounting.tvaModeIri"
|
||||
:options="tvaModeOptions"
|
||||
:label="t('commercial.clients.form.accounting.tvaMode')"
|
||||
empty-option-label=""
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="accounting.nTva"
|
||||
:label="t('commercial.clients.form.accounting.nTva')"
|
||||
readonly
|
||||
/>
|
||||
<MalioSelect
|
||||
:model-value="accounting.paymentDelayIri"
|
||||
:options="paymentDelayOptions"
|
||||
:label="t('commercial.clients.form.accounting.paymentDelay')"
|
||||
empty-option-label=""
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
:model-value="accounting.paymentTypeIri"
|
||||
:options="paymentTypeOptions"
|
||||
:label="t('commercial.clients.form.accounting.paymentType')"
|
||||
empty-option-label=""
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="accounting.bankIri"
|
||||
:model-value="accounting.bankIri"
|
||||
:options="bankOptions"
|
||||
:label="t('commercial.clients.form.accounting.bank')"
|
||||
empty-option-label=""
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blocs RIB (0..n), lecture seule. -->
|
||||
<div
|
||||
v-for="(rib, index) in ribs"
|
||||
:key="rib.id ?? index"
|
||||
class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
<div class="grid grid-cols-3 gap-x-[80px] gap-y-5">
|
||||
<MalioInputText
|
||||
:model-value="rib.label"
|
||||
:label="t('commercial.clients.form.accounting.ribLabel')"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="rib.bic"
|
||||
:label="t('commercial.clients.form.accounting.ribBic')"
|
||||
readonly
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="rib.iban"
|
||||
:label="t('commercial.clients.form.accounting.ribIban')"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Onglets non encore implementes : frame vide (navigation libre). -->
|
||||
<template #transport><TabPlaceholderBlank /></template>
|
||||
<template #statistics><TabPlaceholderBlank /></template>
|
||||
<template #reports><TabPlaceholderBlank /></template>
|
||||
<template #exchanges><TabPlaceholderBlank /></template>
|
||||
</MalioTabList>
|
||||
</template>
|
||||
|
||||
<!-- Modal de confirmation Archiver / Restaurer. -->
|
||||
<MalioModal v-model="confirmOpen" modal-class="max-w-md">
|
||||
<template #header>
|
||||
<h2 class="text-[24px] font-bold">
|
||||
{{ isArchived ? t('commercial.clients.consultation.confirmRestore.title') : t('commercial.clients.consultation.confirmArchive.title') }}
|
||||
</h2>
|
||||
</template>
|
||||
<p>{{ isArchived ? t('commercial.clients.consultation.confirmRestore.message') : t('commercial.clients.consultation.confirmArchive.message') }}</p>
|
||||
<template #footer>
|
||||
<MalioButton
|
||||
variant="secondary"
|
||||
button-class="flex-1"
|
||||
:label="t('commercial.clients.form.confirmDelete.cancel')"
|
||||
@click="confirmOpen = false"
|
||||
/>
|
||||
<MalioButton
|
||||
:variant="isArchived ? 'primary' : 'danger'"
|
||||
button-class="flex-1"
|
||||
:label="t('commercial.clients.form.confirmDelete.confirm')"
|
||||
:disabled="toggling"
|
||||
@click="confirmToggleArchive"
|
||||
/>
|
||||
</template>
|
||||
</MalioModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useClient } from '~/modules/commercial/composables/useClient'
|
||||
import { buildClientFormTabKeys } from '~/modules/commercial/utils/clientFormRules'
|
||||
import {
|
||||
canEditClient,
|
||||
categoryOptionsOf,
|
||||
contactOptionsOf,
|
||||
mapAccountingDraft,
|
||||
mapAddressView,
|
||||
mapContactToDraft,
|
||||
mapRibToDraft,
|
||||
referentialOptionOf,
|
||||
relationOf,
|
||||
showArchiveAction,
|
||||
showRestoreAction,
|
||||
type ClientDetail,
|
||||
type SelectOption,
|
||||
} from '~/modules/commercial/utils/clientConsultation'
|
||||
import { formatPhoneFR } from '~/shared/utils/phone'
|
||||
|
||||
// Masques d'affichage (purement visuels, la donnee reste celle du serveur).
|
||||
const PHONE_MASK = '## ## ## ## ##'
|
||||
const SIREN_MASK = '#########'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const { can, canAny } = usePermissions()
|
||||
|
||||
// Gating de la route : la consultation exige `view`. Usine (sans view) est
|
||||
// redirige vers le repertoire (lui-meme protege). Cf. matrice § 2.7.
|
||||
if (!can('commercial.clients.view')) {
|
||||
await navigateTo('/clients')
|
||||
}
|
||||
|
||||
const clientId = route.params.id as string
|
||||
|
||||
const { client, loading, error, load, archive, restore } = useClient(clientId)
|
||||
|
||||
// ── Permissions / visibilite des actions ───────────────────────────────────
|
||||
const canAccountingView = computed(() => can('commercial.clients.accounting.view'))
|
||||
const canEdit = computed(() => canEditClient(canAny))
|
||||
const isArchived = computed(() => client.value?.isArchived === true)
|
||||
const showArchive = computed(() => showArchiveAction(can, isArchived.value))
|
||||
const showRestore = computed(() => showRestoreAction(can, isArchived.value))
|
||||
|
||||
const headerTitle = computed(() => client.value?.companyName ?? t('commercial.clients.consultation.title'))
|
||||
|
||||
// ── Donnees derivees du payload (lecture seule) ────────────────────────────
|
||||
const relation = computed(() => (client.value ? relationOf(client.value) : { type: null, name: null }))
|
||||
const categoryIris = computed(() => (client.value?.categories ?? []).map(c => c['@id']))
|
||||
|
||||
// Telephones du formulaire principal, formates XX XX XX XX XX (RG d'affichage).
|
||||
const mainPhones = computed(() =>
|
||||
[client.value?.phonePrimary, client.value?.phoneSecondary]
|
||||
.filter((p): p is string => Boolean(p))
|
||||
.map(formatPhoneFR),
|
||||
)
|
||||
|
||||
const information = computed(() => ({
|
||||
description: client.value?.description ?? null,
|
||||
competitors: client.value?.competitors ?? null,
|
||||
// MalioDate attend strictement YYYY-MM-DD : on tronque l'ISO datetime renvoye.
|
||||
foundedAt: client.value?.foundedAt ? client.value.foundedAt.slice(0, 10) : null,
|
||||
employeesCount: client.value?.employeesCount != null ? String(client.value.employeesCount) : null,
|
||||
revenueAmount: client.value?.revenueAmount ?? null,
|
||||
profitAmount: client.value?.profitAmount ?? null,
|
||||
directorName: client.value?.directorName ?? null,
|
||||
}))
|
||||
|
||||
const contacts = computed(() => (client.value?.contacts ?? []).map(mapContactToDraft))
|
||||
// Vue par adresse : brouillon + options (sites/categories) propres a l'adresse.
|
||||
const addressViews = computed(() => (client.value?.addresses ?? []).map(mapAddressView))
|
||||
const ribs = computed(() => (client.value?.ribs ?? []).map(mapRibToDraft))
|
||||
// Draft comptable (tout null si l'utilisateur n'a pas accounting.view).
|
||||
const accounting = computed(() => mapAccountingDraft(client.value ?? ({} as ClientDetail)))
|
||||
|
||||
// ── Options des selects (construites depuis l'EMBED, jamais via un GET de
|
||||
// referentiel : /categories et /sites sont en 403 pour les roles metier
|
||||
// non-admin, ce qui laisserait les libelles vides). ───────────────────────
|
||||
const mainCategoryOptions = computed(() => categoryOptionsOf(client.value?.categories))
|
||||
const contactOptions = computed(() => contactOptionsOf(client.value?.contacts))
|
||||
|
||||
const relationOptions = computed<SelectOption[]>(() => [
|
||||
{ value: 'distributeur', label: t('commercial.clients.form.main.relationDistributor') },
|
||||
{ value: 'courtier', label: t('commercial.clients.form.main.relationBroker') },
|
||||
])
|
||||
|
||||
const countryOptions: SelectOption[] = [
|
||||
{ value: 'France', label: 'France' },
|
||||
{ value: 'Espagne', label: 'Espagne' },
|
||||
]
|
||||
|
||||
// Selects comptables : libelle issu de l'embed (option unique ou vide).
|
||||
const tvaModeOptions = computed(() => referentialOptionOf(client.value?.tvaMode))
|
||||
const paymentDelayOptions = computed(() => referentialOptionOf(client.value?.paymentDelay))
|
||||
const paymentTypeOptions = computed(() => referentialOptionOf(client.value?.paymentType))
|
||||
const bankOptions = computed(() => referentialOptionOf(client.value?.bank))
|
||||
|
||||
// ── Onglets : navigation LIBRE (pas de sequence forcee en consultation) ────
|
||||
// 4 onglets actifs (Information, Contact, Adresse, + Comptabilite si droit) et
|
||||
// 4 coquilles (Transport, Statistiques, Rapports, Echanges).
|
||||
const tabKeys = computed(() => buildClientFormTabKeys(canAccountingView.value, { includeEditOnlyTabs: true }))
|
||||
|
||||
const TAB_ICONS: Record<string, string> = {
|
||||
information: 'mdi:account-outline',
|
||||
contact: 'mdi:account-box-plus-outline',
|
||||
address: 'mdi:map-marker-outline',
|
||||
transport: 'mdi:truck-delivery-outline',
|
||||
accounting: 'mdi:bank-circle-outline',
|
||||
statistics: 'mdi:finance',
|
||||
reports: 'mdi:file-document-edit-outline',
|
||||
exchanges: 'mdi:account-group-outline',
|
||||
}
|
||||
|
||||
const tabs = computed(() => tabKeys.value.map(key => ({
|
||||
key,
|
||||
label: t(`commercial.clients.tab.${key}`),
|
||||
icon: TAB_ICONS[key],
|
||||
})))
|
||||
|
||||
const activeTab = ref('information')
|
||||
|
||||
// ── Navigation ─────────────────────────────────────────────────────────────
|
||||
function goBack(): void {
|
||||
router.push('/clients')
|
||||
}
|
||||
|
||||
function goEdit(): void {
|
||||
router.push(`/clients/${clientId}/edit`)
|
||||
}
|
||||
|
||||
// ── Archivage / Restauration ────────────────────────────────────────────────
|
||||
const confirmOpen = ref(false)
|
||||
const toggling = ref(false)
|
||||
|
||||
function askToggleArchive(): void {
|
||||
confirmOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirme l'archivage ou la restauration (PATCH isArchived seul). Gere le 409
|
||||
* de conflit d'homonyme actif a la restauration (RG-1.23) avec un message dedie.
|
||||
*/
|
||||
async function confirmToggleArchive(): Promise<void> {
|
||||
if (toggling.value) return
|
||||
toggling.value = true
|
||||
const restoring = isArchived.value
|
||||
try {
|
||||
if (restoring) {
|
||||
await restore()
|
||||
toast.success({ title: t('commercial.clients.toast.restoreSuccess') })
|
||||
}
|
||||
else {
|
||||
await archive()
|
||||
toast.success({ title: t('commercial.clients.toast.archiveSuccess') })
|
||||
}
|
||||
confirmOpen.value = false
|
||||
}
|
||||
catch (e) {
|
||||
const status = (e as { response?: { status?: number } })?.response?.status
|
||||
toast.error({
|
||||
title: t('commercial.clients.toast.error'),
|
||||
message: restoring && status === 409
|
||||
? t('commercial.clients.toast.restoreConflict')
|
||||
: t('commercial.clients.toast.error'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
toggling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
useHead({ title: headerTitle })
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
Reference in New Issue
Block a user