Compare commits

..

12 Commits

Author SHA1 Message Date
Matthieu 29e4dbb67b test(transport) : rigueur RG sous-ressources (propertyPath, 404 parent, 401, certif)
Repond aux retours de review (rigueur d'assertion transversale) :
- mutualise assertViolationOnPath dans AbstractCarrierApiTestCase (au lieu d'un
  duplicata local a CarrierWriteApiTest) ;
- asserte le propertyPath des 422 des sous-ressources (adresses city/street/postalCode,
  contacts firstName/phones/email, prix clientDeliveryAddress/supplierSupplyAddress/price)
  -> evite les faux-verts du mapping inline (ERP-101) ;
- 404 parent (POST sur /carriers/999999/{addresses,contacts,prices}) ;
- 401 anonyme + filtre ?certificationType= sur la collection (trous releves sur le
  contrat de lecture).
2026-06-16 15:04:47 +02:00
Matthieu a392466ecb test(transport) : couverture RG-4.01→4.14 + contrat + fixtures (ERP-163)
- CarrierListTest : anti-N+1 liste (fetch-join qualimat), tri name ASC,
  echappatoire ?pagination=false (regle n°13)
- CarrierAuditTest : POST/PATCH/archive -> audit_log entity_type='transport.Carrier'
- CarrierAddressApiTest : CP/ville incoherents acceptes (RG-4.06, pas de
  controle de coherence serveur)
- CarrierFixtures : fixtures dev completes et idempotentes (QUALIMAT validite
  passee, AUTRE+decharge, affrete, LIOT, complet prix CLIENT+FOURNISSEUR,
  archive) ; env-gated dev uniquement
- spec-back § 4.0.bis : JSON reel capture (liste + detail) via CarrierSerializationContractTest
2026-06-16 15:04:36 +02:00
Matthieu 15c6207c78 feat(transport) : filtre archivedOnly sur l'export repertoire (coherence liste)
L'export XLSX du repertoire reflete la vue liste : il propage desormais
?archivedOnly comme CarrierProvider (sinon l'export divergerait de l'ecran
quand le toggle « Voir les archives » est actif).
2026-06-16 15:04:36 +02:00
Matthieu 75d67df79b feat(transport) : export XLSX répertoire + prix transporteur (ERP-162)
GET /api/carriers/export.xlsx (mêmes filtres que la liste : includeArchived,
search, certificationType) et GET /api/carriers/{id}/prices/export.xlsx (tableau
Prix regroupé Benne / Fond Mouvant). Controllers Symfony custom avec
#[Route(priority: 1)] pour éviter le conflit API Platform {id}, génération
déléguée au service Shared SpreadsheetExporterInterface.
2026-06-16 15:04:36 +02:00
Matthieu 71a4c9d903 feat(transport) : sous-ressource prix transporteur (ERP-161)
POST /api/carriers/{id}/prices + PATCH/DELETE /api/carrier_prices/{id}
(security transport.carriers.manage) via CarrierPriceProcessor.

RG-4.09->4.11 : coherence de branche CLIENT/FOURNISSEUR (champs requis +
appartenance de l'adresse de livraison au client / de l'adresse d'appro au
fournisseur, sinon 422), nettoyage de la branche opposee (CHECK BDD). Champs
communs obligatoires via Assert\NotBlank + Assert\Choice.

Les contrats Shared ClientAddressInterface / SupplierAddressInterface exposent
desormais getClient() / getSupplier() (canal cross-module, regle n°1) pour la
verification d'appartenance. Colonnes enum du prix whitelistees dans le miroir
Assert\Length (deja bornees par Choice).
2026-06-16 15:04:35 +02:00
Matthieu c576e4d504 feat(transport) : sous-ressource contacts transporteur (ERP-160) 2026-06-16 15:04:35 +02:00
Matthieu 91dc02ab05 feat(transport) : sous-ressource adresses transporteur (ERP-159)
POST /api/carriers/{id}/addresses + PATCH/DELETE /api/carrier_addresses/{id}
(security transport.carriers.manage), spec-back § 4.5. Jumelle de SupplierAddress
(M2) / ProviderAddress (M3), sans address_type ni M2M.

- CarrierAddress : ajout #[ApiResource] (Get/Post/Patch/Delete) + groupe
  d'ecriture carrier:write:addresses + contraintes FR. RG-4.06 : code postal
  ^[0-9]{4,5}$ (Assert\Regex). Mapping ORM/colonnes inchange.
- CarrierAddressProcessor : rattachement parent (404 si absent) + RG-4.05
  (transporteur affrete -> Pays/CP/Ville/Adresse obligatoires, 422 par champ).
  RG-4.05 portee par le processor car le parent est indisponible a la validation
  Symfony sur un POST sous-ressource read:false. RG-4.07 = front (PATCH accepte).
- EXCLUDED_LENGTH_MIRROR : CarrierAddress::postalCode (Regex borne la longueur).
- Tests : CP invalide 422, affrete incomplet 422, affrete complet 201,
  PATCH/DELETE OK (manage), 403 sans manage.
2026-06-16 15:04:35 +02:00
Matthieu df3b924b17 feat(transport) : endpoint recherche QualimatCarrier (ERP-156)
GET /api/qualimat_carriers?search= pour la saisie assistee du nom (RG-4.01,
spec-back § 4.7) : recherche fuzzy sur name (+ siret), restreinte aux lignes
actives (is_active = true), triee name ASC, paginee (regle n°13).

- QualimatCarrierRepositoryInterface + DoctrineQualimatCarrierRepository :
  QueryBuilder de recherche (forcage is_active cote serveur, fuzzy multi-champs).
- QualimatCarrierSearchProvider : provider de la GetCollection (pagination Hydra
  + echappatoire ?pagination=false), branche uniquement sur la collection.
- ApiResource : provider custom sur GetCollection, retrait des ApiFilter natifs
  (incapables d'unifier name/siret sous ?search= ni d'imposer l'actif). Mapping
  ORM inchange (schema:update reste no-op). Aucune ecriture exposee.
- Tests : actifs seuls, tri name, match siret, pagination Hydra, 403 sans perm.
2026-06-16 15:04:35 +02:00
Matthieu cbabe8f9ac feat(transport) : CarrierProcessor + champs conditionnels formulaire principal (ERP-158)
Ecriture du formulaire principal transporteur (M4, WT4) : POST/PATCH via
CarrierProcessor + CarrierFieldNormalizer, contraintes conditionnelles sur
l'entite Carrier.

- RG-4.01 : POST qualimatCarrier -> certificationType=QUALIMAT + FK persistee ;
  cas LIOT (name=LIOT) -> certification non requise, liotPlates accepte.
- RG-4.02 : certificationType=AUTRE sans dischargeDocument -> 422 (Assert\Callback).
- RG-4.03 : isChartered=true sans indexationRate/containerType/volumeM3 -> 422.
- RG-4.12 : doublon de nom (parmi actifs) -> 409 (index partiel uq_carrier_name_active).
- RG-4.13 : normalisation serveur (name UPPER, liotPlates ;-split/trim/UPPER) +
  methodes personne/telephone/email pour les sous-ressources Contact (WT7).
- RG-4.14 : PATCH isArchived exige transport.carriers.archive (Admin seul),
  mode strict -> 403 + 422 si autre champ ; restauration en conflit -> 409.

Operations Post/Patch ajoutees a l'ApiResource (lecture posee au WT3 conservee).
RG conditionnelles portees par validateMainFormConsistency (Assert\Callback +
->atPath()) pour un propertyPath mappable inline (useFormErrors, ERP-101).

certificationType / containerType whitelistes dans EXCLUDED_LENGTH_MIRROR (Choice
borne deja les valeurs, miroir SupplierAddress::addressType).

Tests : CarrierWriteApiTest (RG-4.01->4.03/4.12->4.14), CarrierRBACMatrixTest
(matrice bureau/compta/commerciale/usine), CarrierArchiveTest (409 restauration),
CarrierFieldNormalizerTest (RG-4.13). make test vert (750).
2026-06-16 15:04:35 +02:00
Matthieu 0f62deb80f feat(transport) : filtre archivedOnly sur le repertoire (coherence M1/M2/M3)
Aligne CarrierProvider/DoctrineCarrierRepository sur Client/Supplier/Provider :
?archivedOnly=true n'expose que les archives (prioritaire sur includeArchived),
pour que le toggle « Voir les archives » du front (ERP-173/ERP-164) soit operant.
Parametre optionnel en fin de signature : retro-compatible avec les appels existants.
2026-06-16 15:04:10 +02:00
Matthieu d9313dbec8 feat(transport) : schéma + entités Carrier + contrat lecture (ERP-155/157)
Schéma BDD du répertoire transporteurs (M4) + entités + contrat de lecture
(liste + détail), socle du front.

- Migration Version20260615150000 : tables carrier / carrier_address /
  carrier_contact / carrier_price (FK cross-module, CHECK enum, index partiel
  uq_carrier_name_active, COMMENT ON COLUMN). uploaded_document et
  qualimat_carrier réutilisées (non recréées).
- Entités Carrier* (#[Auditable], Timestampable/Blamable) + ApiResource
  LECTURE seule (GetCollection + Get via CarrierProvider, anti-N+1, exclusion
  archivés + ?includeArchived). Écriture (POST/PATCH + Processor) reportée WT4+.
- QualimatCarrier : mapping ORM lecture seule sur la table référentielle
  existante (sortie du schema_filter, mapping aligné DDL ERP-39, schema:update
  no-op) + endpoint de recherche read-only (§ 4.7).
- Relations cross-module des prix (Client/Supplier/adresses) via contrats
  Shared (ClientInterface, SupplierInterface, ClientAddressInterface,
  SupplierAddressInterface) + resolve_target_entities — sans import inter-module
  (règle n°1). Ajout du groupe supplier_address:read aux champs de
  SupplierAddress pour l'embed.
- Garde-fous : ColumnCommentsCatalog (carrier* + qualimat_carrier), makefile
  test-db-setup (index partiel carrier), i18n audit (transport_carrier*),
  EntitiesAreTimestampableBlamableTest (QualimatCarrier whitelisté).
- CarrierSerializationContractTest : contrat JSON liste + détail vérifié
  (embeds objet, booléens, enveloppe Hydra) ; JSON réel capturé dans
  spec-back § 4.0.bis.

make db-reset OK, make test vert (731), make nuxt-test vert (480),
php-cs-fixer OK.
2026-06-15 19:15:12 +02:00
Matthieu e607cccf08 feat(transport) : permissions carriers + sidebar (ERP-153)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m50s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m36s
Socle RBAC du module Transport (M4 § 5) :
- TransportModule::permissions() declare transport.carriers.{view,manage,archive}
- RbacSeeder::MATRIX (§ 5.2) : Bureau (view+manage), Commerciale (view) ;
  Compta/Usine aucun acces ; archive admin seul
- config/sidebar.php : section Transport + item /carriers (gate transport.carriers.view)
- i18n sidebar.transport.{section,carriers}
- 3 miroirs RBAC alignes : sidebar.php, personas.ts (user-full), SeedE2ECommand.php
- TransportModuleTest : garde-fou sur le jeu de permissions
2026-06-15 18:11:15 +02:00
11 changed files with 53 additions and 55 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.128'
app.version: '0.1.126'
+4 -5
View File
@@ -2,7 +2,6 @@
"common": {
"loading": "Chargement...",
"save": "Enregistrer",
"validate": "Valider",
"cancel": "Annuler",
"delete": "Supprimer",
"edit": "Modifier",
@@ -75,7 +74,7 @@
"categories": "Catégories",
"sites": "Sites",
"status": "Statut",
"archivedOnly": "Voir les archivés",
"includeArchived": "Inclure les archivés",
"apply": "Voir les résultats",
"reset": "Réinitialiser"
},
@@ -124,7 +123,7 @@
"back": "Retour au répertoire",
"loading": "Chargement du fournisseur…",
"notFound": "Fournisseur introuvable.",
"save": "Enregistrer"
"save": "Valider"
},
"form": {
"title": "Ajouter un fournisseur",
@@ -267,7 +266,7 @@
"back": "Retour au répertoire",
"loading": "Chargement du client…",
"notFound": "Client introuvable.",
"save": "Enregistrer"
"save": "Valider"
},
"validation": {
"informationRequiredForCommercial": "Les informations de l'entreprise sont obligatoires pour le rôle Commerciale.",
@@ -389,7 +388,7 @@
"categories": "Catégories",
"sites": "Sites",
"status": "Statut",
"archivedOnly": "Voir les archivés",
"includeArchived": "Inclure les archivés",
"apply": "Voir les résultats",
"reset": "Réinitialiser"
},
@@ -59,7 +59,7 @@
/>
<MalioButton
v-if="canShowSave"
:label="isCreateMode ? t('common.validate') : t('common.save')"
:label="t('common.save')"
variant="primary"
button-class="w-m-btn-action"
:disabled="form.submitting.value || loadingTypes"
@@ -51,7 +51,7 @@ describe('useSuppliersRepository', () => {
search: 'acme',
'categoryCode[]': ['NEGOCIANT', 'TRANSPORTEUR'],
'siteId[]': ['86', '17'],
archivedOnly: true,
includeArchived: true,
},
{ replace: true },
)
@@ -63,7 +63,7 @@ describe('useSuppliersRepository', () => {
search: 'acme',
'categoryCode[]': ['NEGOCIANT', 'TRANSPORTEUR'],
'siteId[]': ['86', '17'],
archivedOnly: true,
includeArchived: true,
page: 1,
itemsPerPage: 10,
},
@@ -73,7 +73,7 @@ describe('useSuppliersRepository', () => {
it('repasse a une query propre apres reinitialisation des filtres', async () => {
const repo = useSuppliersRepository()
await repo.setFilters({ search: 'acme', archivedOnly: true }, { replace: true })
await repo.setFilters({ search: 'acme', includeArchived: true }, { replace: true })
await repo.setFilters({}, { replace: true })
expect(mockGet).toHaveBeenLastCalledWith(
@@ -172,16 +172,16 @@ describe('Répertoire fournisseurs (page /suppliers)', () => {
)
})
it('repercute le filtre « Voir les archivés » dans setFilters sans toucher l\'URL', async () => {
it('repercute le filtre « Inclure les archivés » dans setFilters sans toucher l\'URL', async () => {
const wrapper = mountPage()
await flushPromises()
// Coche « Voir les archivés » puis applique les filtres.
await wrapper.find('input[data-id="filter-archived-only"]').setValue(true)
// Coche « Inclure les archivés » puis applique les filtres.
await wrapper.find('input[data-id="filter-include-archived"]').setValue(true)
await wrapper.find('[data-label="commercial.suppliers.filters.apply"]').trigger('click')
expect(mockSetFilters).toHaveBeenLastCalledWith(
{ archivedOnly: true },
{ includeArchived: true },
{ replace: true },
)
// Etat 100 % local (regle n°6) : aucune navigation/query string declenchee.
@@ -192,7 +192,7 @@ describe('Répertoire fournisseurs (page /suppliers)', () => {
const wrapper = mountPage()
await flushPromises()
await wrapper.find('input[data-id="filter-archived-only"]').setValue(true)
await wrapper.find('input[data-id="filter-include-archived"]').setValue(true)
await wrapper.find('[data-label="commercial.suppliers.filters.apply"]').trigger('click')
// Le libelle du bouton Filtrer porte le compteur (1 filtre actif).
@@ -128,13 +128,13 @@
</div>
</MalioAccordionItem>
<!-- Statut : bool unique. Coche = archives uniquement, sinon actifs. -->
<!-- Statut : bool unique. Coche = inclut aussi les archives (sinon actifs seuls). -->
<MalioAccordionItem :title="t('commercial.suppliers.filters.status')" value="status">
<MalioCheckbox
id="filter-archived-only"
:label="t('commercial.suppliers.filters.archivedOnly')"
:model-value="draftArchivedOnly"
@update:model-value="(val: boolean) => draftArchivedOnly = val"
id="filter-include-archived"
:label="t('commercial.suppliers.filters.includeArchived')"
:model-value="draftIncludeArchived"
@update:model-value="(val: boolean) => draftIncludeArchived = val"
/>
</MalioAccordionItem>
</MalioAccordion>
@@ -254,12 +254,12 @@ const filterDrawerOpen = ref(false)
const draftSearch = ref('')
const draftCategoryCodes = ref<string[]>([])
const draftSiteIds = ref<string[]>([])
const draftArchivedOnly = ref(false)
const draftIncludeArchived = ref(false)
const appliedSearch = ref('')
const appliedCategoryCodes = ref<string[]>([])
const appliedSiteIds = ref<string[]>([])
const appliedArchivedOnly = ref(false)
const appliedIncludeArchived = ref(false)
// Options des selects multi, chargees une fois (referentiels courts).
const categoryOptions = ref<FilterOption[]>([])
@@ -270,7 +270,7 @@ const activeFilterCount = computed(() => {
if (appliedSearch.value.trim() !== '') count++
if (appliedCategoryCodes.value.length > 0) count++
if (appliedSiteIds.value.length > 0) count++
if (appliedArchivedOnly.value) count++
if (appliedIncludeArchived.value) count++
return count
})
@@ -285,7 +285,7 @@ function openFilters(): void {
draftSearch.value = appliedSearch.value
draftCategoryCodes.value = [...appliedCategoryCodes.value]
draftSiteIds.value = [...appliedSiteIds.value]
draftArchivedOnly.value = appliedArchivedOnly.value
draftIncludeArchived.value = appliedIncludeArchived.value
filterDrawerOpen.value = true
}
@@ -311,7 +311,7 @@ function buildFilterPayload(): Record<string, string | string[] | boolean> {
if (appliedSearch.value.trim() !== '') payload.search = appliedSearch.value.trim()
if (appliedCategoryCodes.value.length > 0) payload['categoryCode[]'] = [...appliedCategoryCodes.value]
if (appliedSiteIds.value.length > 0) payload['siteId[]'] = [...appliedSiteIds.value]
if (appliedArchivedOnly.value) payload.archivedOnly = true
if (appliedIncludeArchived.value) payload.includeArchived = true
return payload
}
@@ -321,7 +321,7 @@ function applyFilters(): void {
appliedSearch.value = draftSearch.value.trim()
appliedCategoryCodes.value = [...draftCategoryCodes.value]
appliedSiteIds.value = [...draftSiteIds.value]
appliedArchivedOnly.value = draftArchivedOnly.value
appliedIncludeArchived.value = draftIncludeArchived.value
setFilters(buildFilterPayload(), { replace: true })
filterDrawerOpen.value = false
@@ -333,12 +333,12 @@ function resetFilters(): void {
draftSearch.value = ''
draftCategoryCodes.value = []
draftSiteIds.value = []
draftArchivedOnly.value = false
draftIncludeArchived.value = false
appliedSearch.value = ''
appliedCategoryCodes.value = []
appliedSiteIds.value = []
appliedArchivedOnly.value = false
appliedIncludeArchived.value = false
setFilters({}, { replace: true })
}
@@ -83,7 +83,7 @@
@click="emit('update:modelValue', false)"
/>
<MalioButton
:label="isEditMode ? t('common.save') : t('common.validate')"
:label="t('common.save')"
variant="primary"
button-class="w-m-btn-action"
:disabled="saving || permissionsLoadFailed"
@@ -103,7 +103,7 @@
@click="emit('update:modelValue', false)"
/>
<MalioButton
:label="isEditMode ? t('common.save') : t('common.validate')"
:label="t('common.save')"
variant="primary"
button-class="w-m-btn-action"
:disabled="saving || !isValidHex"
@@ -14,9 +14,9 @@ vi.stubGlobal('useApi', () => ({ get: mockApiGet }))
* - l'enveloppe Hydra (member / totalItems) est consommee
* - le header `Accept: application/ld+json` est envoye (sinon API Platform 4
* renvoie un tableau plat sans pagination)
* - EXCLUSION DES ARCHIVES PAR DEFAUT : aucun `archivedOnly` n'est envoye
* - EXCLUSION DES ARCHIVES PAR DEFAUT : aucun `includeArchived` n'est envoye
* tant que l'utilisateur ne coche pas le filtre (le back masque alors les
* archives) ; le filtre `archivedOnly` est bien transmis une fois applique.
* archives) ; le filtre `includeArchived` est bien transmis une fois applique.
*/
describe('useProvidersRepository', () => {
beforeEach(() => {
@@ -53,26 +53,26 @@ describe('useProvidersRepository', () => {
expect(repo.totalItems.value).toBe(1)
})
it('exclut les archives par defaut : aucun archivedOnly au premier fetch', async () => {
it('exclut les archives par defaut : aucun includeArchived au premier fetch', async () => {
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
const repo = useProvidersRepository()
await repo.fetch()
const query = mockApiGet.mock.calls[0][1] as Record<string, unknown>
expect(query.archivedOnly).toBeUndefined()
expect(query.includeArchived).toBeUndefined()
})
it('transmet archivedOnly une fois le filtre applique (retour page 1)', async () => {
it('transmet includeArchived une fois le filtre applique (retour page 1)', async () => {
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
const repo = useProvidersRepository()
await repo.fetch()
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
await repo.setFilters({ archivedOnly: true })
await repo.setFilters({ includeArchived: true })
expect(repo.currentPage.value).toBe(1)
const query = mockApiGet.mock.calls.at(-1)?.[1] as Record<string, unknown>
expect(query.archivedOnly).toBe(true)
expect(query.includeArchived).toBe(true)
})
})
@@ -45,11 +45,10 @@ export interface Provider {
* sur la ressource `/providers` (pagination serveur obligatoire ; jamais de
* chargement integral en memoire). Miroir de `useSuppliersRepository` (M2).
*
* Les filtres (recherche, categories, sites, archives) sont pilotes par la page
* via `setFilters` du composable partage — la remise en page 1 est garantie. Par
* defaut, aucun `archivedOnly` n'est envoye : le back masque donc les prestataires
* archives (exclusion par defaut, spec-back § 2.11). Cocher « Voir les archivés »
* envoie `archivedOnly=true` → seules les archives sont listees (aligne sur Client).
* Les filtres (recherche, categories, sites, inclusion des archives) sont pilotes
* par la page via `setFilters` du composable partage — la remise en page 1 est
* garantie. Par defaut, aucun `includeArchived` n'est envoye : le back masque
* donc les prestataires archives (exclusion par defaut, spec-back § 2.11).
*
* Le cloisonnement par site est applique AUTOMATIQUEMENT cote back (§ 2.13) en
* fonction de l'utilisateur — rien a filtrer cote front.
@@ -129,13 +129,13 @@
</div>
</MalioAccordionItem>
<!-- Statut : bool unique. Coche = archives uniquement, sinon actifs. -->
<!-- Statut : bool unique. Coche = inclut aussi les archives (sinon actifs seuls). -->
<MalioAccordionItem :title="t('technique.providers.filters.status')" value="status">
<MalioCheckbox
id="filter-archived-only"
:label="t('technique.providers.filters.archivedOnly')"
:model-value="draftArchivedOnly"
@update:model-value="(val: boolean) => draftArchivedOnly = val"
id="filter-include-archived"
:label="t('technique.providers.filters.includeArchived')"
:model-value="draftIncludeArchived"
@update:model-value="(val: boolean) => draftIncludeArchived = val"
/>
</MalioAccordionItem>
</MalioAccordion>
@@ -258,12 +258,12 @@ const filterDrawerOpen = ref(false)
const draftSearch = ref('')
const draftCategoryCodes = ref<string[]>([])
const draftSiteIds = ref<string[]>([])
const draftArchivedOnly = ref(false)
const draftIncludeArchived = ref(false)
const appliedSearch = ref('')
const appliedCategoryCodes = ref<string[]>([])
const appliedSiteIds = ref<string[]>([])
const appliedArchivedOnly = ref(false)
const appliedIncludeArchived = ref(false)
// Options des selects multi, chargees une fois (referentiels courts).
const categoryOptions = ref<FilterOption[]>([])
@@ -274,7 +274,7 @@ const activeFilterCount = computed(() => {
if (appliedSearch.value.trim() !== '') count++
if (appliedCategoryCodes.value.length > 0) count++
if (appliedSiteIds.value.length > 0) count++
if (appliedArchivedOnly.value) count++
if (appliedIncludeArchived.value) count++
return count
})
@@ -289,7 +289,7 @@ function openFilters(): void {
draftSearch.value = appliedSearch.value
draftCategoryCodes.value = [...appliedCategoryCodes.value]
draftSiteIds.value = [...appliedSiteIds.value]
draftArchivedOnly.value = appliedArchivedOnly.value
draftIncludeArchived.value = appliedIncludeArchived.value
filterDrawerOpen.value = true
}
@@ -315,7 +315,7 @@ function buildFilterPayload(): Record<string, string | string[] | boolean> {
if (appliedSearch.value.trim() !== '') payload.search = appliedSearch.value.trim()
if (appliedCategoryCodes.value.length > 0) payload['categoryCode[]'] = [...appliedCategoryCodes.value]
if (appliedSiteIds.value.length > 0) payload['siteId[]'] = [...appliedSiteIds.value]
if (appliedArchivedOnly.value) payload.archivedOnly = true
if (appliedIncludeArchived.value) payload.includeArchived = true
return payload
}
@@ -325,7 +325,7 @@ function applyFilters(): void {
appliedSearch.value = draftSearch.value.trim()
appliedCategoryCodes.value = [...draftCategoryCodes.value]
appliedSiteIds.value = [...draftSiteIds.value]
appliedArchivedOnly.value = draftArchivedOnly.value
appliedIncludeArchived.value = draftIncludeArchived.value
setFilters(buildFilterPayload(), { replace: true })
filterDrawerOpen.value = false
@@ -337,12 +337,12 @@ function resetFilters(): void {
draftSearch.value = ''
draftCategoryCodes.value = []
draftSiteIds.value = []
draftArchivedOnly.value = false
draftIncludeArchived.value = false
appliedSearch.value = ''
appliedCategoryCodes.value = []
appliedSiteIds.value = []
appliedArchivedOnly.value = false
appliedIncludeArchived.value = false
setFilters({}, { replace: true })
}