Compare commits
5 Commits
v0.1.126
...
2be9cd05d4
| Author | SHA1 | Date | |
|---|---|---|---|
| 2be9cd05d4 | |||
| f61e189441 | |||
| 9d9f9861b1 | |||
| 39071cbec0 | |||
| b82acdac01 |
@@ -78,6 +78,23 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
// Section "Transport" (M4, ERP-153) : pole logistique, porte le repertoire
|
||||||
|
// transporteurs. L'item est gate par `transport.carriers.view` ; la section
|
||||||
|
// disparait automatiquement (SidebarProvider) si le module `transport` est
|
||||||
|
// desactive ou si l'user n'a pas la permission (Compta / Usine).
|
||||||
|
[
|
||||||
|
'label' => 'sidebar.transport.section',
|
||||||
|
'icon' => 'mdi:truck-outline',
|
||||||
|
'items' => [
|
||||||
|
[
|
||||||
|
'label' => 'sidebar.transport.carriers',
|
||||||
|
'to' => '/carriers',
|
||||||
|
'icon' => 'mdi:truck-outline',
|
||||||
|
'module' => 'transport',
|
||||||
|
'permission' => 'transport.carriers.view',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
// Section "Administration" : regroupe toutes les pages de configuration
|
// Section "Administration" : regroupe toutes les pages de configuration
|
||||||
// applicative (RBAC, users, sites, audit log).
|
// applicative (RBAC, users, sites, audit log).
|
||||||
//
|
//
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.126'
|
app.version: '0.1.128'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
"save": "Enregistrer",
|
"save": "Enregistrer",
|
||||||
|
"validate": "Valider",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
@@ -34,6 +35,10 @@
|
|||||||
"section": "Technique",
|
"section": "Technique",
|
||||||
"providers": "Répertoire prestataires"
|
"providers": "Répertoire prestataires"
|
||||||
},
|
},
|
||||||
|
"transport": {
|
||||||
|
"section": "Transport",
|
||||||
|
"carriers": "Répertoire transporteurs"
|
||||||
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"roles": "Gestion des rôles",
|
"roles": "Gestion des rôles",
|
||||||
"users": "Utilisateurs",
|
"users": "Utilisateurs",
|
||||||
@@ -70,7 +75,7 @@
|
|||||||
"categories": "Catégories",
|
"categories": "Catégories",
|
||||||
"sites": "Sites",
|
"sites": "Sites",
|
||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"includeArchived": "Inclure les archivés",
|
"archivedOnly": "Voir les archivés",
|
||||||
"apply": "Voir les résultats",
|
"apply": "Voir les résultats",
|
||||||
"reset": "Réinitialiser"
|
"reset": "Réinitialiser"
|
||||||
},
|
},
|
||||||
@@ -119,7 +124,7 @@
|
|||||||
"back": "Retour au répertoire",
|
"back": "Retour au répertoire",
|
||||||
"loading": "Chargement du fournisseur…",
|
"loading": "Chargement du fournisseur…",
|
||||||
"notFound": "Fournisseur introuvable.",
|
"notFound": "Fournisseur introuvable.",
|
||||||
"save": "Valider"
|
"save": "Enregistrer"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"title": "Ajouter un fournisseur",
|
"title": "Ajouter un fournisseur",
|
||||||
@@ -262,7 +267,7 @@
|
|||||||
"back": "Retour au répertoire",
|
"back": "Retour au répertoire",
|
||||||
"loading": "Chargement du client…",
|
"loading": "Chargement du client…",
|
||||||
"notFound": "Client introuvable.",
|
"notFound": "Client introuvable.",
|
||||||
"save": "Valider"
|
"save": "Enregistrer"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"informationRequiredForCommercial": "Les informations de l'entreprise sont obligatoires pour le rôle Commerciale.",
|
"informationRequiredForCommercial": "Les informations de l'entreprise sont obligatoires pour le rôle Commerciale.",
|
||||||
@@ -384,7 +389,7 @@
|
|||||||
"categories": "Catégories",
|
"categories": "Catégories",
|
||||||
"sites": "Sites",
|
"sites": "Sites",
|
||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"includeArchived": "Inclure les archivés",
|
"archivedOnly": "Voir les archivés",
|
||||||
"apply": "Voir les résultats",
|
"apply": "Voir les résultats",
|
||||||
"reset": "Réinitialiser"
|
"reset": "Réinitialiser"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
/>
|
/>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
v-if="canShowSave"
|
v-if="canShowSave"
|
||||||
:label="t('common.save')"
|
:label="isCreateMode ? t('common.validate') : t('common.save')"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
button-class="w-m-btn-action"
|
button-class="w-m-btn-action"
|
||||||
:disabled="form.submitting.value || loadingTypes"
|
:disabled="form.submitting.value || loadingTypes"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe('useSuppliersRepository', () => {
|
|||||||
search: 'acme',
|
search: 'acme',
|
||||||
'categoryCode[]': ['NEGOCIANT', 'TRANSPORTEUR'],
|
'categoryCode[]': ['NEGOCIANT', 'TRANSPORTEUR'],
|
||||||
'siteId[]': ['86', '17'],
|
'siteId[]': ['86', '17'],
|
||||||
includeArchived: true,
|
archivedOnly: true,
|
||||||
},
|
},
|
||||||
{ replace: true },
|
{ replace: true },
|
||||||
)
|
)
|
||||||
@@ -63,7 +63,7 @@ describe('useSuppliersRepository', () => {
|
|||||||
search: 'acme',
|
search: 'acme',
|
||||||
'categoryCode[]': ['NEGOCIANT', 'TRANSPORTEUR'],
|
'categoryCode[]': ['NEGOCIANT', 'TRANSPORTEUR'],
|
||||||
'siteId[]': ['86', '17'],
|
'siteId[]': ['86', '17'],
|
||||||
includeArchived: true,
|
archivedOnly: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
itemsPerPage: 10,
|
itemsPerPage: 10,
|
||||||
},
|
},
|
||||||
@@ -73,7 +73,7 @@ describe('useSuppliersRepository', () => {
|
|||||||
|
|
||||||
it('repasse a une query propre apres reinitialisation des filtres', async () => {
|
it('repasse a une query propre apres reinitialisation des filtres', async () => {
|
||||||
const repo = useSuppliersRepository()
|
const repo = useSuppliersRepository()
|
||||||
await repo.setFilters({ search: 'acme', includeArchived: true }, { replace: true })
|
await repo.setFilters({ search: 'acme', archivedOnly: true }, { replace: true })
|
||||||
await repo.setFilters({}, { replace: true })
|
await repo.setFilters({}, { replace: true })
|
||||||
|
|
||||||
expect(mockGet).toHaveBeenLastCalledWith(
|
expect(mockGet).toHaveBeenLastCalledWith(
|
||||||
|
|||||||
@@ -172,16 +172,16 @@ describe('Répertoire fournisseurs (page /suppliers)', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('repercute le filtre « Inclure les archivés » dans setFilters sans toucher l\'URL', async () => {
|
it('repercute le filtre « Voir les archivés » dans setFilters sans toucher l\'URL', async () => {
|
||||||
const wrapper = mountPage()
|
const wrapper = mountPage()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// Coche « Inclure les archivés » puis applique les filtres.
|
// Coche « Voir les archivés » puis applique les filtres.
|
||||||
await wrapper.find('input[data-id="filter-include-archived"]').setValue(true)
|
await wrapper.find('input[data-id="filter-archived-only"]').setValue(true)
|
||||||
await wrapper.find('[data-label="commercial.suppliers.filters.apply"]').trigger('click')
|
await wrapper.find('[data-label="commercial.suppliers.filters.apply"]').trigger('click')
|
||||||
|
|
||||||
expect(mockSetFilters).toHaveBeenLastCalledWith(
|
expect(mockSetFilters).toHaveBeenLastCalledWith(
|
||||||
{ includeArchived: true },
|
{ archivedOnly: true },
|
||||||
{ replace: true },
|
{ replace: true },
|
||||||
)
|
)
|
||||||
// Etat 100 % local (regle n°6) : aucune navigation/query string declenchee.
|
// Etat 100 % local (regle n°6) : aucune navigation/query string declenchee.
|
||||||
@@ -192,7 +192,7 @@ describe('Répertoire fournisseurs (page /suppliers)', () => {
|
|||||||
const wrapper = mountPage()
|
const wrapper = mountPage()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
await wrapper.find('input[data-id="filter-include-archived"]').setValue(true)
|
await wrapper.find('input[data-id="filter-archived-only"]').setValue(true)
|
||||||
await wrapper.find('[data-label="commercial.suppliers.filters.apply"]').trigger('click')
|
await wrapper.find('[data-label="commercial.suppliers.filters.apply"]').trigger('click')
|
||||||
|
|
||||||
// Le libelle du bouton Filtrer porte le compteur (1 filtre actif).
|
// Le libelle du bouton Filtrer porte le compteur (1 filtre actif).
|
||||||
|
|||||||
@@ -128,13 +128,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</MalioAccordionItem>
|
</MalioAccordionItem>
|
||||||
|
|
||||||
<!-- Statut : bool unique. Coche = inclut aussi les archives (sinon actifs seuls). -->
|
<!-- Statut : bool unique. Coche = archives uniquement, sinon actifs. -->
|
||||||
<MalioAccordionItem :title="t('commercial.suppliers.filters.status')" value="status">
|
<MalioAccordionItem :title="t('commercial.suppliers.filters.status')" value="status">
|
||||||
<MalioCheckbox
|
<MalioCheckbox
|
||||||
id="filter-include-archived"
|
id="filter-archived-only"
|
||||||
:label="t('commercial.suppliers.filters.includeArchived')"
|
:label="t('commercial.suppliers.filters.archivedOnly')"
|
||||||
:model-value="draftIncludeArchived"
|
:model-value="draftArchivedOnly"
|
||||||
@update:model-value="(val: boolean) => draftIncludeArchived = val"
|
@update:model-value="(val: boolean) => draftArchivedOnly = val"
|
||||||
/>
|
/>
|
||||||
</MalioAccordionItem>
|
</MalioAccordionItem>
|
||||||
</MalioAccordion>
|
</MalioAccordion>
|
||||||
@@ -254,12 +254,12 @@ const filterDrawerOpen = ref(false)
|
|||||||
const draftSearch = ref('')
|
const draftSearch = ref('')
|
||||||
const draftCategoryCodes = ref<string[]>([])
|
const draftCategoryCodes = ref<string[]>([])
|
||||||
const draftSiteIds = ref<string[]>([])
|
const draftSiteIds = ref<string[]>([])
|
||||||
const draftIncludeArchived = ref(false)
|
const draftArchivedOnly = ref(false)
|
||||||
|
|
||||||
const appliedSearch = ref('')
|
const appliedSearch = ref('')
|
||||||
const appliedCategoryCodes = ref<string[]>([])
|
const appliedCategoryCodes = ref<string[]>([])
|
||||||
const appliedSiteIds = ref<string[]>([])
|
const appliedSiteIds = ref<string[]>([])
|
||||||
const appliedIncludeArchived = ref(false)
|
const appliedArchivedOnly = ref(false)
|
||||||
|
|
||||||
// Options des selects multi, chargees une fois (referentiels courts).
|
// Options des selects multi, chargees une fois (referentiels courts).
|
||||||
const categoryOptions = ref<FilterOption[]>([])
|
const categoryOptions = ref<FilterOption[]>([])
|
||||||
@@ -270,7 +270,7 @@ const activeFilterCount = computed(() => {
|
|||||||
if (appliedSearch.value.trim() !== '') count++
|
if (appliedSearch.value.trim() !== '') count++
|
||||||
if (appliedCategoryCodes.value.length > 0) count++
|
if (appliedCategoryCodes.value.length > 0) count++
|
||||||
if (appliedSiteIds.value.length > 0) count++
|
if (appliedSiteIds.value.length > 0) count++
|
||||||
if (appliedIncludeArchived.value) count++
|
if (appliedArchivedOnly.value) count++
|
||||||
return count
|
return count
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ function openFilters(): void {
|
|||||||
draftSearch.value = appliedSearch.value
|
draftSearch.value = appliedSearch.value
|
||||||
draftCategoryCodes.value = [...appliedCategoryCodes.value]
|
draftCategoryCodes.value = [...appliedCategoryCodes.value]
|
||||||
draftSiteIds.value = [...appliedSiteIds.value]
|
draftSiteIds.value = [...appliedSiteIds.value]
|
||||||
draftIncludeArchived.value = appliedIncludeArchived.value
|
draftArchivedOnly.value = appliedArchivedOnly.value
|
||||||
filterDrawerOpen.value = true
|
filterDrawerOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ function buildFilterPayload(): Record<string, string | string[] | boolean> {
|
|||||||
if (appliedSearch.value.trim() !== '') payload.search = appliedSearch.value.trim()
|
if (appliedSearch.value.trim() !== '') payload.search = appliedSearch.value.trim()
|
||||||
if (appliedCategoryCodes.value.length > 0) payload['categoryCode[]'] = [...appliedCategoryCodes.value]
|
if (appliedCategoryCodes.value.length > 0) payload['categoryCode[]'] = [...appliedCategoryCodes.value]
|
||||||
if (appliedSiteIds.value.length > 0) payload['siteId[]'] = [...appliedSiteIds.value]
|
if (appliedSiteIds.value.length > 0) payload['siteId[]'] = [...appliedSiteIds.value]
|
||||||
if (appliedIncludeArchived.value) payload.includeArchived = true
|
if (appliedArchivedOnly.value) payload.archivedOnly = true
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ function applyFilters(): void {
|
|||||||
appliedSearch.value = draftSearch.value.trim()
|
appliedSearch.value = draftSearch.value.trim()
|
||||||
appliedCategoryCodes.value = [...draftCategoryCodes.value]
|
appliedCategoryCodes.value = [...draftCategoryCodes.value]
|
||||||
appliedSiteIds.value = [...draftSiteIds.value]
|
appliedSiteIds.value = [...draftSiteIds.value]
|
||||||
appliedIncludeArchived.value = draftIncludeArchived.value
|
appliedArchivedOnly.value = draftArchivedOnly.value
|
||||||
|
|
||||||
setFilters(buildFilterPayload(), { replace: true })
|
setFilters(buildFilterPayload(), { replace: true })
|
||||||
filterDrawerOpen.value = false
|
filterDrawerOpen.value = false
|
||||||
@@ -333,12 +333,12 @@ function resetFilters(): void {
|
|||||||
draftSearch.value = ''
|
draftSearch.value = ''
|
||||||
draftCategoryCodes.value = []
|
draftCategoryCodes.value = []
|
||||||
draftSiteIds.value = []
|
draftSiteIds.value = []
|
||||||
draftIncludeArchived.value = false
|
draftArchivedOnly.value = false
|
||||||
|
|
||||||
appliedSearch.value = ''
|
appliedSearch.value = ''
|
||||||
appliedCategoryCodes.value = []
|
appliedCategoryCodes.value = []
|
||||||
appliedSiteIds.value = []
|
appliedSiteIds.value = []
|
||||||
appliedIncludeArchived.value = false
|
appliedArchivedOnly.value = false
|
||||||
|
|
||||||
setFilters({}, { replace: true })
|
setFilters({}, { replace: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
@click="emit('update:modelValue', false)"
|
@click="emit('update:modelValue', false)"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
:label="t('common.save')"
|
:label="isEditMode ? t('common.save') : t('common.validate')"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
button-class="w-m-btn-action"
|
button-class="w-m-btn-action"
|
||||||
:disabled="saving || permissionsLoadFailed"
|
:disabled="saving || permissionsLoadFailed"
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
@click="emit('update:modelValue', false)"
|
@click="emit('update:modelValue', false)"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
:label="t('common.save')"
|
:label="isEditMode ? t('common.save') : t('common.validate')"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
button-class="w-m-btn-action"
|
button-class="w-m-btn-action"
|
||||||
:disabled="saving || !isValidHex"
|
:disabled="saving || !isValidHex"
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ vi.stubGlobal('useApi', () => ({ get: mockApiGet }))
|
|||||||
* - l'enveloppe Hydra (member / totalItems) est consommee
|
* - l'enveloppe Hydra (member / totalItems) est consommee
|
||||||
* - le header `Accept: application/ld+json` est envoye (sinon API Platform 4
|
* - le header `Accept: application/ld+json` est envoye (sinon API Platform 4
|
||||||
* renvoie un tableau plat sans pagination)
|
* renvoie un tableau plat sans pagination)
|
||||||
* - EXCLUSION DES ARCHIVES PAR DEFAUT : aucun `includeArchived` n'est envoye
|
* - EXCLUSION DES ARCHIVES PAR DEFAUT : aucun `archivedOnly` n'est envoye
|
||||||
* tant que l'utilisateur ne coche pas le filtre (le back masque alors les
|
* tant que l'utilisateur ne coche pas le filtre (le back masque alors les
|
||||||
* archives) ; le filtre `includeArchived` est bien transmis une fois applique.
|
* archives) ; le filtre `archivedOnly` est bien transmis une fois applique.
|
||||||
*/
|
*/
|
||||||
describe('useProvidersRepository', () => {
|
describe('useProvidersRepository', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -53,26 +53,26 @@ describe('useProvidersRepository', () => {
|
|||||||
expect(repo.totalItems.value).toBe(1)
|
expect(repo.totalItems.value).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('exclut les archives par defaut : aucun includeArchived au premier fetch', async () => {
|
it('exclut les archives par defaut : aucun archivedOnly au premier fetch', async () => {
|
||||||
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
|
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
|
||||||
const repo = useProvidersRepository()
|
const repo = useProvidersRepository()
|
||||||
|
|
||||||
await repo.fetch()
|
await repo.fetch()
|
||||||
|
|
||||||
const query = mockApiGet.mock.calls[0][1] as Record<string, unknown>
|
const query = mockApiGet.mock.calls[0][1] as Record<string, unknown>
|
||||||
expect(query.includeArchived).toBeUndefined()
|
expect(query.archivedOnly).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('transmet includeArchived une fois le filtre applique (retour page 1)', async () => {
|
it('transmet archivedOnly une fois le filtre applique (retour page 1)', async () => {
|
||||||
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
|
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
|
||||||
const repo = useProvidersRepository()
|
const repo = useProvidersRepository()
|
||||||
await repo.fetch()
|
await repo.fetch()
|
||||||
|
|
||||||
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
|
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
|
||||||
await repo.setFilters({ includeArchived: true })
|
await repo.setFilters({ archivedOnly: true })
|
||||||
|
|
||||||
expect(repo.currentPage.value).toBe(1)
|
expect(repo.currentPage.value).toBe(1)
|
||||||
const query = mockApiGet.mock.calls.at(-1)?.[1] as Record<string, unknown>
|
const query = mockApiGet.mock.calls.at(-1)?.[1] as Record<string, unknown>
|
||||||
expect(query.includeArchived).toBe(true)
|
expect(query.archivedOnly).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ export interface Provider {
|
|||||||
* sur la ressource `/providers` (pagination serveur obligatoire ; jamais de
|
* sur la ressource `/providers` (pagination serveur obligatoire ; jamais de
|
||||||
* chargement integral en memoire). Miroir de `useSuppliersRepository` (M2).
|
* chargement integral en memoire). Miroir de `useSuppliersRepository` (M2).
|
||||||
*
|
*
|
||||||
* Les filtres (recherche, categories, sites, inclusion des archives) sont pilotes
|
* Les filtres (recherche, categories, sites, archives) sont pilotes par la page
|
||||||
* par la page via `setFilters` du composable partage — la remise en page 1 est
|
* via `setFilters` du composable partage — la remise en page 1 est garantie. Par
|
||||||
* garantie. Par defaut, aucun `includeArchived` n'est envoye : le back masque
|
* defaut, aucun `archivedOnly` n'est envoye : le back masque donc les prestataires
|
||||||
* donc les prestataires archives (exclusion par defaut, spec-back § 2.11).
|
* archives (exclusion par defaut, spec-back § 2.11). Cocher « Voir les archivés »
|
||||||
|
* envoie `archivedOnly=true` → seules les archives sont listees (aligne sur Client).
|
||||||
*
|
*
|
||||||
* Le cloisonnement par site est applique AUTOMATIQUEMENT cote back (§ 2.13) en
|
* Le cloisonnement par site est applique AUTOMATIQUEMENT cote back (§ 2.13) en
|
||||||
* fonction de l'utilisateur — rien a filtrer cote front.
|
* fonction de l'utilisateur — rien a filtrer cote front.
|
||||||
|
|||||||
@@ -129,13 +129,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</MalioAccordionItem>
|
</MalioAccordionItem>
|
||||||
|
|
||||||
<!-- Statut : bool unique. Coche = inclut aussi les archives (sinon actifs seuls). -->
|
<!-- Statut : bool unique. Coche = archives uniquement, sinon actifs. -->
|
||||||
<MalioAccordionItem :title="t('technique.providers.filters.status')" value="status">
|
<MalioAccordionItem :title="t('technique.providers.filters.status')" value="status">
|
||||||
<MalioCheckbox
|
<MalioCheckbox
|
||||||
id="filter-include-archived"
|
id="filter-archived-only"
|
||||||
:label="t('technique.providers.filters.includeArchived')"
|
:label="t('technique.providers.filters.archivedOnly')"
|
||||||
:model-value="draftIncludeArchived"
|
:model-value="draftArchivedOnly"
|
||||||
@update:model-value="(val: boolean) => draftIncludeArchived = val"
|
@update:model-value="(val: boolean) => draftArchivedOnly = val"
|
||||||
/>
|
/>
|
||||||
</MalioAccordionItem>
|
</MalioAccordionItem>
|
||||||
</MalioAccordion>
|
</MalioAccordion>
|
||||||
@@ -258,12 +258,12 @@ const filterDrawerOpen = ref(false)
|
|||||||
const draftSearch = ref('')
|
const draftSearch = ref('')
|
||||||
const draftCategoryCodes = ref<string[]>([])
|
const draftCategoryCodes = ref<string[]>([])
|
||||||
const draftSiteIds = ref<string[]>([])
|
const draftSiteIds = ref<string[]>([])
|
||||||
const draftIncludeArchived = ref(false)
|
const draftArchivedOnly = ref(false)
|
||||||
|
|
||||||
const appliedSearch = ref('')
|
const appliedSearch = ref('')
|
||||||
const appliedCategoryCodes = ref<string[]>([])
|
const appliedCategoryCodes = ref<string[]>([])
|
||||||
const appliedSiteIds = ref<string[]>([])
|
const appliedSiteIds = ref<string[]>([])
|
||||||
const appliedIncludeArchived = ref(false)
|
const appliedArchivedOnly = ref(false)
|
||||||
|
|
||||||
// Options des selects multi, chargees une fois (referentiels courts).
|
// Options des selects multi, chargees une fois (referentiels courts).
|
||||||
const categoryOptions = ref<FilterOption[]>([])
|
const categoryOptions = ref<FilterOption[]>([])
|
||||||
@@ -274,7 +274,7 @@ const activeFilterCount = computed(() => {
|
|||||||
if (appliedSearch.value.trim() !== '') count++
|
if (appliedSearch.value.trim() !== '') count++
|
||||||
if (appliedCategoryCodes.value.length > 0) count++
|
if (appliedCategoryCodes.value.length > 0) count++
|
||||||
if (appliedSiteIds.value.length > 0) count++
|
if (appliedSiteIds.value.length > 0) count++
|
||||||
if (appliedIncludeArchived.value) count++
|
if (appliedArchivedOnly.value) count++
|
||||||
return count
|
return count
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ function openFilters(): void {
|
|||||||
draftSearch.value = appliedSearch.value
|
draftSearch.value = appliedSearch.value
|
||||||
draftCategoryCodes.value = [...appliedCategoryCodes.value]
|
draftCategoryCodes.value = [...appliedCategoryCodes.value]
|
||||||
draftSiteIds.value = [...appliedSiteIds.value]
|
draftSiteIds.value = [...appliedSiteIds.value]
|
||||||
draftIncludeArchived.value = appliedIncludeArchived.value
|
draftArchivedOnly.value = appliedArchivedOnly.value
|
||||||
filterDrawerOpen.value = true
|
filterDrawerOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +315,7 @@ function buildFilterPayload(): Record<string, string | string[] | boolean> {
|
|||||||
if (appliedSearch.value.trim() !== '') payload.search = appliedSearch.value.trim()
|
if (appliedSearch.value.trim() !== '') payload.search = appliedSearch.value.trim()
|
||||||
if (appliedCategoryCodes.value.length > 0) payload['categoryCode[]'] = [...appliedCategoryCodes.value]
|
if (appliedCategoryCodes.value.length > 0) payload['categoryCode[]'] = [...appliedCategoryCodes.value]
|
||||||
if (appliedSiteIds.value.length > 0) payload['siteId[]'] = [...appliedSiteIds.value]
|
if (appliedSiteIds.value.length > 0) payload['siteId[]'] = [...appliedSiteIds.value]
|
||||||
if (appliedIncludeArchived.value) payload.includeArchived = true
|
if (appliedArchivedOnly.value) payload.archivedOnly = true
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ function applyFilters(): void {
|
|||||||
appliedSearch.value = draftSearch.value.trim()
|
appliedSearch.value = draftSearch.value.trim()
|
||||||
appliedCategoryCodes.value = [...draftCategoryCodes.value]
|
appliedCategoryCodes.value = [...draftCategoryCodes.value]
|
||||||
appliedSiteIds.value = [...draftSiteIds.value]
|
appliedSiteIds.value = [...draftSiteIds.value]
|
||||||
appliedIncludeArchived.value = draftIncludeArchived.value
|
appliedArchivedOnly.value = draftArchivedOnly.value
|
||||||
|
|
||||||
setFilters(buildFilterPayload(), { replace: true })
|
setFilters(buildFilterPayload(), { replace: true })
|
||||||
filterDrawerOpen.value = false
|
filterDrawerOpen.value = false
|
||||||
@@ -337,12 +337,12 @@ function resetFilters(): void {
|
|||||||
draftSearch.value = ''
|
draftSearch.value = ''
|
||||||
draftCategoryCodes.value = []
|
draftCategoryCodes.value = []
|
||||||
draftSiteIds.value = []
|
draftSiteIds.value = []
|
||||||
draftIncludeArchived.value = false
|
draftArchivedOnly.value = false
|
||||||
|
|
||||||
appliedSearch.value = ''
|
appliedSearch.value = ''
|
||||||
appliedCategoryCodes.value = []
|
appliedCategoryCodes.value = []
|
||||||
appliedSiteIds.value = []
|
appliedSiteIds.value = []
|
||||||
appliedIncludeArchived.value = false
|
appliedArchivedOnly.value = false
|
||||||
|
|
||||||
setFilters({}, { replace: true })
|
setFilters({}, { replace: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ export const personas: Record<PersonaKey, Persona> = {
|
|||||||
'technique.providers.accounting.view',
|
'technique.providers.accounting.view',
|
||||||
'technique.providers.accounting.manage',
|
'technique.providers.accounting.manage',
|
||||||
'technique.providers.archive',
|
'technique.providers.archive',
|
||||||
|
// Transport — Repertoire transporteurs (M4, ERP-153). Meme logique :
|
||||||
|
// mappe sur le persona "tout", pas de nouveau persona (regle ABSOLUE
|
||||||
|
// n°7). transport.carriers.view n'ajoute pas de lien dans la section
|
||||||
|
// Administration, donc expectedAdminLinks reste inchange.
|
||||||
|
'transport.carriers.view',
|
||||||
|
'transport.carriers.manage',
|
||||||
|
'transport.carriers.archive',
|
||||||
],
|
],
|
||||||
expectedAdminLinks: ['users', 'roles', 'sites', 'categories', 'audit-log'],
|
expectedAdminLinks: ['users', 'roles', 'sites', 'categories', 'audit-log'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ final class RbacSeeder
|
|||||||
* Definition unique des 4 roles + matrice § 2.7. La cle est le code du role,
|
* Definition unique des 4 roles + matrice § 2.7. La cle est le code du role,
|
||||||
* `label` le libelle FR affichable, `permissions` la liste des codes RBAC a
|
* `label` le libelle FR affichable, `permissions` la liste des codes RBAC a
|
||||||
* attacher (admin n'apparait pas car il bypass tout via isAdmin ;
|
* attacher (admin n'apparait pas car il bypass tout via isAdmin ;
|
||||||
* `commercial.clients.archive`, `commercial.suppliers.archive` et
|
* `commercial.clients.archive`, `commercial.suppliers.archive`,
|
||||||
* `technique.providers.archive` ne sont attaches a aucun role metier —
|
* `technique.providers.archive` et `transport.carriers.archive` ne sont
|
||||||
* admin seul).
|
* attaches a aucun role metier — admin seul).
|
||||||
*
|
*
|
||||||
* Cloisonnement par site des prestataires (M3 § 2.13) : la permission
|
* Cloisonnement par site des prestataires (M3 § 2.13) : la permission
|
||||||
* `sites.bypass_scope` est attribuee par defaut a Bureau / Compta /
|
* `sites.bypass_scope` est attribuee par defaut a Bureau / Compta /
|
||||||
@@ -77,6 +77,9 @@ final class RbacSeeder
|
|||||||
// Prestataires (M3 § 2.9, ERP-138) : view + manage (hors Comptabilite).
|
// Prestataires (M3 § 2.9, ERP-138) : view + manage (hors Comptabilite).
|
||||||
'technique.providers.view',
|
'technique.providers.view',
|
||||||
'technique.providers.manage',
|
'technique.providers.manage',
|
||||||
|
// Transporteurs (M4 § 5.2, ERP-153) : view + manage (PAS archive -> admin seul).
|
||||||
|
'transport.carriers.view',
|
||||||
|
'transport.carriers.manage',
|
||||||
// Visibilite multi-site des prestataires (M3 § 2.13) : voit tous les sites.
|
// Visibilite multi-site des prestataires (M3 § 2.13) : voit tous les sites.
|
||||||
'sites.bypass_scope',
|
'sites.bypass_scope',
|
||||||
// Lecture des referentiels transverses pour les selects client (ERP-102).
|
// Lecture des referentiels transverses pour les selects client (ERP-102).
|
||||||
@@ -120,6 +123,9 @@ final class RbacSeeder
|
|||||||
// (onglet Comptabilite masque/filtre pour la Commerciale).
|
// (onglet Comptabilite masque/filtre pour la Commerciale).
|
||||||
'technique.providers.view',
|
'technique.providers.view',
|
||||||
'technique.providers.manage',
|
'technique.providers.manage',
|
||||||
|
// Transporteurs (M4 § 5.2, ERP-153) : view seul (consultation « Tout »,
|
||||||
|
// ni manage ni archive pour la Commerciale).
|
||||||
|
'transport.carriers.view',
|
||||||
// Visibilite multi-site des prestataires (M3 § 2.13) : voit tous les sites.
|
// Visibilite multi-site des prestataires (M3 § 2.13) : voit tous les sites.
|
||||||
'sites.bypass_scope',
|
'sites.bypass_scope',
|
||||||
// Lecture des referentiels transverses pour les selects client (ERP-102).
|
// Lecture des referentiels transverses pour les selects client (ERP-102).
|
||||||
|
|||||||
@@ -212,6 +212,11 @@ final class SeedE2ECommand extends Command
|
|||||||
'technique.providers.accounting.view',
|
'technique.providers.accounting.view',
|
||||||
'technique.providers.accounting.manage',
|
'technique.providers.accounting.manage',
|
||||||
'technique.providers.archive',
|
'technique.providers.archive',
|
||||||
|
// Transport — Repertoire transporteurs (M4, ERP-153). Meme
|
||||||
|
// logique : mappe sur le persona "tout". Miroir de personas.ts.
|
||||||
|
'transport.carriers.view',
|
||||||
|
'transport.carriers.manage',
|
||||||
|
'transport.carriers.archive',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -13,17 +13,22 @@ final class TransportModule
|
|||||||
/**
|
/**
|
||||||
* Liste declarative des permissions RBAC exposees par le module Transport.
|
* Liste declarative des permissions RBAC exposees par le module Transport.
|
||||||
*
|
*
|
||||||
* Vide a ce stade : le module ne porte que des referentiels externes
|
* Socle du repertoire transporteurs (M4 § 5.1, ERP-153) :
|
||||||
* synchronises par commandes console (codes IDTF - ERP-149, transporteurs
|
* - `view` : consultation de la liste / fiche transporteur ;
|
||||||
* QUALIMAT - ERP-39), sans ecran ni action protegee. Les permissions seront
|
* - `manage` : creation / modification (hors archivage) ;
|
||||||
* ajoutees quand une page de consultation sera exposee.
|
* - `archive` : archivage / restauration (admin seul, cf. matrice § 5.2).
|
||||||
*
|
*
|
||||||
* Consommee par `app:sync-permissions` (un tableau vide est valide).
|
* Consommee par `app:sync-permissions`. Matrice role -> permissions dans
|
||||||
|
* `RbacSeeder::MATRIX` (§ 5.2).
|
||||||
*
|
*
|
||||||
* @return array<int, array{code: string, label: string}>
|
* @return array<int, array{code: string, label: string}>
|
||||||
*/
|
*/
|
||||||
public static function permissions(): array
|
public static function permissions(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [
|
||||||
|
['code' => 'transport.carriers.view', 'label' => 'Voir les transporteurs'],
|
||||||
|
['code' => 'transport.carriers.manage', 'label' => 'Créer / modifier les transporteurs'],
|
||||||
|
['code' => 'transport.carriers.archive', 'label' => 'Archiver / restaurer un transporteur'],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Module\Transport;
|
||||||
|
|
||||||
|
use App\Module\Transport\TransportModule;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests structurels du module Transport (M4) : identite et contrat
|
||||||
|
* `permissions()` (socle RBAC, ERP-153).
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class TransportModuleTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testModuleIdentity(): void
|
||||||
|
{
|
||||||
|
self::assertSame('transport', TransportModule::ID);
|
||||||
|
self::assertSame('Transport', TransportModule::LABEL);
|
||||||
|
self::assertFalse(TransportModule::REQUIRED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPermissionsSetContainsExactlyThreeCodes(): void
|
||||||
|
{
|
||||||
|
// Garde-fou : le jeu de permissions du module est fige par ce test. Si
|
||||||
|
// quelqu'un ajoute / retire une permission sans ajuster la spec (§ 5.1)
|
||||||
|
// ni la matrice RBAC (§ 5.2), le test casse explicitement.
|
||||||
|
$codes = array_column(TransportModule::permissions(), 'code');
|
||||||
|
sort($codes);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'transport.carriers.archive',
|
||||||
|
'transport.carriers.manage',
|
||||||
|
'transport.carriers.view',
|
||||||
|
],
|
||||||
|
$codes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEveryPermissionCodeIsPrefixedByModuleId(): void
|
||||||
|
{
|
||||||
|
// Convention de nommage `module.resource[.sub].action` : le prefixe doit
|
||||||
|
// correspondre exactement a l'ID du module (verifie aussi par
|
||||||
|
// app:sync-permissions).
|
||||||
|
foreach (TransportModule::permissions() as $permission) {
|
||||||
|
self::assertStringStartsWith(
|
||||||
|
TransportModule::ID.'.',
|
||||||
|
$permission['code'],
|
||||||
|
'Chaque code de permission doit etre prefixe par l\'ID du module.',
|
||||||
|
);
|
||||||
|
self::assertNotSame('', $permission['label'], 'Chaque permission doit porter un label.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user