feat(front) : referentiels + blocs reutilisables contact/adresse/placeholder (ERP-63)
- useClientReferentials : chargement des selects (categories, sites, modes TVA, delais/types de reglement, banques) en ?pagination=false + listes distributeurs/courtiers via ?categoryCode=DISTRIBUTEUR|COURTIER. - ClientContactBlock / ClientAddressBlock : blocs reutilises par 1.11/1.12. L'adresse gere la saisie assistee BAN (via le stub) avec bascule en mode degrade (ville/adresse en saisie libre). - TabPlaceholderBlank : frame vide des onglets non implementes.
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="rounded-md border border-neutral-200 bg-white p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-bold">{{ title }}</h3>
|
||||
<!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
|
||||
<MalioButtonIcon
|
||||
v-if="removable && !readonly"
|
||||
icon="mdi:trash-can-outline"
|
||||
variant="ghost"
|
||||
v-bind="{ ariaLabel: t('commercial.clients.form.address.remove') }"
|
||||
@click="$emit('remove')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Usage de l'adresse : Prospect exclusif de Livraison/Facturation
|
||||
(RG-1.06/07/08). On masque l'option incompatible selon l'etat. -->
|
||||
<div class="mb-4 flex flex-wrap gap-6">
|
||||
<MalioCheckbox
|
||||
v-if="canSelectProspect(model)"
|
||||
:model-value="model.isProspect"
|
||||
:label="t('commercial.clients.form.address.prospect')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: boolean) => toggleFlag('isProspect', v)"
|
||||
/>
|
||||
<MalioCheckbox
|
||||
v-if="canSelectDeliveryOrBilling(model)"
|
||||
:model-value="model.isDelivery"
|
||||
:label="t('commercial.clients.form.address.delivery')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: boolean) => toggleFlag('isDelivery', v)"
|
||||
/>
|
||||
<MalioCheckbox
|
||||
v-if="canSelectDeliveryOrBilling(model)"
|
||||
:model-value="model.isBilling"
|
||||
:label="t('commercial.clients.form.address.billing')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: boolean) => toggleFlag('isBilling', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<MalioSelectCheckbox
|
||||
:model-value="model.categoryIris"
|
||||
:options="categoryOptions"
|
||||
:label="t('commercial.clients.form.address.categories')"
|
||||
:display-tag="true"
|
||||
:disabled="readonly"
|
||||
@update:model-value="(v: (string | number)[]) => update('categoryIris', v.map(String))"
|
||||
/>
|
||||
|
||||
<MalioSelect
|
||||
:model-value="model.country"
|
||||
:options="countryOptions"
|
||||
:label="t('commercial.clients.form.address.country')"
|
||||
:disabled="readonly"
|
||||
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
||||
/>
|
||||
|
||||
<MalioInputText
|
||||
:model-value="model.postalCode"
|
||||
:label="t('commercial.clients.form.address.postalCode')"
|
||||
:mask="POSTAL_CODE_MASK"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onPostalCodeChange"
|
||||
/>
|
||||
|
||||
<!-- Ville : MalioSelect alimente par le code postal (BAN). En mode
|
||||
degrade (service indisponible), bascule en saisie libre. -->
|
||||
<MalioSelect
|
||||
v-if="!degraded"
|
||||
:model-value="model.city"
|
||||
:options="cityOptions"
|
||||
:label="t('commercial.clients.form.address.city')"
|
||||
:disabled="readonly"
|
||||
empty-option-label=""
|
||||
@update:model-value="(v: string | number | null) => update('city', v === null ? null : String(v))"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-else
|
||||
:model-value="model.city"
|
||||
:label="t('commercial.clients.form.address.city')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('city', v)"
|
||||
/>
|
||||
|
||||
<!-- Adresse : saisie assistee (BAN) ou libre en mode degrade. -->
|
||||
<MalioInputAutocomplete
|
||||
v-if="!degraded"
|
||||
:model-value="model.street"
|
||||
:options="addressOptions"
|
||||
:loading="addressLoading"
|
||||
:min-search-length="3"
|
||||
:label="t('commercial.clients.form.address.street')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string | number | null) => update('street', v === null ? null : String(v))"
|
||||
@search="onAddressSearch"
|
||||
@select="onAddressSelect"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-else
|
||||
:model-value="model.street"
|
||||
:label="t('commercial.clients.form.address.street')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('street', v)"
|
||||
/>
|
||||
|
||||
<MalioInputText
|
||||
:model-value="model.streetComplement"
|
||||
:label="t('commercial.clients.form.address.streetComplement')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('streetComplement', v)"
|
||||
/>
|
||||
|
||||
<MalioSelectCheckbox
|
||||
:model-value="model.siteIris"
|
||||
:options="siteOptions"
|
||||
:label="t('commercial.clients.form.address.sites')"
|
||||
:display-tag="true"
|
||||
:disabled="readonly"
|
||||
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
||||
/>
|
||||
|
||||
<MalioSelectCheckbox
|
||||
:model-value="model.contactIris"
|
||||
:options="contactOptions"
|
||||
:label="t('commercial.clients.form.address.contacts')"
|
||||
:display-tag="true"
|
||||
:disabled="readonly"
|
||||
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
||||
/>
|
||||
|
||||
<!-- Email de facturation : visible/obligatoire seulement si Facturation
|
||||
est coche (RG-1.11). -->
|
||||
<MalioInputText
|
||||
v-if="isBillingEmailRequired(model)"
|
||||
:model-value="model.billingEmail"
|
||||
:label="t('commercial.clients.form.address.billingEmail')"
|
||||
:required="true"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('billingEmail', v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
applyProspectExclusivity,
|
||||
canSelectDeliveryOrBilling,
|
||||
canSelectProspect,
|
||||
isBillingEmailRequired,
|
||||
type AddressFlagsDraft,
|
||||
} from '~/modules/commercial/utils/clientFormRules'
|
||||
import { useAddressAutocomplete, type AddressSuggestion } from '~/shared/composables/useAddressAutocomplete'
|
||||
import type { CategoryOption, RefOption } from '~/modules/commercial/composables/useClientReferentials'
|
||||
import type { AddressFormDraft } from '~/modules/commercial/types/clientForm'
|
||||
|
||||
// Masque code postal FR : 5 chiffres.
|
||||
const POSTAL_CODE_MASK = '#####'
|
||||
|
||||
const props = defineProps<{
|
||||
/** Brouillon de l'adresse (v-model). */
|
||||
modelValue: AddressFormDraft
|
||||
title: string
|
||||
/** Categories autorisees sur une adresse (DISTRIBUTEUR/COURTIER exclus, RG-1.29). */
|
||||
categoryOptions: CategoryOption[]
|
||||
/** Sites Starseed disponibles. */
|
||||
siteOptions: RefOption[]
|
||||
/** Contacts deja saisis, rattachables a l'adresse. */
|
||||
contactOptions: RefOption[]
|
||||
/** Pays disponibles (France par defaut). */
|
||||
countryOptions: RefOption[]
|
||||
removable?: boolean
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: AddressFormDraft]
|
||||
'remove': []
|
||||
/** Emis une fois quand le service d'autocompletion bascule en indisponible. */
|
||||
'degraded': []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const autocomplete = useAddressAutocomplete()
|
||||
|
||||
const model = computed(() => props.modelValue)
|
||||
|
||||
// Mode degrade : service BAN indisponible → Ville/Adresse en saisie libre.
|
||||
const degraded = ref(false)
|
||||
const cityOptions = ref<RefOption[]>([])
|
||||
const addressOptions = ref<RefOption[]>([])
|
||||
const addressLoading = ref(false)
|
||||
// Conserve les suggestions d'adresse pour retrouver ville/CP au moment du select.
|
||||
let lastAddressSuggestions: AddressSuggestion[] = []
|
||||
|
||||
/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */
|
||||
function update<K extends keyof AddressFormDraft>(field: K, value: AddressFormDraft[K]): void {
|
||||
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
||||
}
|
||||
|
||||
/** Applique l'exclusivite Prospect / (Livraison|Facturation) au changement. */
|
||||
function toggleFlag(field: keyof AddressFlagsDraft, value: boolean): void {
|
||||
const flags = applyProspectExclusivity(
|
||||
{ isProspect: model.value.isProspect, isDelivery: model.value.isDelivery, isBilling: model.value.isBilling },
|
||||
field,
|
||||
value,
|
||||
)
|
||||
emit('update:modelValue', { ...props.modelValue, ...flags })
|
||||
}
|
||||
|
||||
/** Bascule définitivement en mode degrade et previent le parent (toast unique). */
|
||||
function enterDegraded(): void {
|
||||
if (!degraded.value) {
|
||||
degraded.value = true
|
||||
emit('degraded')
|
||||
}
|
||||
}
|
||||
|
||||
/** Saisie du code postal → met a jour le champ + interroge la BAN pour la ville. */
|
||||
async function onPostalCodeChange(value: string): Promise<void> {
|
||||
update('postalCode', value)
|
||||
|
||||
if (degraded.value) {
|
||||
return
|
||||
}
|
||||
const digits = (value ?? '').replace(/\D/g, '')
|
||||
if (digits.length < 5) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const suggestions = await autocomplete.searchCity(digits)
|
||||
cityOptions.value = suggestions.map(s => ({ value: s.city, label: s.city }))
|
||||
}
|
||||
catch {
|
||||
enterDegraded()
|
||||
}
|
||||
}
|
||||
|
||||
/** Recherche d'adresse assistee (event de MalioInputAutocomplete). */
|
||||
async function onAddressSearch(query: string): Promise<void> {
|
||||
if (degraded.value) {
|
||||
return
|
||||
}
|
||||
addressLoading.value = true
|
||||
try {
|
||||
const postalCode = (model.value.postalCode ?? '').replace(/\D/g, '') || undefined
|
||||
const suggestions = await autocomplete.searchAddress(query, postalCode)
|
||||
lastAddressSuggestions = suggestions
|
||||
addressOptions.value = suggestions.map(s => ({ value: s.street, label: s.label }))
|
||||
}
|
||||
catch {
|
||||
enterDegraded()
|
||||
}
|
||||
finally {
|
||||
addressLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection d'une suggestion d'adresse → remplit rue + ville + CP.
|
||||
* Le type d'option suit le contrat MalioInputAutocomplete ({ label, value }).
|
||||
*/
|
||||
function onAddressSelect(option: { label: string, value: string | number } | null): void {
|
||||
if (option === null) {
|
||||
return
|
||||
}
|
||||
const suggestion = lastAddressSuggestions.find(s => s.street === option.value)
|
||||
if (!suggestion) {
|
||||
update('street', String(option.value))
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
street: suggestion.street,
|
||||
city: suggestion.city,
|
||||
postalCode: suggestion.postalCode,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="rounded-md border border-neutral-200 bg-white p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-bold">{{ title }}</h3>
|
||||
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee
|
||||
si non supprimable (1er bloc obligatoire RG-1.14) ou en lecture seule. -->
|
||||
<!-- ariaLabel passe via v-bind objet : la prop du composant est
|
||||
camelCase (aria-* serait traite en attribut HTML, jamais en prop). -->
|
||||
<MalioButtonIcon
|
||||
v-if="removable && !readonly"
|
||||
icon="mdi:trash-can-outline"
|
||||
variant="ghost"
|
||||
v-bind="{ ariaLabel: t('commercial.clients.form.contact.remove') }"
|
||||
@click="$emit('remove')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<MalioInputText
|
||||
:model-value="model.lastName"
|
||||
:label="t('commercial.clients.form.contact.lastName')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('lastName', v)"
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="model.firstName"
|
||||
:label="t('commercial.clients.form.contact.firstName')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('firstName', v)"
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="model.jobTitle"
|
||||
:label="t('commercial.clients.form.contact.jobTitle')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||
/>
|
||||
<MalioInputEmail
|
||||
:model-value="model.email"
|
||||
:label="t('commercial.clients.form.contact.email')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('email', v)"
|
||||
/>
|
||||
<MalioInputPhone
|
||||
:model-value="model.phonePrimary"
|
||||
:label="t('commercial.clients.form.contact.phonePrimary')"
|
||||
:mask="PHONE_MASK"
|
||||
:readonly="readonly"
|
||||
:addable="!model.hasSecondaryPhone && !readonly"
|
||||
:add-button-label="t('commercial.clients.form.contact.addPhone')"
|
||||
@update:model-value="(v: string) => update('phonePrimary', v)"
|
||||
@add="revealSecondaryPhone"
|
||||
/>
|
||||
<MalioInputPhone
|
||||
v-if="model.hasSecondaryPhone"
|
||||
:model-value="model.phoneSecondary"
|
||||
:label="t('commercial.clients.form.contact.phoneSecondary')"
|
||||
:mask="PHONE_MASK"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ContactFormDraft } from '~/modules/commercial/types/clientForm'
|
||||
|
||||
// Masque telephone FR : 5 groupes de 2 chiffres (la normalisation finale reste
|
||||
// serveur, cf. formatPhoneFR re-applique a la valeur renvoyee).
|
||||
const PHONE_MASK = '## ## ## ## ##'
|
||||
|
||||
const props = defineProps<{
|
||||
/** Brouillon du contact (v-model). */
|
||||
modelValue: ContactFormDraft
|
||||
/** Titre du bloc (ex: « Contact 1 »). */
|
||||
title: string
|
||||
/** Affiche l'icone de suppression (1er bloc non supprimable, RG-1.14). */
|
||||
removable?: boolean
|
||||
/** Bloc en lecture seule (onglet valide). */
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ContactFormDraft]
|
||||
'remove': []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Alias local pour la lisibilite du template.
|
||||
const model = computed(() => props.modelValue)
|
||||
|
||||
/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */
|
||||
function update<K extends keyof ContactFormDraft>(field: K, value: ContactFormDraft[K]): void {
|
||||
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
||||
}
|
||||
|
||||
/** Revele le 2e numero (RG-1.02/1.20 : max 1 secondaire, le « + » disparait). */
|
||||
function revealSecondaryPhone(): void {
|
||||
emit('update:modelValue', { ...props.modelValue, hasSecondaryPhone: true })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<!--
|
||||
Placeholder des onglets non encore implementes (Transport, Statistiques,
|
||||
Rapports, Echanges). Frame vide blanche : aucun champ, aucun bouton,
|
||||
aucun message « En cours » (decision Tristan 28/05). L'orchestrateur passe
|
||||
automatiquement a l'onglet suivant — ce composant n'est qu'une coquille
|
||||
visuelle reutilisee par 1.11/1.12.
|
||||
-->
|
||||
<div class="min-h-[240px] rounded-md bg-white" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Composant purement presentationnel : aucune prop, aucun event.
|
||||
</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Charge les referentiels (listes courtes) alimentant les selects de l'ecran
|
||||
* « Ajouter un client » : categories, sites, modes de TVA, delais et types de
|
||||
* reglement, banques, et les listes distributeurs / courtiers.
|
||||
*
|
||||
* Toutes les collections sont recuperees en entier via l'echappatoire prevue
|
||||
* `?pagination=false` (referentiels de quelques dizaines d'entrees max), avec
|
||||
* l'en-tete `Accept: application/ld+json` impose par API Platform 4 pour obtenir
|
||||
* l'enveloppe Hydra (`member`). Les valeurs d'option sont les IRI Hydra (`@id`)
|
||||
* pour pouvoir etre renvoyees telles quelles dans les payloads POST/PATCH
|
||||
* (relations ManyToOne / ManyToMany).
|
||||
*
|
||||
* Etat 100 % local a l'instance (refs) — aucune persistance URL.
|
||||
*/
|
||||
|
||||
/** Option generique au format attendu par MalioSelect / MalioSelectCheckbox ({ label, value }). */
|
||||
export interface RefOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
/** Option de type de reglement enrichie de son code stable (RG-1.12 / RG-1.13). */
|
||||
export interface PaymentTypeOption extends RefOption {
|
||||
code: string
|
||||
}
|
||||
|
||||
/** Option de categorie enrichie de son code stable (filtrage RG-1.29 cote adresse). */
|
||||
export interface CategoryOption extends RefOption {
|
||||
code: string
|
||||
}
|
||||
|
||||
/** Option de client (distributeur / courtier) — value = IRI du client lie. */
|
||||
export type ClientOption = RefOption
|
||||
|
||||
interface HydraMember {
|
||||
'@id': string
|
||||
}
|
||||
|
||||
interface CategoryMember extends HydraMember {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface SiteMember extends HydraMember {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface ReferentialMember extends HydraMember {
|
||||
code: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface ClientMember extends HydraMember {
|
||||
companyName: string
|
||||
}
|
||||
|
||||
const LD_JSON_HEADERS = { Accept: 'application/ld+json' }
|
||||
|
||||
export function useClientReferentials() {
|
||||
const api = useApi()
|
||||
|
||||
const categories = ref<CategoryOption[]>([])
|
||||
const sites = ref<RefOption[]>([])
|
||||
const tvaModes = ref<RefOption[]>([])
|
||||
const paymentDelays = ref<RefOption[]>([])
|
||||
const paymentTypes = ref<PaymentTypeOption[]>([])
|
||||
const banks = ref<RefOption[]>([])
|
||||
const distributors = ref<ClientOption[]>([])
|
||||
const brokers = ref<ClientOption[]>([])
|
||||
|
||||
/** Recupere une collection complete (pagination desactivee) en Hydra. */
|
||||
async function fetchAll<T extends HydraMember>(
|
||||
url: string,
|
||||
query: Record<string, string | string[]> = {},
|
||||
): Promise<T[]> {
|
||||
const res = await api.get<{ member?: T[] }>(
|
||||
url,
|
||||
{ pagination: 'false', ...query },
|
||||
{ headers: LD_JSON_HEADERS, toast: false },
|
||||
)
|
||||
return res.member ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge en parallele les referentiels communs (hors distributeurs/courtiers,
|
||||
* charges a la demande selon la relation choisie). Les selects compta ne sont
|
||||
* pertinents que si l'utilisateur a acces a l'onglet, mais le cout est
|
||||
* negligeable et simplifie l'orchestration.
|
||||
*/
|
||||
async function loadCommon(): Promise<void> {
|
||||
const [cats, sitesList, tva, delays, types, banksList] = await Promise.all([
|
||||
fetchAll<CategoryMember>('/categories'),
|
||||
fetchAll<SiteMember>('/sites'),
|
||||
fetchAll<ReferentialMember>('/tva_modes'),
|
||||
fetchAll<ReferentialMember>('/payment_delays'),
|
||||
fetchAll<ReferentialMember>('/payment_types'),
|
||||
fetchAll<ReferentialMember>('/banks'),
|
||||
])
|
||||
|
||||
categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code }))
|
||||
sites.value = sitesList.map(s => ({ value: s['@id'], label: s.name }))
|
||||
tvaModes.value = tva.map(t => ({ value: t['@id'], label: t.label }))
|
||||
paymentDelays.value = delays.map(d => ({ value: d['@id'], label: d.label }))
|
||||
paymentTypes.value = types.map(t => ({ value: t['@id'], label: t.label, code: t.code }))
|
||||
banks.value = banksList.map(b => ({ value: b['@id'], label: b.label }))
|
||||
}
|
||||
|
||||
/** Liste des clients pouvant etre choisis comme distributeur (code DISTRIBUTEUR). */
|
||||
async function loadDistributors(): Promise<void> {
|
||||
if (distributors.value.length > 0) {
|
||||
return
|
||||
}
|
||||
const clients = await fetchAll<ClientMember>('/clients', { categoryCode: 'DISTRIBUTEUR' })
|
||||
distributors.value = clients.map(c => ({ value: c['@id'], label: c.companyName }))
|
||||
}
|
||||
|
||||
/** Liste des clients pouvant etre choisis comme courtier (code COURTIER). */
|
||||
async function loadBrokers(): Promise<void> {
|
||||
if (brokers.value.length > 0) {
|
||||
return
|
||||
}
|
||||
const clients = await fetchAll<ClientMember>('/clients', { categoryCode: 'COURTIER' })
|
||||
brokers.value = clients.map(c => ({ value: c['@id'], label: c.companyName }))
|
||||
}
|
||||
|
||||
return {
|
||||
categories,
|
||||
sites,
|
||||
tvaModes,
|
||||
paymentDelays,
|
||||
paymentTypes,
|
||||
banks,
|
||||
distributors,
|
||||
brokers,
|
||||
loadCommon,
|
||||
loadDistributors,
|
||||
loadBrokers,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user