From acd9e14ba50f1d33316801c820198464810bb013 Mon Sep 17 00:00:00 2001 From: Selecta Keke Date: Wed, 24 Jun 2026 00:02:55 -0300 Subject: [PATCH] Initial local backup snapshot --- .codex-admin-directory-desktop.png | Bin 0 -> 21408 bytes .dockerignore | 10 + .gitignore | 114 + .gitkeep | 0 .manus/db/db-query-1769818203014.json | 28 + .manus/db/db-query-1769818206932.json | 34 + .manus/db/db-query-1769818255994.json | 9 + .prettierignore | 35 + .prettierrc | 15 + BDD ACTEURS SAVANES 2024 1.xlsx | Bin 0 -> 65867 bytes CCDS_Organigramme_2022.png | Bin 0 -> 170918 bytes Dockerfile | 21 + MAIL_PROVIDERS.md | 70 + cahier-des-charges-techniques-ccds.md | 359 + client/index.html | 23 + client/public/.gitkeep | 0 client/public/__manus__/debug-collector.js | 821 + client/public/__manus__/version.json | 4 + .../openmaptiles-positron-admin/style.json | 2375 +++ .../openmaptiles-positron/sprite.json | 16 + .../openmaptiles-positron/sprite.png | Bin 0 -> 529 bytes .../openmaptiles-positron/sprite@2x.json | 16 + .../openmaptiles-positron/sprite@2x.png | Bin 0 -> 921 bytes .../openmaptiles-positron/style.json | 2375 +++ client/src/App.tsx | 266 + client/src/_core/hooks/useAuth.ts | 111 + client/src/assets/ccds.png | Bin 0 -> 22907 bytes client/src/assets/hero-ccds-illustration.svg | 58 + client/src/components/AIChatBox.tsx | 335 + .../src/components/AccountSecurityPanel.tsx | 428 + .../components/AddressAutocompleteInput.tsx | 183 + .../components/AssociationPublicMap.lazy.ts | 1 + .../src/components/AssociationPublicMap.tsx | 934 + .../components/AuthenticatedPageSkeleton.tsx | 72 + .../src/components/CCDSInternalDirectory.tsx | 1604 ++ .../components/ComplianceOperationsPanel.tsx | 1652 ++ client/src/components/DashboardLayout.tsx | 779 + .../components/DashboardLayoutSkeleton.tsx | 46 + .../DataPrivacyValidationDialog.tsx | 129 + client/src/components/DirectionsMenu.tsx | 249 + .../src/components/DirectorSignatureAdmin.tsx | 782 + client/src/components/EmptyStateCard.tsx | 51 + client/src/components/ErrorBoundary.tsx | 68 + client/src/components/Map.tsx | 155 + .../src/components/ProfileRequiredState.tsx | 109 + .../src/components/PublicDocumentBackLink.tsx | 28 + client/src/components/ReservationCalendar.tsx | 1842 ++ client/src/components/SignaturePad.tsx | 129 + client/src/components/UserOperatingTour.tsx | 358 + .../components/admin/AdminAnalyticsCharts.tsx | 187 + .../components/admin/AdminDashboardCharts.tsx | 159 + .../src/components/portal/PortalSurface.tsx | 174 + client/src/components/ui/accordion.tsx | 64 + client/src/components/ui/alert-dialog.tsx | 155 + client/src/components/ui/alert.tsx | 66 + client/src/components/ui/aspect-ratio.tsx | 9 + client/src/components/ui/avatar.tsx | 51 + client/src/components/ui/badge.tsx | 46 + client/src/components/ui/breadcrumb.tsx | 109 + client/src/components/ui/button-group.tsx | 83 + client/src/components/ui/button.tsx | 61 + client/src/components/ui/calendar.tsx | 211 + client/src/components/ui/card.tsx | 140 + client/src/components/ui/carousel.tsx | 239 + client/src/components/ui/chart.tsx | 355 + client/src/components/ui/checkbox.tsx | 30 + client/src/components/ui/collapsible.tsx | 31 + client/src/components/ui/command.tsx | 184 + client/src/components/ui/context-menu.tsx | 250 + client/src/components/ui/dialog.tsx | 209 + client/src/components/ui/drawer.tsx | 133 + client/src/components/ui/dropdown-menu.tsx | 255 + client/src/components/ui/empty.tsx | 104 + client/src/components/ui/field.tsx | 242 + client/src/components/ui/form.tsx | 168 + client/src/components/ui/hover-card.tsx | 42 + client/src/components/ui/input-group.tsx | 168 + client/src/components/ui/input-otp.tsx | 75 + client/src/components/ui/input.tsx | 70 + client/src/components/ui/item.tsx | 193 + client/src/components/ui/kbd.tsx | 28 + client/src/components/ui/label.tsx | 22 + client/src/components/ui/menubar.tsx | 274 + client/src/components/ui/navigation-menu.tsx | 168 + client/src/components/ui/pagination.tsx | 127 + client/src/components/ui/popover.tsx | 46 + client/src/components/ui/progress.tsx | 29 + client/src/components/ui/radio-group.tsx | 43 + client/src/components/ui/resizable.tsx | 54 + client/src/components/ui/scroll-area.tsx | 56 + client/src/components/ui/select.tsx | 185 + client/src/components/ui/separator.tsx | 26 + client/src/components/ui/sheet.tsx | 139 + client/src/components/ui/sidebar.tsx | 733 + client/src/components/ui/skeleton.tsx | 13 + client/src/components/ui/slider.tsx | 61 + client/src/components/ui/sonner.tsx | 23 + client/src/components/ui/spinner.tsx | 16 + client/src/components/ui/switch.tsx | 29 + client/src/components/ui/table.tsx | 114 + client/src/components/ui/tabs.tsx | 64 + client/src/components/ui/textarea.tsx | 67 + client/src/components/ui/toggle-group.tsx | 73 + client/src/components/ui/toggle.tsx | 45 + client/src/components/ui/tooltip.tsx | 59 + client/src/const.ts | 5 + client/src/contexts/ThemeContext.tsx | 140 + client/src/hooks/useComposition.ts | 81 + client/src/hooks/useMobile.tsx | 83 + client/src/hooks/usePersistFn.ts | 20 + client/src/index.css | 530 + client/src/lib/addressAutocomplete.test.ts | 62 + client/src/lib/addressAutocomplete.ts | 84 + client/src/lib/appearance.ts | 242 + client/src/lib/appearanceScopes.ts | 137 + client/src/lib/loadXlsx.ts | 3 + client/src/lib/mapDirections.ts | 85 + client/src/lib/materialEvent.ts | 18 + client/src/lib/materialReturnFinalPdf.ts | 35 + client/src/lib/reservationSalle.test.ts | 163 + client/src/lib/reservationSalle.ts | 418 + client/src/lib/routePrefetch.ts | 62 + client/src/lib/trpc.ts | 4 + client/src/lib/userFeedback.ts | 54 + client/src/lib/utils.ts | 6 + client/src/main.tsx | 61 + client/src/pages/Admin.tsx | 15494 ++++++++++++++++ .../src/pages/AssociationDirectoryPortal.tsx | 481 + client/src/pages/AssociationInvitation.tsx | 217 + client/src/pages/AssociationMapPublic.tsx | 948 + client/src/pages/AssociationPublicProfile.tsx | 346 + client/src/pages/ComponentShowcase.tsx | 1437 ++ client/src/pages/CookiesPolicy.tsx | 63 + client/src/pages/Dashboard.tsx | 706 + .../src/pages/DemandeMaterielEvenementiel.tsx | 650 + client/src/pages/Documents.tsx | 723 + client/src/pages/EditRequest.tsx | 1439 ++ client/src/pages/FAQ.tsx | 222 + client/src/pages/Home.tsx | 404 + client/src/pages/LegalNotice.tsx | 78 + client/src/pages/Login.tsx | 326 + .../src/pages/MaterialConventionDownload.tsx | 123 + .../src/pages/MaterialReturnFinalPdfView.tsx | 105 + client/src/pages/MaterialReturnUpload.tsx | 682 + client/src/pages/NewRequest.tsx | 752 + client/src/pages/NotFound.tsx | 52 + client/src/pages/PrivacyPolicy.tsx | 151 + client/src/pages/Profile.tsx | 886 + client/src/pages/RequestDetail.tsx | 5023 +++++ client/src/pages/Requests.tsx | 408 + client/src/pages/ReservationSalle.tsx | 994 + .../pages/SalleWorkflowDocumentDownload.tsx | 230 + client/src/pages/StatsReportArchiveView.tsx | 182 + client/src/pages/TermsOfService.tsx | 98 + client/src/pages/TerrainMissions.tsx | 254 + components.json | 19 + deployments/README.md | 42 + deployments/preprod/.env.example | 33 + deployments/preprod/README.md | 67 + deployments/preprod/docker-compose.yml | 72 + deployments/prod/.env.example | 21 + deployments/prod/README.md | 50 + docker-compose.yml | 72 + .../ISO27001-RISK-001-matrice-ecarts.md | 33 + docs/compliance/README.md | 33 + .../RGPD-002-registre-des-traitements.md | 46 + ...05-accord-de-traitement-des-donnees-dpa.md | 57 + ...-006-politique-de-retention-des-donnees.md | 65 + ...rocedure-suppression-et-purge-planifiee.md | 154 + ...PD-008-rapport-mensuel-dpo-exploitation.md | 90 + docs/compliance/SEC-001-gestion-des-acces.md | 131 + ...gestion-des-sauvegardes-et-restauration.md | 113 + .../SEC-002A-preuve-execution-sauvegarde.md | 76 + .../SEC-002B-test-restauration-historise.md | 82 + .../SEC-003-gestion-des-incidents.md | 88 + ...registre-fournisseurs-et-sous-traitants.md | 90 + ...sociation-directory-official-enrichment.md | 197 + ...s-portail-association973-mars-juin-2026.md | 179 + docs/openmaptiles-autonome-premium.md | 227 + docs/runtime-health-maintenance.md | 127 + docs/ux/PORTAL-SKILLS-SYSTEM.md | 400 + docs/ux/ROADMAP-OPTIMISATION-PORTAIL.md | 474 + drizzle.config.ts | 15 + drizzle/0000_lush_vulcan.sql | 13 + drizzle/0001_green_rachel_grey.sql | 73 + drizzle/0002_numerous_cyclops.sql | 65 + drizzle/0003_closed_night_thrasher.sql | 13 + drizzle/0004_serious_deathstrike.sql | 2 + drizzle/0005_rich_blockbuster.sql | 5 + drizzle/0006_local_jwt_auth.sql | 1 + drizzle/0007_event_material_request.sql | 2 + drizzle/0008_association_directory.sql | 27 + drizzle/0009_association_invitations.sql | 19 + drizzle/0010_association_directory_geo.sql | 8 + ...1_association_directory_reference_sync.sql | 6 + drizzle/0012_association_social_links.sql | 3 + ...013_association_directory_social_links.sql | 3 + drizzle/0014_operational_recap_services.sql | 10 + drizzle/0015_association_thematics.sql | 21 + ...0016_association_thematics_multiselect.sql | 21 + drizzle/0017_association_governance.sql | 6 + drizzle/0018_material_return_followups.sql | 27 + .../0019_material_return_mobile_workflow.sql | 19 + ...material_return_geolocation_resilience.sql | 3 + ...material_return_litigation_arbitration.sql | 13 + ..._material_return_reactivation_tracking.sql | 4 + ...material_return_supervision_recipients.sql | 3 + drizzle/0024_user_logistics_permissions.sql | 6 + drizzle/0025_salle_quote_email_actions.sql | 2 + drizzle/0026_salle_signature_delegation.sql | 3 + drizzle/0027_accueil_role.sql | 2 + drizzle/0028_logistique_controle_role.sql | 2 + .../0029_operational_recap_service_usage.sql | 2 + drizzle/0030_service_terrain_role.sql | 10 + .../0031_association_directory_reviews.sql | 20 + drizzle/0032_amusing_boomerang.sql | 191 + ...association_directory_update_proposals.sql | 14 + drizzle/meta/0000_snapshot.json | 110 + drizzle/meta/0001_snapshot.json | 600 + drizzle/meta/0002_snapshot.json | 1007 + drizzle/meta/0003_snapshot.json | 1096 ++ drizzle/meta/0004_snapshot.json | 1096 ++ drizzle/meta/0005_snapshot.json | 1096 ++ drizzle/meta/0032_snapshot.json | 2380 +++ drizzle/meta/_journal.json | 237 + drizzle/migrations/.gitkeep | 0 drizzle/relations.ts | 1 + drizzle/schema.ts | 627 + exports/annuaires-integration-20260605.tar.gz | Bin 0 -> 41105 bytes .../annuaires-integration-20260605/README.md | 108 + .../src/components/AssociationPublicMap.tsx | 742 + .../src/components/CCDSInternalDirectory.tsx | 1485 ++ .../src/pages/AssociationDirectoryPortal.tsx | 405 + .../client/src/pages/AssociationMapPublic.tsx | 565 + .../src/pages/AssociationPublicProfile.tsx | 309 + .../server/associationDirectory.ts | 419 + .../server/associationDirectoryMatcher.ts | 183 + .../server/associationGeo.ts | 180 + .../server/associationHelloAssoSync.ts | 442 + .../shared/associationCommunes.ts | 44 + .../shared/associationGeo.ts | 29 + .../shared/associationThematics.ts | 88 + ...-ccds-portail-associations-2026-06-11.json | 73 + ...n-ccds-portail-associations-2026-06-11.pdf | Bin 0 -> 31247 bytes ...ds-portail-associations-2026-06-11.pdf.png | Bin 0 -> 185014 bytes ...e-honnete-ccds-portail-ovh-2026-06-11.json | 61 + ...re-honnete-ccds-portail-ovh-2026-06-11.pdf | Bin 0 -> 29554 bytes ...onnete-ccds-portail-ovh-2026-06-11.pdf.png | Bin 0 -> 176138 bytes ...-portail-associations-6300-2026-06-11.json | 74 + ...s-portail-associations-6300-2026-06-11.pdf | Bin 0 -> 31529 bytes ...rtail-associations-6300-2026-06-11.pdf.png | Bin 0 -> 191643 bytes local-services/forgejo/README.md | 36 + local-services/forgejo/docker-compose.yml | 50 + ...gramme-ccds-hierarchie-complete-page-1.png | Bin 0 -> 92453 bytes ...gramme-ccds-hierarchie-complete-page-2.png | Bin 0 -> 97894 bytes organigramme-ccds-hierarchie-complete.html | 702 + organigramme-conseil-communautaire.html | 570 + ...rtail-association973-projet-2026-06-22.pdf | 113 + package.json | 125 + patches/wouter@3.7.1.patch | 28 + pnpm-lock.yaml | 9586 ++++++++++ presentation-ccds-portail-du-site.html | 590 + presentation-ccds-portail-du-site.md | 302 + presentation-ccds-portail.html | 256 + scripts/check-openmaptiles-vps.sh | 24 + scripts/deploy-vps.sh | 179 + scripts/fill_compliance_observed.mts | 171 + scripts/generate_ccds_invoice_draft.mjs | 301 + scripts/generate_ccds_invoice_simple.mjs | 242 + scripts/maintenance-flag.sh | 30 + scripts/monitor-http-health.sh | 29 + scripts/submit-and-notify.mjs | 124 + server/_core/auth.ts | 962 + server/_core/context.ts | 28 + server/_core/cookies.ts | 50 + server/_core/dataApi.ts | 64 + server/_core/env.ts | 30 + server/_core/imageGeneration.ts | 92 + server/_core/index.ts | 217 + server/_core/llm.ts | 332 + server/_core/map.ts | 319 + server/_core/notification.ts | 110 + server/_core/runtime.ts | 78 + server/_core/systemRouter.ts | 29 + server/_core/trpc.ts | 45 + server/_core/types/cookie.d.ts | 6 + server/_core/vite.ts | 88 + server/_core/voiceTranscription.ts | 284 + server/admin.test.ts | 452 + server/association.test.ts | 510 + server/associationCommunes.test.ts | 27 + server/associationDirectory.test.ts | 73 + server/associationDirectory.ts | 419 + server/associationDirectoryMatcher.ts | 254 + server/associationGeo.test.ts | 85 + server/associationGeo.ts | 180 + server/associationHelloAssoSync.ts | 442 + server/associationInvitationEmail.ts | 68 + server/associationReferenceSync.ts | 402 + server/auth.logout.test.ts | 67 + server/calendar.test.ts | 231 + server/cancelRequest.test.ts | 70 + server/db.ts | 1649 ++ server/dsu.test.ts | 280 + server/emailActions.test.ts | 159 + server/emailActions.ts | 369 + server/emailRecap.ts | 343 + server/internalAccess.ts | 108 + server/logisticsGroup.ts | 82 + server/logisticsSettings.ts | 161 + server/mailSettings.ts | 256 + server/mailer.ts | 58 + server/materialConventionPdf.ts | 288 + server/materialReturnEmail.ts | 238 + server/materialReturnLitigationPdf.ts | 202 + server/materialReturnPdf.ts | 462 + server/materialReturnWorkflow.test.ts | 143 + server/materialReturnWorkflow.ts | 436 + server/operationalTour.ts | 952 + server/pdfGenerator.test.ts | 166 + server/pdfGenerator.ts | 958 + server/requestDetail.test.ts | 224 + server/requestPdf.ts | 66 + server/routers.ts | 10846 +++++++++++ server/salleAdministrativeDecisionPdf.ts | 89 + server/salleConventionPdf.ts | 254 + server/salleInvoicePdf.ts | 150 + server/salleQuotePdf.ts | 182 + server/salleReservationEmails.ts | 137 + server/salleReservationWorkflow.ts | 123 + server/salleSignatureDelegation.ts | 102 + server/socialAuth.ts | 281 + server/statsReportArtifacts.ts | 231 + server/storage.ts | 39 + .../fiche_etat_des_lieux_officielle.pdf | Bin 0 -> 103411 bytes shared/_core/errors.ts | 19 + shared/appearance.ts | 764 + shared/associationCommunes.ts | 44 + shared/associationGeo.ts | 29 + shared/associationGovernance.ts | 194 + shared/associationThematics.ts | 88 + shared/const.ts | 5 + shared/mailProviders.test.ts | 57 + shared/mailProviders.ts | 125 + shared/materialEvent.ts | 72 + shared/privacyCompliance.ts | 36 + shared/sallePricing.ts | 298 + shared/socialLinks.ts | 62 + shared/types.ts | 7 + tmp-documents-entry.png | Bin 0 -> 35372 bytes tmp-documents-header-mobile.png | Bin 0 -> 34517 bytes tmp-material-entry.png | Bin 0 -> 15031 bytes tmp-mes-demandes-mobile.png | Bin 0 -> 156128 bytes tmp-mes-demandes-tabs-narrow.png | Bin 0 -> 31321 bytes tmp-new-request-entry.png | Bin 0 -> 30949 bytes tmp-new-request-header-mobile.png | Bin 0 -> 30142 bytes tmp-profile-entry.png | Bin 0 -> 34977 bytes tmp-profile-header-mobile.png | Bin 0 -> 34728 bytes tmp-requests-entry.png | Bin 0 -> 33899 bytes tmp-requests-header-mobile.png | Bin 0 -> 33895 bytes tmp-reservation-entry.png | Bin 0 -> 15033 bytes todo.md | 125 + tsconfig.json | 23 + ...apport-statistiques-ccds-7d-2026-06-14.pdf | Bin 0 -> 3533 bytes ...pport-statistiques-ccds-7d-2026-06-14.xlsx | Bin 0 -> 24288 bytes vite.config.ts | 218 + vitest.config.ts | 19 + 367 files changed, 118038 insertions(+) create mode 100644 .codex-admin-directory-desktop.png create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .gitkeep create mode 100644 .manus/db/db-query-1769818203014.json create mode 100644 .manus/db/db-query-1769818206932.json create mode 100644 .manus/db/db-query-1769818255994.json create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 BDD ACTEURS SAVANES 2024 1.xlsx create mode 100644 CCDS_Organigramme_2022.png create mode 100644 Dockerfile create mode 100644 MAIL_PROVIDERS.md create mode 100644 cahier-des-charges-techniques-ccds.md create mode 100644 client/index.html create mode 100644 client/public/.gitkeep create mode 100644 client/public/__manus__/debug-collector.js create mode 100644 client/public/__manus__/version.json create mode 100644 client/public/map-styles/openmaptiles-positron-admin/style.json create mode 100644 client/public/map-styles/openmaptiles-positron/sprite.json create mode 100644 client/public/map-styles/openmaptiles-positron/sprite.png create mode 100644 client/public/map-styles/openmaptiles-positron/sprite@2x.json create mode 100644 client/public/map-styles/openmaptiles-positron/sprite@2x.png create mode 100644 client/public/map-styles/openmaptiles-positron/style.json create mode 100644 client/src/App.tsx create mode 100644 client/src/_core/hooks/useAuth.ts create mode 100644 client/src/assets/ccds.png create mode 100644 client/src/assets/hero-ccds-illustration.svg create mode 100644 client/src/components/AIChatBox.tsx create mode 100644 client/src/components/AccountSecurityPanel.tsx create mode 100644 client/src/components/AddressAutocompleteInput.tsx create mode 100644 client/src/components/AssociationPublicMap.lazy.ts create mode 100644 client/src/components/AssociationPublicMap.tsx create mode 100644 client/src/components/AuthenticatedPageSkeleton.tsx create mode 100644 client/src/components/CCDSInternalDirectory.tsx create mode 100644 client/src/components/ComplianceOperationsPanel.tsx create mode 100644 client/src/components/DashboardLayout.tsx create mode 100644 client/src/components/DashboardLayoutSkeleton.tsx create mode 100644 client/src/components/DataPrivacyValidationDialog.tsx create mode 100644 client/src/components/DirectionsMenu.tsx create mode 100644 client/src/components/DirectorSignatureAdmin.tsx create mode 100644 client/src/components/EmptyStateCard.tsx create mode 100644 client/src/components/ErrorBoundary.tsx create mode 100644 client/src/components/Map.tsx create mode 100644 client/src/components/ProfileRequiredState.tsx create mode 100644 client/src/components/PublicDocumentBackLink.tsx create mode 100644 client/src/components/ReservationCalendar.tsx create mode 100644 client/src/components/SignaturePad.tsx create mode 100644 client/src/components/UserOperatingTour.tsx create mode 100644 client/src/components/admin/AdminAnalyticsCharts.tsx create mode 100644 client/src/components/admin/AdminDashboardCharts.tsx create mode 100644 client/src/components/portal/PortalSurface.tsx create mode 100644 client/src/components/ui/accordion.tsx create mode 100644 client/src/components/ui/alert-dialog.tsx create mode 100644 client/src/components/ui/alert.tsx create mode 100644 client/src/components/ui/aspect-ratio.tsx create mode 100644 client/src/components/ui/avatar.tsx create mode 100644 client/src/components/ui/badge.tsx create mode 100644 client/src/components/ui/breadcrumb.tsx create mode 100644 client/src/components/ui/button-group.tsx create mode 100644 client/src/components/ui/button.tsx create mode 100644 client/src/components/ui/calendar.tsx create mode 100644 client/src/components/ui/card.tsx create mode 100644 client/src/components/ui/carousel.tsx create mode 100644 client/src/components/ui/chart.tsx create mode 100644 client/src/components/ui/checkbox.tsx create mode 100644 client/src/components/ui/collapsible.tsx create mode 100644 client/src/components/ui/command.tsx create mode 100644 client/src/components/ui/context-menu.tsx create mode 100644 client/src/components/ui/dialog.tsx create mode 100644 client/src/components/ui/drawer.tsx create mode 100644 client/src/components/ui/dropdown-menu.tsx create mode 100644 client/src/components/ui/empty.tsx create mode 100644 client/src/components/ui/field.tsx create mode 100644 client/src/components/ui/form.tsx create mode 100644 client/src/components/ui/hover-card.tsx create mode 100644 client/src/components/ui/input-group.tsx create mode 100644 client/src/components/ui/input-otp.tsx create mode 100644 client/src/components/ui/input.tsx create mode 100644 client/src/components/ui/item.tsx create mode 100644 client/src/components/ui/kbd.tsx create mode 100644 client/src/components/ui/label.tsx create mode 100644 client/src/components/ui/menubar.tsx create mode 100644 client/src/components/ui/navigation-menu.tsx create mode 100644 client/src/components/ui/pagination.tsx create mode 100644 client/src/components/ui/popover.tsx create mode 100644 client/src/components/ui/progress.tsx create mode 100644 client/src/components/ui/radio-group.tsx create mode 100644 client/src/components/ui/resizable.tsx create mode 100644 client/src/components/ui/scroll-area.tsx create mode 100644 client/src/components/ui/select.tsx create mode 100644 client/src/components/ui/separator.tsx create mode 100644 client/src/components/ui/sheet.tsx create mode 100644 client/src/components/ui/sidebar.tsx create mode 100644 client/src/components/ui/skeleton.tsx create mode 100644 client/src/components/ui/slider.tsx create mode 100644 client/src/components/ui/sonner.tsx create mode 100644 client/src/components/ui/spinner.tsx create mode 100644 client/src/components/ui/switch.tsx create mode 100644 client/src/components/ui/table.tsx create mode 100644 client/src/components/ui/tabs.tsx create mode 100644 client/src/components/ui/textarea.tsx create mode 100644 client/src/components/ui/toggle-group.tsx create mode 100644 client/src/components/ui/toggle.tsx create mode 100644 client/src/components/ui/tooltip.tsx create mode 100644 client/src/const.ts create mode 100644 client/src/contexts/ThemeContext.tsx create mode 100644 client/src/hooks/useComposition.ts create mode 100644 client/src/hooks/useMobile.tsx create mode 100644 client/src/hooks/usePersistFn.ts create mode 100644 client/src/index.css create mode 100644 client/src/lib/addressAutocomplete.test.ts create mode 100644 client/src/lib/addressAutocomplete.ts create mode 100644 client/src/lib/appearance.ts create mode 100644 client/src/lib/appearanceScopes.ts create mode 100644 client/src/lib/loadXlsx.ts create mode 100644 client/src/lib/mapDirections.ts create mode 100644 client/src/lib/materialEvent.ts create mode 100644 client/src/lib/materialReturnFinalPdf.ts create mode 100644 client/src/lib/reservationSalle.test.ts create mode 100644 client/src/lib/reservationSalle.ts create mode 100644 client/src/lib/routePrefetch.ts create mode 100644 client/src/lib/trpc.ts create mode 100644 client/src/lib/userFeedback.ts create mode 100644 client/src/lib/utils.ts create mode 100644 client/src/main.tsx create mode 100644 client/src/pages/Admin.tsx create mode 100644 client/src/pages/AssociationDirectoryPortal.tsx create mode 100644 client/src/pages/AssociationInvitation.tsx create mode 100644 client/src/pages/AssociationMapPublic.tsx create mode 100644 client/src/pages/AssociationPublicProfile.tsx create mode 100644 client/src/pages/ComponentShowcase.tsx create mode 100644 client/src/pages/CookiesPolicy.tsx create mode 100644 client/src/pages/Dashboard.tsx create mode 100644 client/src/pages/DemandeMaterielEvenementiel.tsx create mode 100644 client/src/pages/Documents.tsx create mode 100644 client/src/pages/EditRequest.tsx create mode 100644 client/src/pages/FAQ.tsx create mode 100644 client/src/pages/Home.tsx create mode 100644 client/src/pages/LegalNotice.tsx create mode 100644 client/src/pages/Login.tsx create mode 100644 client/src/pages/MaterialConventionDownload.tsx create mode 100644 client/src/pages/MaterialReturnFinalPdfView.tsx create mode 100644 client/src/pages/MaterialReturnUpload.tsx create mode 100644 client/src/pages/NewRequest.tsx create mode 100644 client/src/pages/NotFound.tsx create mode 100644 client/src/pages/PrivacyPolicy.tsx create mode 100644 client/src/pages/Profile.tsx create mode 100644 client/src/pages/RequestDetail.tsx create mode 100644 client/src/pages/Requests.tsx create mode 100644 client/src/pages/ReservationSalle.tsx create mode 100644 client/src/pages/SalleWorkflowDocumentDownload.tsx create mode 100644 client/src/pages/StatsReportArchiveView.tsx create mode 100644 client/src/pages/TermsOfService.tsx create mode 100644 client/src/pages/TerrainMissions.tsx create mode 100644 components.json create mode 100644 deployments/README.md create mode 100644 deployments/preprod/.env.example create mode 100644 deployments/preprod/README.md create mode 100644 deployments/preprod/docker-compose.yml create mode 100644 deployments/prod/.env.example create mode 100644 deployments/prod/README.md create mode 100644 docker-compose.yml create mode 100644 docs/compliance/ISO27001-RISK-001-matrice-ecarts.md create mode 100644 docs/compliance/README.md create mode 100644 docs/compliance/RGPD-002-registre-des-traitements.md create mode 100644 docs/compliance/RGPD-005-accord-de-traitement-des-donnees-dpa.md create mode 100644 docs/compliance/RGPD-006-politique-de-retention-des-donnees.md create mode 100644 docs/compliance/RGPD-007-procedure-suppression-et-purge-planifiee.md create mode 100644 docs/compliance/RGPD-008-rapport-mensuel-dpo-exploitation.md create mode 100644 docs/compliance/SEC-001-gestion-des-acces.md create mode 100644 docs/compliance/SEC-002-gestion-des-sauvegardes-et-restauration.md create mode 100644 docs/compliance/SEC-002A-preuve-execution-sauvegarde.md create mode 100644 docs/compliance/SEC-002B-test-restauration-historise.md create mode 100644 docs/compliance/SEC-003-gestion-des-incidents.md create mode 100644 docs/compliance/SEC-005-registre-fournisseurs-et-sous-traitants.md create mode 100644 docs/compliance/association-directory-official-enrichment.md create mode 100644 docs/facturation/projet-facture-ccds-portail-association973-mars-juin-2026.md create mode 100644 docs/openmaptiles-autonome-premium.md create mode 100644 docs/runtime-health-maintenance.md create mode 100644 docs/ux/PORTAL-SKILLS-SYSTEM.md create mode 100644 docs/ux/ROADMAP-OPTIMISATION-PORTAIL.md create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_lush_vulcan.sql create mode 100644 drizzle/0001_green_rachel_grey.sql create mode 100644 drizzle/0002_numerous_cyclops.sql create mode 100644 drizzle/0003_closed_night_thrasher.sql create mode 100644 drizzle/0004_serious_deathstrike.sql create mode 100644 drizzle/0005_rich_blockbuster.sql create mode 100644 drizzle/0006_local_jwt_auth.sql create mode 100644 drizzle/0007_event_material_request.sql create mode 100644 drizzle/0008_association_directory.sql create mode 100644 drizzle/0009_association_invitations.sql create mode 100644 drizzle/0010_association_directory_geo.sql create mode 100644 drizzle/0011_association_directory_reference_sync.sql create mode 100644 drizzle/0012_association_social_links.sql create mode 100644 drizzle/0013_association_directory_social_links.sql create mode 100644 drizzle/0014_operational_recap_services.sql create mode 100644 drizzle/0015_association_thematics.sql create mode 100644 drizzle/0016_association_thematics_multiselect.sql create mode 100644 drizzle/0017_association_governance.sql create mode 100644 drizzle/0018_material_return_followups.sql create mode 100644 drizzle/0019_material_return_mobile_workflow.sql create mode 100644 drizzle/0020_material_return_geolocation_resilience.sql create mode 100644 drizzle/0021_material_return_litigation_arbitration.sql create mode 100644 drizzle/0022_material_return_reactivation_tracking.sql create mode 100644 drizzle/0023_material_return_supervision_recipients.sql create mode 100644 drizzle/0024_user_logistics_permissions.sql create mode 100644 drizzle/0025_salle_quote_email_actions.sql create mode 100644 drizzle/0026_salle_signature_delegation.sql create mode 100644 drizzle/0027_accueil_role.sql create mode 100644 drizzle/0028_logistique_controle_role.sql create mode 100644 drizzle/0029_operational_recap_service_usage.sql create mode 100644 drizzle/0030_service_terrain_role.sql create mode 100644 drizzle/0031_association_directory_reviews.sql create mode 100644 drizzle/0032_amusing_boomerang.sql create mode 100644 drizzle/0033_association_directory_update_proposals.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 drizzle/meta/0005_snapshot.json create mode 100644 drizzle/meta/0032_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 drizzle/migrations/.gitkeep create mode 100644 drizzle/relations.ts create mode 100644 drizzle/schema.ts create mode 100644 exports/annuaires-integration-20260605.tar.gz create mode 100644 exports/annuaires-integration-20260605/README.md create mode 100644 exports/annuaires-integration-20260605/client/src/components/AssociationPublicMap.tsx create mode 100644 exports/annuaires-integration-20260605/client/src/components/CCDSInternalDirectory.tsx create mode 100644 exports/annuaires-integration-20260605/client/src/pages/AssociationDirectoryPortal.tsx create mode 100644 exports/annuaires-integration-20260605/client/src/pages/AssociationMapPublic.tsx create mode 100644 exports/annuaires-integration-20260605/client/src/pages/AssociationPublicProfile.tsx create mode 100644 exports/annuaires-integration-20260605/server/associationDirectory.ts create mode 100644 exports/annuaires-integration-20260605/server/associationDirectoryMatcher.ts create mode 100644 exports/annuaires-integration-20260605/server/associationGeo.ts create mode 100644 exports/annuaires-integration-20260605/server/associationHelloAssoSync.ts create mode 100644 exports/annuaires-integration-20260605/shared/associationCommunes.ts create mode 100644 exports/annuaires-integration-20260605/shared/associationGeo.ts create mode 100644 exports/annuaires-integration-20260605/shared/associationThematics.ts create mode 100644 exports/factures/facture-brouillon-ccds-portail-associations-2026-06-11.json create mode 100644 exports/factures/facture-brouillon-ccds-portail-associations-2026-06-11.pdf create mode 100644 exports/factures/facture-brouillon-ccds-portail-associations-2026-06-11.pdf.png create mode 100644 exports/factures/facture-honnete-ccds-portail-ovh-2026-06-11.json create mode 100644 exports/factures/facture-honnete-ccds-portail-ovh-2026-06-11.pdf create mode 100644 exports/factures/facture-honnete-ccds-portail-ovh-2026-06-11.pdf.png create mode 100644 exports/factures/facture-projet-ccds-portail-associations-6300-2026-06-11.json create mode 100644 exports/factures/facture-projet-ccds-portail-associations-6300-2026-06-11.pdf create mode 100644 exports/factures/facture-projet-ccds-portail-associations-6300-2026-06-11.pdf.png create mode 100644 local-services/forgejo/README.md create mode 100644 local-services/forgejo/docker-compose.yml create mode 100644 organigramme-ccds-hierarchie-complete-page-1.png create mode 100644 organigramme-ccds-hierarchie-complete-page-2.png create mode 100644 organigramme-ccds-hierarchie-complete.html create mode 100644 organigramme-conseil-communautaire.html create mode 100644 output/pdf/facture-ccds-portail-association973-projet-2026-06-22.pdf create mode 100644 package.json create mode 100644 patches/wouter@3.7.1.patch create mode 100644 pnpm-lock.yaml create mode 100644 presentation-ccds-portail-du-site.html create mode 100644 presentation-ccds-portail-du-site.md create mode 100644 presentation-ccds-portail.html create mode 100755 scripts/check-openmaptiles-vps.sh create mode 100755 scripts/deploy-vps.sh create mode 100644 scripts/fill_compliance_observed.mts create mode 100644 scripts/generate_ccds_invoice_draft.mjs create mode 100644 scripts/generate_ccds_invoice_simple.mjs create mode 100755 scripts/maintenance-flag.sh create mode 100755 scripts/monitor-http-health.sh create mode 100644 scripts/submit-and-notify.mjs create mode 100644 server/_core/auth.ts create mode 100644 server/_core/context.ts create mode 100644 server/_core/cookies.ts create mode 100644 server/_core/dataApi.ts create mode 100644 server/_core/env.ts create mode 100644 server/_core/imageGeneration.ts create mode 100644 server/_core/index.ts create mode 100644 server/_core/llm.ts create mode 100644 server/_core/map.ts create mode 100644 server/_core/notification.ts create mode 100644 server/_core/runtime.ts create mode 100644 server/_core/systemRouter.ts create mode 100644 server/_core/trpc.ts create mode 100644 server/_core/types/cookie.d.ts create mode 100644 server/_core/vite.ts create mode 100644 server/_core/voiceTranscription.ts create mode 100644 server/admin.test.ts create mode 100644 server/association.test.ts create mode 100644 server/associationCommunes.test.ts create mode 100644 server/associationDirectory.test.ts create mode 100644 server/associationDirectory.ts create mode 100644 server/associationDirectoryMatcher.ts create mode 100644 server/associationGeo.test.ts create mode 100644 server/associationGeo.ts create mode 100644 server/associationHelloAssoSync.ts create mode 100644 server/associationInvitationEmail.ts create mode 100644 server/associationReferenceSync.ts create mode 100644 server/auth.logout.test.ts create mode 100644 server/calendar.test.ts create mode 100644 server/cancelRequest.test.ts create mode 100644 server/db.ts create mode 100644 server/dsu.test.ts create mode 100644 server/emailActions.test.ts create mode 100644 server/emailActions.ts create mode 100644 server/emailRecap.ts create mode 100644 server/internalAccess.ts create mode 100644 server/logisticsGroup.ts create mode 100644 server/logisticsSettings.ts create mode 100644 server/mailSettings.ts create mode 100644 server/mailer.ts create mode 100644 server/materialConventionPdf.ts create mode 100644 server/materialReturnEmail.ts create mode 100644 server/materialReturnLitigationPdf.ts create mode 100644 server/materialReturnPdf.ts create mode 100644 server/materialReturnWorkflow.test.ts create mode 100644 server/materialReturnWorkflow.ts create mode 100644 server/operationalTour.ts create mode 100644 server/pdfGenerator.test.ts create mode 100644 server/pdfGenerator.ts create mode 100644 server/requestDetail.test.ts create mode 100644 server/requestPdf.ts create mode 100644 server/routers.ts create mode 100644 server/salleAdministrativeDecisionPdf.ts create mode 100644 server/salleConventionPdf.ts create mode 100644 server/salleInvoicePdf.ts create mode 100644 server/salleQuotePdf.ts create mode 100644 server/salleReservationEmails.ts create mode 100644 server/salleReservationWorkflow.ts create mode 100644 server/salleSignatureDelegation.ts create mode 100644 server/socialAuth.ts create mode 100644 server/statsReportArtifacts.ts create mode 100644 server/storage.ts create mode 100644 server/templates/fiche_etat_des_lieux_officielle.pdf create mode 100644 shared/_core/errors.ts create mode 100644 shared/appearance.ts create mode 100644 shared/associationCommunes.ts create mode 100644 shared/associationGeo.ts create mode 100644 shared/associationGovernance.ts create mode 100644 shared/associationThematics.ts create mode 100644 shared/const.ts create mode 100644 shared/mailProviders.test.ts create mode 100644 shared/mailProviders.ts create mode 100644 shared/materialEvent.ts create mode 100644 shared/privacyCompliance.ts create mode 100644 shared/sallePricing.ts create mode 100644 shared/socialLinks.ts create mode 100644 shared/types.ts create mode 100644 tmp-documents-entry.png create mode 100644 tmp-documents-header-mobile.png create mode 100644 tmp-material-entry.png create mode 100644 tmp-mes-demandes-mobile.png create mode 100644 tmp-mes-demandes-tabs-narrow.png create mode 100644 tmp-new-request-entry.png create mode 100644 tmp-new-request-header-mobile.png create mode 100644 tmp-profile-entry.png create mode 100644 tmp-profile-header-mobile.png create mode 100644 tmp-requests-entry.png create mode 100644 tmp-requests-header-mobile.png create mode 100644 tmp-reservation-entry.png create mode 100644 todo.md create mode 100644 tsconfig.json create mode 100644 uploads/portal/stats-reports/weekly/6YN2tyVLHpcywrfeIZArg-rapport-statistiques-ccds-7d-2026-06-14.pdf create mode 100644 uploads/portal/stats-reports/weekly/M4T20z4sa-myDunNJhP8W-rapport-statistiques-ccds-7d-2026-06-14.xlsx create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.codex-admin-directory-desktop.png b/.codex-admin-directory-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..ca7741420b7139015e25e2db80a6e40bcba22170 GIT binary patch literal 21408 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 literal 0 HcmV?d00001 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2168f06 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.git +.DS_Store +.env +.env.* +npm-debug.log* +pnpm-debug.log* +coverage +uploads diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6decbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,114 @@ +# Dependencies +**/node_modules +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock +*.bak + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Local services +local-services/forgejo/data/ +local-services/forgejo/db/ + +# Local tools +local-services/forgejo/.env.local diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.manus/db/db-query-1769818203014.json b/.manus/db/db-query-1769818203014.json new file mode 100644 index 0000000..b717437 --- /dev/null +++ b/.manus/db/db-query-1769818203014.json @@ -0,0 +1,28 @@ +{ + "query": "SELECT id, name, email, role, isActive, createdAt, lastSignedIn FROM users ORDER BY createdAt;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 2fRWrTVEDLYRs9G.c8049d3e27ba --database EAGCgSPXcL4ryFq9XLBKvW --execute SELECT id, name, email, role, isActive, createdAt, lastSignedIn FROM users ORDER BY createdAt;", + "rows": [ + { + "id": "1", + "name": "Selecta Keke", + "email": "selectakeke973@gmail.com", + "role": "admin", + "isActive": "1", + "createdAt": "2026-01-30 21:56:11", + "lastSignedIn": "2026-01-31 00:03:40" + }, + { + "id": "90004", + "name": "William", + "email": "william.meri@gmail.com", + "role": "user", + "isActive": "1", + "createdAt": "2026-01-30 23:26:14", + "lastSignedIn": "2026-01-31 00:08:06" + } + ], + "messages": [], + "stdout": "id\tname\temail\trole\tisActive\tcreatedAt\tlastSignedIn\n1\tSelecta Keke\tselectakeke973@gmail.com\tadmin\t1\t2026-01-30 21:56:11\t2026-01-31 00:03:40\n90004\tWilliam\twilliam.meri@gmail.com\tuser\t1\t2026-01-30 23:26:14\t2026-01-31 00:08:06\n", + "stderr": "", + "execution_time_ms": 214 +} \ No newline at end of file diff --git a/.manus/db/db-query-1769818206932.json b/.manus/db/db-query-1769818206932.json new file mode 100644 index 0000000..80dd0bc --- /dev/null +++ b/.manus/db/db-query-1769818206932.json @@ -0,0 +1,34 @@ +{ + "query": "SELECT * FROM users;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 2fRWrTVEDLYRs9G.c8049d3e27ba --database EAGCgSPXcL4ryFq9XLBKvW --execute SELECT * FROM users;", + "rows": [ + { + "id": "1", + "openId": "Pzgrt3DbY8fVWA3UaMwk39", + "name": "Selecta Keke", + "email": "selectakeke973@gmail.com", + "loginMethod": "google", + "role": "admin", + "createdAt": "2026-01-30 21:56:11", + "updatedAt": "2026-01-31 00:03:40", + "lastSignedIn": "2026-01-31 00:03:40", + "isActive": "1" + }, + { + "id": "90004", + "openId": "ijeVp4mua5wFXDoHd43yfm", + "name": "William", + "email": "william.meri@gmail.com", + "loginMethod": "google", + "role": "user", + "createdAt": "2026-01-30 23:26:14", + "updatedAt": "2026-01-31 00:08:05", + "lastSignedIn": "2026-01-31 00:08:06", + "isActive": "1" + } + ], + "messages": [], + "stdout": "id\topenId\tname\temail\tloginMethod\trole\tcreatedAt\tupdatedAt\tlastSignedIn\tisActive\n1\tPzgrt3DbY8fVWA3UaMwk39\tSelecta Keke\tselectakeke973@gmail.com\tgoogle\tadmin\t2026-01-30 21:56:11\t2026-01-31 00:03:40\t2026-01-31 00:03:40\t1\n90004\tijeVp4mua5wFXDoHd43yfm\tWilliam\twilliam.meri@gmail.com\tgoogle\tuser\t2026-01-30 23:26:14\t2026-01-31 00:08:05\t2026-01-31 00:08:06\t1\n", + "stderr": "", + "execution_time_ms": 62 +} \ No newline at end of file diff --git a/.manus/db/db-query-1769818255994.json b/.manus/db/db-query-1769818255994.json new file mode 100644 index 0000000..f6f1131 --- /dev/null +++ b/.manus/db/db-query-1769818255994.json @@ -0,0 +1,9 @@ +{ + "query": "UPDATE users SET role = 'super_admin' WHERE id = 1; UPDATE users SET role = 'admin' WHERE id = 90004;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 2fRWrTVEDLYRs9G.c8049d3e27ba --database EAGCgSPXcL4ryFq9XLBKvW --execute UPDATE users SET role = 'super_admin' WHERE id = 1; UPDATE users SET role = 'admin' WHERE id = 90004;", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 65 +} \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7284259 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Generated files +*.tsbuildinfo +coverage/ + +# Package files +package-lock.json +pnpm-lock.yaml + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log + +# Environment files +.env* + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..67c0bc8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "proseWrap": "preserve" +} diff --git a/BDD ACTEURS SAVANES 2024 1.xlsx b/BDD ACTEURS SAVANES 2024 1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b395c9c2eade5af290a842f2f8b1ccf8f9573dbf GIT binary patch literal 65867 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%0De{#sw)9fIG(YO++q? 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 literal 0 HcmV?d00001 diff --git a/CCDS_Organigramme_2022.png b/CCDS_Organigramme_2022.png new file mode 100644 index 0000000000000000000000000000000000000000..546a26d824edba36ae924d817dcb1b2183206568 GIT binary patch literal 170918 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

WiwQg#(EekgA!;Fcy2=YA=1-jGi#9prvEGZ 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�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 Z0~bOJpY{@=fcxPw date restitution + 3 jours` +- **Alerte orange** : nombre de litiges / degradations non arbitres +- **Badge bleu** : nouvelles demandes en attente d'instruction + +Chaque indicateur doit etre cliquable et appliquer un filtre direct sur les dossiers concernes. + +#### Flux operationnel journalier + +Deux colonnes : + +- **Departs / matin** : mises a disposition ou livraisons du jour +- **Retours / apres-midi** : restitutions attendues au jour J + +Chaque ligne doit faire apparaitre : + +- l'association ; +- le type de mission ; +- le statut d'assignation ; +- un acces rapide au dossier. + +#### Composant de visualisation des stocks + +- graphique donut ou barres ; +- quantites disponibles / engagees / bloquees ; +- lecture simple par categorie de materiel. + +#### Fil d'activite + +- affichage des 5 derniers evenements du journal d'audit ; +- lecture chronologique claire ; +- acces au journal complet. + +### 2.2 Module unifie "Dossier & assignation logistique" + +Le systeme doit supprimer les blocages de navigation entre consultation et action. + +#### Depuis la fiche dossier + +La fiche complete d'une demande acceptee doit integrer un bloc : + +`Assigner un service recuperateur` + +Ce bloc doit permettre : + +- de choisir un service ou agent via un menu relie a l'annuaire interne ; +- de valider l'assignation sans quitter la fiche ; +- de declencher le workflow de terrain. + +#### Depuis l'ecran d'assignation + +Un bouton secondaire : + +`Voir la demande` + +doit ouvrir un panneau lateral ou une drawer affichant : + +- details de l'association ; +- liste complete du materiel ; +- dates ; +- lieu / manifestation ; +- informations utiles a la decision. + +### 2.3 Calendrier logistique predictif + +Le calendrier doit etre relie a l'inventaire reel. + +#### Regle d'indexation stock + +Lorsqu'une demande est validee sur une periode donnee : + +- les quantites reservees doivent etre deduites du stock global ; +- cette deduction ne s'applique que sur la plage temporelle concernee. + +#### Affichage visuel attendu + +Chaque dossier doit produire trois phases visibles : + +- **Sortie / livraison** : bleu +- **Manifestation / indisponibilite stock** : phase d'utilisation +- **Alerte restitution** : jaune + +#### Interactions attendues + +Au clic sur un evenement du calendrier : + +- consulter l'agent ou service affecte ; +- reassigner la mission ; +- telecharger le PDF finalise si disponible. + +### 2.4 Cartographie interactive & implantation territoriale + +La cartographie doit etre connectee a la base de donnees et servir a la fois le grand public et l'administration. + +Technologies recommandees : + +- Leaflet.js +- ou Mapbox GL JS + +Traitements attendus : + +- geocodage automatique des adresses via `api-adresse.data.gouv.fr` ou `Nominatim` +- stockage des coordonnees geographiques +- clustering obligatoire pour eviter la surcharge visuelle + +Filtres attendus : + +- annuaire public par thematique ; +- vue logistique admin ; +- heatmap / densite ; +- filtres territoriaux sur Iracoubo, Kourou, Saint-Elie, Sinnamary. + +## 3. Specifications du workflow mobile terrain + +### 3.1 Declenchement & routage + +Un traitement planifie quotidien doit verifier les dossiers. + +Pour tout dossier dont la restitution theorique est a **J-4** : + +- generer un jeton d'acces securise temporaire ; +- construire une URL unique ; +- injecter cette URL dans l'email du service ou agent affecte. + +Exemple de forme attendue : + +`https://ccds.fr/restitution/v1?token=sha256_unique_hash` + +### 3.2 Interface mobile & gestion non bloquante + +Lors de l'ouverture sur smartphone : + +- les donnees de mise a disposition sont pre-remplies et figees ; +- l'agent ne modifie pas les donnees source de la demande ; +- le constat terrain se fait sur l'etat reel de restitution. + +#### Bloc "Restitution de materiel" + +Boutons radio exclusifs : + +- `Conforme` +- `Non conforme / Partiel` + +Comportement attendu : + +- `Conforme` valide globalement le retour ; +- `Non conforme / Partiel` active obligatoirement le bloc observations. + +#### Bloc "Observations contradictoires" + +Cases a cocher : + +- equipement manquant +- accessoires / visserie manquants +- armature deformee +- toile / bache dechiree +- impact / choc majeur +- restitution sale +- materiel humide + +Des qu'une case est cochee : + +- le champ texte libre devient obligatoire. + +### 3.3 Double signature, horodatage et resilience GPS + +#### Horodatage certifie + +Le backend doit ecraser toute date / heure issue de l'appareil. + +La date officielle est generee exclusivement par le serveur au moment de la soumission. + +#### Gestion robuste de la geolocalisation + +Le script `navigator.geolocation` doit : + +- fonctionner en asynchrone ; +- comporter un timeout strict de `5000 ms` ; +- ne jamais bloquer la validation finale. + +Scenarios : + +- **Succes** : les coordonnees sont jointes a la soumission +- **Echec** : la valeur `Non disponible` est enregistree, mais la validation continue normalement + +#### Signature tactile + +Le systeme doit integrer : + +- une zone de signature pour l'agent ; +- une zone de signature pour l'emprunteur ; +- les qualites associees ; +- un rendu suffisamment fluide pour un usage doigt / stylet. + +### 3.4 Generation dynamique du PDF & archivage + +Apres validation finale : + +- le serveur compile le document officiel ; +- fusionne les donnees, signatures et metadonnees ; +- applique le logo CCDS ; +- produit un PDF A4 sur une page si le contenu le permet. + +Contraintes de sortie : + +- CSS imprimee calibree ; +- signatures visibles ; +- mise en page stable ; +- envoi automatique en piece jointe : + - a l'association ; + - a la DSU ; +- mise a jour du statut en base. + +## 4. Protocole expeditif automatise : gestion des litiges + +Si la restitution est saisie comme `Non conforme / Partiel` ou `Mauvais etat` : + +### 4.1 Rupture de flux & gel logistique + +Le dossier passe en : + +`LITIGE / ATTENTE ARBITRAGE` + +Le materiel concerne doit etre marque indisponible pour : + +- bloquer son retour dans le calendrier ; +- empecher sa relocation par erreur. + +### 4.2 Interface d'arbitrage financier + +La fiche admin du dossier doit permettre : + +- de saisir le cout d'un devis ; +- de saisir un cout interne ; +- ou d'appliquer la valeur de remplacement a neuf issue de l'inventaire. + +### 4.3 Notification & cloture + +Le systeme doit proposer : + +- un bouton `Generer courrier de litige` +- un document pre-rempli pour notifier l'association + +Le dossier ne peut etre cloture definitivement qu'apres arbitrage explicite parmi : + +- retenue partielle sur caution +- encaissement total +- classement sans suite + +La decision doit : + +- mettre a jour le stock ; +- liberer ou non le materiel ; +- etre ecrite dans le journal d'audit. + +## 5. Vision d'ensemble + +La plateforme doit etre comprise comme un **ecosysteme logistique connecte et predictif**. + +Elle doit permettre : + +- de centraliser la gestion associative ; +- de piloter le materiel dans le temps ; +- de fluidifier les workflows terrain ; +- de proteger juridiquement et financierement la collectivite ; +- d'offrir a la direction un vrai centre de controle. + +## 6. Resultat attendu + +Le systeme final ne doit pas etre un simple outil de gestion administrative. + +Il doit devenir : + +- une plateforme de service public numerique ; +- un centre de pilotage logistique ; +- un outil de tracabilite ; +- un support de decision ; +- et un cadre fiable pour l'arbitrage, le stock et les operations terrain. diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..b4f5eb1 --- /dev/null +++ b/client/index.html @@ -0,0 +1,23 @@ + + + + + + + + Portail Associations + + + + +

+ + + + diff --git a/client/public/.gitkeep b/client/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/public/__manus__/debug-collector.js b/client/public/__manus__/debug-collector.js new file mode 100644 index 0000000..0504555 --- /dev/null +++ b/client/public/__manus__/debug-collector.js @@ -0,0 +1,821 @@ +/** + * Manus Debug Collector (agent-friendly) + * + * Captures: + * 1) Console logs + * 2) Network requests (fetch + XHR) + * 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.) + * + * Data is periodically sent to /__manus__/logs + * Note: uiEvents are mirrored to sessionEvents for sessionReplay.log + */ +(function () { + "use strict"; + + // Prevent double initialization + if (window.__MANUS_DEBUG_COLLECTOR__) return; + + // ========================================================================== + // Configuration + // ========================================================================== + const CONFIG = { + reportEndpoint: "/__manus__/logs", + bufferSize: { + console: 500, + network: 200, + // semantic, agent-friendly UI events + ui: 500, + }, + reportInterval: 2000, + sensitiveFields: [ + "password", + "token", + "secret", + "key", + "authorization", + "cookie", + "session", + ], + maxBodyLength: 10240, + // UI event logging privacy policy: + // - inputs matching sensitiveFields or type=password are masked by default + // - non-sensitive inputs log up to 200 chars + uiInputMaxLen: 200, + uiTextMaxLen: 80, + // Scroll throttling: minimum ms between scroll events + scrollThrottleMs: 500, + }; + + // ========================================================================== + // Storage + // ========================================================================== + const store = { + consoleLogs: [], + networkRequests: [], + uiEvents: [], + lastReportTime: Date.now(), + lastScrollTime: 0, + }; + + // ========================================================================== + // Utility Functions + // ========================================================================== + + function sanitizeValue(value, depth) { + if (depth === void 0) depth = 0; + if (depth > 5) return "[Max Depth]"; + if (value === null) return null; + if (value === undefined) return undefined; + + if (typeof value === "string") { + return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value; + } + + if (typeof value !== "object") return value; + + if (Array.isArray(value)) { + return value.slice(0, 100).map(function (v) { + return sanitizeValue(v, depth + 1); + }); + } + + var sanitized = {}; + for (var k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + var isSensitive = CONFIG.sensitiveFields.some(function (f) { + return k.toLowerCase().indexOf(f) !== -1; + }); + if (isSensitive) { + sanitized[k] = "[REDACTED]"; + } else { + sanitized[k] = sanitizeValue(value[k], depth + 1); + } + } + } + return sanitized; + } + + function formatArg(arg) { + try { + if (arg instanceof Error) { + return { type: "Error", message: arg.message, stack: arg.stack }; + } + if (typeof arg === "object") return sanitizeValue(arg); + return String(arg); + } catch (e) { + return "[Unserializable]"; + } + } + + function formatArgs(args) { + var result = []; + for (var i = 0; i < args.length; i++) result.push(formatArg(args[i])); + return result; + } + + function pruneBuffer(buffer, maxSize) { + if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize); + } + + function tryParseJson(str) { + if (typeof str !== "string") return str; + try { + return JSON.parse(str); + } catch (e) { + return str; + } + } + + // ========================================================================== + // Semantic UI Event Logging (agent-friendly) + // ========================================================================== + + function shouldIgnoreTarget(target) { + try { + if (!target || !(target instanceof Element)) return false; + return !!target.closest(".manus-no-record"); + } catch (e) { + return false; + } + } + + function compactText(s, maxLen) { + try { + var t = (s || "").trim().replace(/\s+/g, " "); + if (!t) return ""; + return t.length > maxLen ? t.slice(0, maxLen) + "…" : t; + } catch (e) { + return ""; + } + } + + function elText(el) { + try { + var t = el.innerText || el.textContent || ""; + return compactText(t, CONFIG.uiTextMaxLen); + } catch (e) { + return ""; + } + } + + function describeElement(el) { + if (!el || !(el instanceof Element)) return null; + + var getAttr = function (name) { + return el.getAttribute(name); + }; + + var tag = el.tagName ? el.tagName.toLowerCase() : null; + var id = el.id || null; + var name = getAttr("name") || null; + var role = getAttr("role") || null; + var ariaLabel = getAttr("aria-label") || null; + + var dataLoc = getAttr("data-loc") || null; + var testId = + getAttr("data-testid") || + getAttr("data-test-id") || + getAttr("data-test") || + null; + + var type = tag === "input" ? (getAttr("type") || "text") : null; + var href = tag === "a" ? getAttr("href") || null : null; + + // a small, stable hint for agents (avoid building full CSS paths) + var selectorHint = null; + if (testId) selectorHint = '[data-testid="' + testId + '"]'; + else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]'; + else if (id) selectorHint = "#" + id; + else selectorHint = tag || "unknown"; + + return { + tag: tag, + id: id, + name: name, + type: type, + role: role, + ariaLabel: ariaLabel, + testId: testId, + dataLoc: dataLoc, + href: href, + text: elText(el), + selectorHint: selectorHint, + }; + } + + function isSensitiveField(el) { + if (!el || !(el instanceof Element)) return false; + var tag = el.tagName ? el.tagName.toLowerCase() : ""; + if (tag !== "input" && tag !== "textarea") return false; + + var type = (el.getAttribute("type") || "").toLowerCase(); + if (type === "password") return true; + + var name = (el.getAttribute("name") || "").toLowerCase(); + var id = (el.id || "").toLowerCase(); + + return CONFIG.sensitiveFields.some(function (f) { + return name.indexOf(f) !== -1 || id.indexOf(f) !== -1; + }); + } + + function getInputValueSafe(el) { + if (!el || !(el instanceof Element)) return null; + var tag = el.tagName ? el.tagName.toLowerCase() : ""; + if (tag !== "input" && tag !== "textarea" && tag !== "select") return null; + + var v = ""; + try { + v = el.value != null ? String(el.value) : ""; + } catch (e) { + v = ""; + } + + if (isSensitiveField(el)) return { masked: true, length: v.length }; + + if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…"; + return v; + } + + function logUiEvent(kind, payload) { + var entry = { + timestamp: Date.now(), + kind: kind, + url: location.href, + viewport: { width: window.innerWidth, height: window.innerHeight }, + payload: sanitizeValue(payload), + }; + store.uiEvents.push(entry); + pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); + } + + function installUiEventListeners() { + // Clicks + document.addEventListener( + "click", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("click", { + target: describeElement(t), + x: e.clientX, + y: e.clientY, + }); + }, + true + ); + + // Typing "commit" events + document.addEventListener( + "change", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("change", { + target: describeElement(t), + value: getInputValueSafe(t), + }); + }, + true + ); + + document.addEventListener( + "focusin", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("focusin", { target: describeElement(t) }); + }, + true + ); + + document.addEventListener( + "focusout", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("focusout", { + target: describeElement(t), + value: getInputValueSafe(t), + }); + }, + true + ); + + // Enter/Escape are useful for form flows & modals + document.addEventListener( + "keydown", + function (e) { + if (e.key !== "Enter" && e.key !== "Escape") return; + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("keydown", { key: e.key, target: describeElement(t) }); + }, + true + ); + + // Form submissions + document.addEventListener( + "submit", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("submit", { target: describeElement(t) }); + }, + true + ); + + // Throttled scroll events + window.addEventListener( + "scroll", + function () { + var now = Date.now(); + if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return; + store.lastScrollTime = now; + + logUiEvent("scroll", { + scrollX: window.scrollX, + scrollY: window.scrollY, + documentHeight: document.documentElement.scrollHeight, + viewportHeight: window.innerHeight, + }); + }, + { passive: true } + ); + + // Navigation tracking for SPAs + function nav(reason) { + logUiEvent("navigate", { reason: reason }); + } + + var origPush = history.pushState; + history.pushState = function () { + origPush.apply(this, arguments); + nav("pushState"); + }; + + var origReplace = history.replaceState; + history.replaceState = function () { + origReplace.apply(this, arguments); + nav("replaceState"); + }; + + window.addEventListener("popstate", function () { + nav("popstate"); + }); + window.addEventListener("hashchange", function () { + nav("hashchange"); + }); + } + + // ========================================================================== + // Console Interception + // ========================================================================== + + var originalConsole = { + log: console.log.bind(console), + debug: console.debug.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), + }; + + ["log", "debug", "info", "warn", "error"].forEach(function (method) { + console[method] = function () { + var args = Array.prototype.slice.call(arguments); + + var entry = { + timestamp: Date.now(), + level: method.toUpperCase(), + args: formatArgs(args), + stack: method === "error" ? new Error().stack : null, + }; + + store.consoleLogs.push(entry); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + originalConsole[method].apply(console, args); + }; + }); + + window.addEventListener("error", function (event) { + store.consoleLogs.push({ + timestamp: Date.now(), + level: "ERROR", + args: [ + { + type: "UncaughtError", + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + stack: event.error ? event.error.stack : null, + }, + ], + stack: event.error ? event.error.stack : null, + }); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + // Mark an error moment in UI event stream for agents + logUiEvent("error", { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }); + }); + + window.addEventListener("unhandledrejection", function (event) { + var reason = event.reason; + store.consoleLogs.push({ + timestamp: Date.now(), + level: "ERROR", + args: [ + { + type: "UnhandledRejection", + reason: reason && reason.message ? reason.message : String(reason), + stack: reason && reason.stack ? reason.stack : null, + }, + ], + stack: reason && reason.stack ? reason.stack : null, + }); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + logUiEvent("unhandledrejection", { + reason: reason && reason.message ? reason.message : String(reason), + }); + }); + + // ========================================================================== + // Fetch Interception + // ========================================================================== + + var originalFetch = window.fetch.bind(window); + + window.fetch = function (input, init) { + init = init || {}; + var startTime = Date.now(); + // Handle string, Request object, or URL object + var url = typeof input === "string" + ? input + : (input && (input.url || input.href || String(input))) || ""; + var method = init.method || (input && input.method) || "GET"; + + // Don't intercept internal requests + if (url.indexOf("/__manus__/") === 0) { + return originalFetch(input, init); + } + + // Safely parse headers (avoid breaking if headers format is invalid) + var requestHeaders = {}; + try { + if (init.headers) { + requestHeaders = Object.fromEntries(new Headers(init.headers).entries()); + } + } catch (e) { + requestHeaders = { _parseError: true }; + } + + var entry = { + timestamp: startTime, + type: "fetch", + method: method.toUpperCase(), + url: url, + request: { + headers: requestHeaders, + body: init.body ? sanitizeValue(tryParseJson(init.body)) : null, + }, + response: null, + duration: null, + error: null, + }; + + return originalFetch(input, init) + .then(function (response) { + entry.duration = Date.now() - startTime; + + var contentType = (response.headers.get("content-type") || "").toLowerCase(); + var contentLength = response.headers.get("content-length"); + + entry.response = { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: null, + }; + + // Semantic network hint for agents on failures (sync, no need to wait for body) + if (response.status >= 400) { + logUiEvent("network_error", { + kind: "fetch", + method: entry.method, + url: entry.url, + status: response.status, + statusText: response.statusText, + }); + } + + // Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks + var isStreaming = contentType.indexOf("text/event-stream") !== -1 || + contentType.indexOf("application/stream") !== -1 || + contentType.indexOf("application/x-ndjson") !== -1; + if (isStreaming) { + entry.response.body = "[Streaming response - not captured]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // Skip body capture for large responses to avoid memory issues + if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) { + entry.response.body = "[Response too large: " + contentLength + " bytes]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // Skip body capture for binary content types + var isBinary = contentType.indexOf("image/") !== -1 || + contentType.indexOf("video/") !== -1 || + contentType.indexOf("audio/") !== -1 || + contentType.indexOf("application/octet-stream") !== -1 || + contentType.indexOf("application/pdf") !== -1 || + contentType.indexOf("application/zip") !== -1; + if (isBinary) { + entry.response.body = "[Binary content: " + contentType + "]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // For text responses, clone and read body in background + var clonedResponse = response.clone(); + + // Async: read body in background, don't block the response + clonedResponse + .text() + .then(function (text) { + if (text.length <= CONFIG.maxBodyLength) { + entry.response.body = sanitizeValue(tryParseJson(text)); + } else { + entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; + } + }) + .catch(function () { + entry.response.body = "[Unable to read body]"; + }) + .finally(function () { + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + }); + + // Return response immediately, don't wait for body reading + return response; + }) + .catch(function (error) { + entry.duration = Date.now() - startTime; + entry.error = { message: error.message, stack: error.stack }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + logUiEvent("network_error", { + kind: "fetch", + method: entry.method, + url: entry.url, + message: error.message, + }); + + throw error; + }); + }; + + // ========================================================================== + // XHR Interception + // ========================================================================== + + var originalXHROpen = XMLHttpRequest.prototype.open; + var originalXHRSend = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.open = function (method, url) { + this._manusData = { + method: (method || "GET").toUpperCase(), + url: url, + startTime: null, + }; + return originalXHROpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function (body) { + var xhr = this; + + if ( + xhr._manusData && + xhr._manusData.url && + xhr._manusData.url.indexOf("/__manus__/") !== 0 + ) { + xhr._manusData.startTime = Date.now(); + xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null; + + xhr.addEventListener("load", function () { + var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase(); + var responseBody = null; + + // Skip body capture for streaming responses + var isStreaming = contentType.indexOf("text/event-stream") !== -1 || + contentType.indexOf("application/stream") !== -1 || + contentType.indexOf("application/x-ndjson") !== -1; + + // Skip body capture for binary content types + var isBinary = contentType.indexOf("image/") !== -1 || + contentType.indexOf("video/") !== -1 || + contentType.indexOf("audio/") !== -1 || + contentType.indexOf("application/octet-stream") !== -1 || + contentType.indexOf("application/pdf") !== -1 || + contentType.indexOf("application/zip") !== -1; + + if (isStreaming) { + responseBody = "[Streaming response - not captured]"; + } else if (isBinary) { + responseBody = "[Binary content: " + contentType + "]"; + } else { + // Safe to read responseText for text responses + try { + var text = xhr.responseText || ""; + if (text.length > CONFIG.maxBodyLength) { + responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; + } else { + responseBody = sanitizeValue(tryParseJson(text)); + } + } catch (e) { + // responseText may throw for non-text responses + responseBody = "[Unable to read response: " + e.message + "]"; + } + } + + var entry = { + timestamp: xhr._manusData.startTime, + type: "xhr", + method: xhr._manusData.method, + url: xhr._manusData.url, + request: { body: xhr._manusData.requestBody }, + response: { + status: xhr.status, + statusText: xhr.statusText, + body: responseBody, + }, + duration: Date.now() - xhr._manusData.startTime, + error: null, + }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + if (entry.response && entry.response.status >= 400) { + logUiEvent("network_error", { + kind: "xhr", + method: entry.method, + url: entry.url, + status: entry.response.status, + statusText: entry.response.statusText, + }); + } + }); + + xhr.addEventListener("error", function () { + var entry = { + timestamp: xhr._manusData.startTime, + type: "xhr", + method: xhr._manusData.method, + url: xhr._manusData.url, + request: { body: xhr._manusData.requestBody }, + response: null, + duration: Date.now() - xhr._manusData.startTime, + error: { message: "Network error" }, + }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + logUiEvent("network_error", { + kind: "xhr", + method: entry.method, + url: entry.url, + message: "Network error", + }); + }); + } + + return originalXHRSend.apply(this, arguments); + }; + + // ========================================================================== + // Data Reporting + // ========================================================================== + + function reportLogs() { + var consoleLogs = store.consoleLogs.splice(0); + var networkRequests = store.networkRequests.splice(0); + var uiEvents = store.uiEvents.splice(0); + + // Skip if no new data + if ( + consoleLogs.length === 0 && + networkRequests.length === 0 && + uiEvents.length === 0 + ) { + return Promise.resolve(); + } + + var payload = { + timestamp: Date.now(), + consoleLogs: consoleLogs, + networkRequests: networkRequests, + // Mirror uiEvents to sessionEvents for sessionReplay.log + sessionEvents: uiEvents, + // agent-friendly semantic events + uiEvents: uiEvents, + }; + + return originalFetch(CONFIG.reportEndpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }).catch(function () { + // Put data back on failure (but respect limits) + store.consoleLogs = consoleLogs.concat(store.consoleLogs); + store.networkRequests = networkRequests.concat(store.networkRequests); + store.uiEvents = uiEvents.concat(store.uiEvents); + + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); + }); + } + + // Periodic reporting + setInterval(reportLogs, CONFIG.reportInterval); + + // Report on page unload + window.addEventListener("beforeunload", function () { + var consoleLogs = store.consoleLogs; + var networkRequests = store.networkRequests; + var uiEvents = store.uiEvents; + + if ( + consoleLogs.length === 0 && + networkRequests.length === 0 && + uiEvents.length === 0 + ) { + return; + } + + var payload = { + timestamp: Date.now(), + consoleLogs: consoleLogs, + networkRequests: networkRequests, + // Mirror uiEvents to sessionEvents for sessionReplay.log + sessionEvents: uiEvents, + uiEvents: uiEvents, + }; + + if (navigator.sendBeacon) { + var payloadStr = JSON.stringify(payload); + // sendBeacon has ~64KB limit, truncate if too large + var MAX_BEACON_SIZE = 60000; // Leave some margin + if (payloadStr.length > MAX_BEACON_SIZE) { + // Prioritize: keep recent events, drop older logs + var truncatedPayload = { + timestamp: Date.now(), + consoleLogs: consoleLogs.slice(-50), + networkRequests: networkRequests.slice(-20), + sessionEvents: uiEvents.slice(-100), + uiEvents: uiEvents.slice(-100), + _truncated: true, + }; + payloadStr = JSON.stringify(truncatedPayload); + } + navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr); + } + }); + + // ========================================================================== + // Initialization + // ========================================================================== + + // Install semantic UI listeners ASAP + try { + installUiEventListeners(); + } catch (e) { + console.warn("[Manus] Failed to install UI listeners:", e); + } + + // Mark as initialized + window.__MANUS_DEBUG_COLLECTOR__ = { + version: "2.0-no-rrweb", + store: store, + forceReport: reportLogs, + }; + + console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)"); +})(); diff --git a/client/public/__manus__/version.json b/client/public/__manus__/version.json new file mode 100644 index 0000000..2c84e70 --- /dev/null +++ b/client/public/__manus__/version.json @@ -0,0 +1,4 @@ +{ + "version": "9ae06d72", + "timestamp": 1775163491691 +} \ No newline at end of file diff --git a/client/public/map-styles/openmaptiles-positron-admin/style.json b/client/public/map-styles/openmaptiles-positron-admin/style.json new file mode 100644 index 0000000..604ae4f --- /dev/null +++ b/client/public/map-styles/openmaptiles-positron-admin/style.json @@ -0,0 +1,2375 @@ +{ + "version": 8, + "name": "OSM Bright CCDS Admin Dense", + "metadata": { + "mapbox:autocomposite": false, + "mapbox:groups": { + "101da9f13b64a08fa4b6ac1168e89e5f": { + "collapsed": false, + "name": "Places" + }, + "a14c9607bc7954ba1df7205bf660433f": { + "name": "Boundaries" + }, + "b6371a3f2f5a9932464fa3867530a2e5": { + "collapsed": false, + "name": "Transportation" + } + }, + "mapbox:type": "template", + "openmaptiles:mapbox:owner": "openmaptiles", + "openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t", + "openmaptiles:version": "3.x" + }, + "sources": { + "openmaptiles": { + "type": "vector", + "url": "https://www.portail-association973.com/data/openmaptiles.json" + } + }, + "sprite": "https://www.portail-association973.com/map-styles/openmaptiles-positron/sprite", + "glyphs": "https://www.portail-association973.com/fonts/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#f6f4ef" + } + }, + { + "id": "park", + "type": "fill", + "source": "openmaptiles", + "source-layer": "park", + "filter": [ + "==", + "$type", + "Polygon" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#e4ebdf" + } + }, + { + "id": "water", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "#c7dde7" + } + }, + { + "id": "landcover_ice_shelf", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "subclass", + "ice_shelf" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "hsl(0, 0%, 98%)", + "fill-opacity": 0.7 + } + }, + { + "id": "landcover_glacier", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "subclass", + "glacier" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "hsl(0, 0%, 98%)", + "fill-opacity": { + "base": 1, + "stops": [ + [ + 0, + 1 + ], + [ + 8, + 0.5 + ] + ] + } + } + }, + { + "id": "landuse_residential", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "maxzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "residential" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#ebe7de", + "fill-opacity": { + "base": 0.6, + "stops": [ + [ + 8, + 0.8 + ], + [ + 9, + 0.6 + ] + ] + } + } + }, + { + "id": "landcover_wood", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "minzoom": 10, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "wood" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#dbe6d3", + "fill-opacity": { + "base": 1, + "stops": [ + [ + 8, + 0 + ], + [ + 12, + 1 + ] + ] + } + } + }, + { + "id": "waterway", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "#8fb4c7" + } + }, + { + "id": "water_name", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 520, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Italic" + ], + "text-rotation-alignment": "map", + "text-size": 11.5 + }, + "paint": { + "text-color": "#6a8aa0", + "text-halo-blur": 1, + "text-halo-color": "#f6f4ef", + "text-halo-width": 1.3 + }, + "minzoom": 8 + }, + { + "id": "building", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "minzoom": 12, + "paint": { + "fill-antialias": true, + "fill-color": "#e7e1d7", + "fill-outline-color": "#d7cec2" + } + }, + { + "id": "tunnel_motorway_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "brunnel", + "tunnel" + ], + [ + "==", + "class", + "motorway" + ] + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(213, 213, 213)", + "line-opacity": 1, + "line-width": { + "base": 1.4, + "stops": [ + [ + 5.8, + 0 + ], + [ + 6, + 3 + ], + [ + 20, + 40 + ] + ] + } + } + }, + { + "id": "tunnel_motorway_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "brunnel", + "tunnel" + ], + [ + "==", + "class", + "motorway" + ] + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(234,234,234)", + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ], + [ + 20, + 30 + ] + ] + } + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": [ + "all", + [ + "in", + "class", + "taxiway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(0, 0%, 88%)", + "line-opacity": 1, + "line-width": { + "base": 1.55, + "stops": [ + [ + 13, + 1.8 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "aeroway-runway-casing", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "in", + "class", + "runway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(0, 0%, 88%)", + "line-opacity": 1, + "line-width": { + "base": 1.5, + "stops": [ + [ + 11, + 6 + ], + [ + 17, + 55 + ] + ] + } + } + }, + { + "id": "aeroway-area", + "type": "fill", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "in", + "class", + "runway", + "taxiway" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgba(255, 255, 255, 1)", + "fill-opacity": { + "base": 1, + "stops": [ + [ + 13, + 0 + ], + [ + 14, + 1 + ] + ] + } + } + }, + { + "id": "aeroway-runway", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "in", + "class", + "runway" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(255, 255, 255, 1)", + "line-opacity": 1, + "line-width": { + "base": 1.5, + "stops": [ + [ + 11, + 4 + ], + [ + 17, + 50 + ] + ] + } + } + }, + { + "id": "road_area_pier", + "type": "fill", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "pier" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "#f3f1ea" + } + }, + { + "id": "road_pier", + "type": "line", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "pier" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-color": "#f3f1ea", + "line-width": { + "base": 1.2, + "stops": [ + [ + 15, + 1 + ], + [ + 17, + 4 + ] + ] + } + } + }, + { + "id": "highway_path", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "path" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#ddd8cd", + "line-opacity": 0.9, + "line-width": { + "base": 1.2, + "stops": [ + [ + 13, + 1 + ], + [ + 20, + 10 + ] + ] + } + } + }, + { + "id": "highway_minor", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "minor", + "service", + "track" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#ebe4d8", + "line-opacity": 0.9, + "line-width": { + "base": 1.55, + "stops": [ + [ + 13, + 1.8 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "highway_major_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "#d3c7b5", + "line-dasharray": [ + 12, + 0 + ], + "line-width": { + "base": 1.3, + "stops": [ + [ + 10, + 3 + ], + [ + 20, + 23 + ] + ] + } + } + }, + { + "id": "highway_major_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fffdf8", + "line-width": { + "base": 1.3, + "stops": [ + [ + 10, + 2 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "highway_major_subtle", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "maxzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(201, 188, 168, 0.45)", + "line-width": 2 + } + }, + { + "id": "highway_motorway_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "!in", + "brunnel", + "bridge", + "tunnel" + ], + [ + "==", + "class", + "motorway" + ] + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "#c4a27d", + "line-dasharray": [ + 2, + 0 + ], + "line-opacity": 1, + "line-width": { + "base": 1.4, + "stops": [ + [ + 5.8, + 0 + ], + [ + 6, + 3 + ], + [ + 20, + 40 + ] + ] + } + } + }, + { + "id": "highway_motorway_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "!in", + "brunnel", + "bridge", + "tunnel" + ], + [ + "==", + "class", + "motorway" + ] + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": { + "base": 1, + "stops": [ + [ + 5.8, + "rgba(196, 162, 125, 0.34)" + ], + [ + 6, + "#f8f0e3" + ] + ] + }, + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ], + [ + 20, + 30 + ] + ] + } + } + }, + { + "id": "highway_motorway_subtle", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(184, 149, 110, 0.35)", + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ] + ] + } + } + }, + { + "id": "railway_transit", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "transit" + ], + [ + "!in", + "brunnel", + "tunnel" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#cdc8c1", + "line-width": 3 + } + }, + { + "id": "railway_transit_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "transit" + ], + [ + "!in", + "brunnel", + "tunnel" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fafafa", + "line-dasharray": [ + 3, + 3 + ], + "line-width": 2 + } + }, + { + "id": "railway_service", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "has", + "service" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#d7d2ca", + "line-width": 3 + } + }, + { + "id": "railway_service_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "rail" + ], + [ + "has", + "service" + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fafafa", + "line-dasharray": [ + 3, + 3 + ], + "line-width": 2 + } + }, + { + "id": "railway", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "!has", + "service" + ], + [ + "==", + "class", + "rail" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#cac6bf", + "line-width": { + "base": 1.3, + "stops": [ + [ + 16, + 3 + ], + [ + 20, + 7 + ] + ] + } + } + }, + { + "id": "railway_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "!has", + "service" + ], + [ + "==", + "class", + "rail" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fafafa", + "line-dasharray": [ + 3, + 3 + ], + "line-width": { + "base": 1.3, + "stops": [ + [ + 16, + 2 + ], + [ + 20, + 6 + ] + ] + } + } + }, + { + "id": "highway_motorway_bridge_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "brunnel", + "bridge" + ], + [ + "==", + "class", + "motorway" + ] + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "#b8956e", + "line-dasharray": [ + 2, + 0 + ], + "line-opacity": 1, + "line-width": { + "base": 1.4, + "stops": [ + [ + 5.8, + 0 + ], + [ + 6, + 5 + ], + [ + 20, + 45 + ] + ] + } + } + }, + { + "id": "highway_motorway_bridge_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "brunnel", + "bridge" + ], + [ + "==", + "class", + "motorway" + ] + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": { + "base": 1, + "stops": [ + [ + 5.8, + "rgba(184, 149, 110, 0.45)" + ], + [ + 6, + "#f6ead6" + ] + ] + }, + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ], + [ + 20, + 30 + ] + ] + } + } + }, + { + "id": "highway_name_other", + "type": "symbol", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation_name", + "filter": [ + "all", + [ + "!=", + "class", + "motorway" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": "{name:latin} {name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-max-angle": 30, + "text-pitch-alignment": "viewport", + "text-rotation-alignment": "map", + "text-size": 9.4, + "visibility": "visible", + "text-letter-spacing": 0.04 + }, + "paint": { + "text-color": "#8a8176", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.3, + "text-translate": [ + 0, + 0 + ] + }, + "minzoom": 10 + }, + { + "id": "highway_name_motorway", + "type": "symbol", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation_name", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 420, + "text-field": "{ref}", + "text-font": [ + "Noto Sans Regular" + ], + "text-pitch-alignment": "viewport", + "text-rotation-alignment": "viewport", + "text-size": 9.5, + "visibility": "visible" + }, + "paint": { + "text-color": "#7d6f61", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.3, + "text-translate": [ + 0, + 2 + ] + }, + "minzoom": 9.5 + }, + { + "id": "boundary_state", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "==", + "admin_level", + 4 + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-blur": 0.4, + "line-color": "#d1c6c8", + "line-dasharray": [ + 2, + 2 + ], + "line-opacity": 1, + "line-width": { + "base": 1.3, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 15 + ] + ] + } + } + }, + { + "id": "boundary_country_z0-4", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "maxzoom": 5, + "filter": [ + "all", + [ + "==", + "admin_level", + 2 + ], + [ + "!has", + "claimed_by" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-blur": { + "base": 1, + "stops": [ + [ + 0, + 0.4 + ], + [ + 22, + 4 + ] + ] + }, + "line-color": "#d1c6c8", + "line-opacity": 1, + "line-width": { + "base": 1.1, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 20 + ] + ] + } + } + }, + { + "id": "boundary_country_z5-", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "minzoom": 5, + "filter": [ + "==", + "admin_level", + 2 + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-blur": { + "base": 1, + "stops": [ + [ + 0, + 0.4 + ], + [ + 22, + 4 + ] + ] + }, + "line-color": "#d1c6c8", + "line-opacity": 1, + "line-width": { + "base": 1.1, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 20 + ] + ] + } + } + }, + { + "id": "place_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "in", + "class", + "continent", + "hamlet", + "neighbourhood", + "isolated_dwelling" + ], + [ + "==", + "$type", + "Point" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "center", + "text-offset": [ + 0.5, + 0 + ], + "text-size": 9.5, + "text-transform": "uppercase", + "visibility": "visible", + "text-letter-spacing": 0.1 + }, + "paint": { + "text-color": "#667483", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.3 + } + }, + { + "id": "place_suburb", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "suburb" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "center", + "text-offset": [ + 0.5, + 0 + ], + "text-size": 9.5, + "text-transform": "uppercase", + "visibility": "visible", + "text-letter-spacing": 0.1 + }, + "paint": { + "text-color": "#667483", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.3 + } + }, + { + "id": "place_village", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "village" + ] + ], + "layout": { + "icon-size": 0.4, + "text-anchor": "left", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 9.5, + "visibility": "visible", + "text-letter-spacing": 0.04 + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "#556371", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.3 + }, + "minzoom": 7.5 + }, + { + "id": "place_town", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "town" + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 8, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "visibility": "visible", + "text-letter-spacing": 0.03 + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "#556371", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.3 + }, + "minzoom": 6.5 + }, + { + "id": "place_city", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "all", + [ + "!=", + "capital", + 2 + ], + [ + "==", + "class", + "city" + ], + [ + ">", + "rank", + 3 + ] + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 8, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 11, + "visibility": "visible", + "text-letter-spacing": 0.015 + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "#344556", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.4 + }, + "minzoom": 5.8 + }, + { + "id": "place_capital", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "all", + [ + "==", + "capital", + 2 + ], + [ + "==", + "class", + "city" + ] + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "star-11" + ], + [ + 8, + "" + ] + ] + }, + "icon-size": 1, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 13.5, + "visibility": "visible", + "text-letter-spacing": 0.015 + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "#2a3b4a", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.4 + }, + "minzoom": 4 + }, + { + "id": "place_city_large", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "all", + [ + "!=", + "capital", + 2 + ], + [ + "<=", + "rank", + 3 + ], + [ + "==", + "class", + "city" + ] + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 8, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 12.5, + "visibility": "visible", + "text-letter-spacing": 0.015 + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "#2f4051", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.4 + }, + "minzoom": 4.8 + }, + { + "id": "place_state", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "state" + ] + ], + "layout": { + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible", + "text-letter-spacing": 0.04 + }, + "paint": { + "text-color": "#627181", + "text-halo-blur": 1, + "text-halo-color": "#fcfbf8", + "text-halo-width": 1.4 + }, + "minzoom": 3.8 + }, + { + "id": "place_country_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + "!has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Italic" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 9 + ], + [ + 6, + 11 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": { + "base": 1, + "stops": [ + [ + 3, + "#8ea0af" + ], + [ + 4, + "#7a8b9a" + ] + ] + }, + "text-halo-color": "rgba(248,246,239,0.85)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_minor", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + ">=", + "rank", + 2 + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 10 + ], + [ + 6, + 12 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": { + "base": 1, + "stops": [ + [ + 3, + "#8799aa" + ], + [ + 4, + "#718495" + ] + ] + }, + "text-halo-color": "rgba(248,246,239,0.85)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_major", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "<=", + "rank", + 1 + ], + [ + "==", + "class", + "country" + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-size": { + "base": 1.4, + "stops": [ + [ + 0, + 10 + ], + [ + 3, + 12 + ], + [ + 4, + 14 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": { + "base": 1, + "stops": [ + [ + 3, + "#708497" + ], + [ + 4, + "#5d7184" + ] + ] + }, + "text-halo-color": "rgba(248,246,239,0.9)", + "text-halo-width": 1.4 + } + } + ], + "id": "positron" +} diff --git a/client/public/map-styles/openmaptiles-positron/sprite.json b/client/public/map-styles/openmaptiles-positron/sprite.json new file mode 100644 index 0000000..2d20beb --- /dev/null +++ b/client/public/map-styles/openmaptiles-positron/sprite.json @@ -0,0 +1,16 @@ +{ + "circle-11": { + "height": 17, + "pixelRatio": 1, + "width": 17, + "x": 0, + "y": 0 + }, + "star-11": { + "height": 17, + "pixelRatio": 1, + "width": 17, + "x": 17, + "y": 0 + } +} \ No newline at end of file diff --git a/client/public/map-styles/openmaptiles-positron/sprite.png b/client/public/map-styles/openmaptiles-positron/sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..35398342435808247b8a8b53be62d35c9a803fdd GIT binary patch literal 529 zcmV+s0`C2ZP)W3Ri;GwJ`ue_eadG(vqCtF+I7prpv*yj4R|GWU@18w-VqU*~ z%}bOG!Z0v2jMLK64uJILvol9eEJ*z|*=kD(Q16drT4x}Ed zLqHPH=)ma^p!{2027LYcReH;oEeS-J@eZU8q#mn7KnW9+Ch!>ok_V~7Y5>GBGWZ;W z6d=F!1f$H@VfTS7%_;4-yB-g36jp`dKLVZ(-`sHmuGAaRg9NF71PP*So2i*(89)2CN=baeCp^_jzpc8~`^ zrEGI^^JI`bsOTr?5J~{BkP-wr99Uo}uUfT=8Ch=s{{1XKaTSm}NF7ZbGUNdOz={fQ T5D+`700000NkvXXu0mjf5={7) literal 0 HcmV?d00001 diff --git a/client/public/map-styles/openmaptiles-positron/sprite@2x.json b/client/public/map-styles/openmaptiles-positron/sprite@2x.json new file mode 100644 index 0000000..498b044 --- /dev/null +++ b/client/public/map-styles/openmaptiles-positron/sprite@2x.json @@ -0,0 +1,16 @@ +{ + "circle-11": { + "height": 34, + "pixelRatio": 2, + "width": 34, + "x": 0, + "y": 0 + }, + "star-11": { + "height": 34, + "pixelRatio": 2, + "width": 34, + "x": 34, + "y": 0 + } +} \ No newline at end of file diff --git a/client/public/map-styles/openmaptiles-positron/sprite@2x.png b/client/public/map-styles/openmaptiles-positron/sprite@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1939a3b759c23d6f9bda5f1496861ace5a5bfad9 GIT binary patch literal 921 zcmV;K17`e*P)6(!-EQ~ML=DL0a_4A8ZV~Z)T9GBmm=!$Z zH@riiN!iosbnZfk&CSjC*=#m|=W4Zj#c<-?-QBY%CntC4!=LeL3~T6vz8GV)O~kbf z&Ojhgip64S$xG};^4bm$4=)n&Jb&=c&d!^Sjg398*IRcwoxjlVjNkB1Cd@p>8f90j z)w&6u?d|QT!{PYj@pziWV$lPSHH=ph1EH~+PN!dmLZJf4M>X_8UrCfDql_a@cnA(! zhzxTl5{VD^jY8?3a6-u?|9J*Izs3rT6N4N2VhnqV=1sZo_qklIF7x_)zRJ*A~|iy1MC@E#E^@^o`SiwaMx4h!91H7=YPHbr6n^ zk8krKQ7t#0gD_SPx9QvNAkcAqPOFDRqtVaOQGQOBAZ8!Tlt?C%XZU98_xsCg`O#P6 zU&b7|L^1Q|u0*|Fzcwmd+-`SgT-aa^U4l&0=hylM1#VG zy@K--5;l7g_oYkTPvK$1Ftdax4;Q6Z&{qCHUTK$?4d~eskH| z-H0MH&ArHiD7<2pUe74|^WyPAE|bZ;L>Niq7Dd`-3Y3K+rB@6?rBb", + "rank", + 3 + ] + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 8, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 12.5, + "visibility": "visible", + "text-letter-spacing": 0.01 + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "#334454", + "text-halo-blur": 1, + "text-halo-color": "#f8f6ef", + "text-halo-width": 1.5 + }, + "minzoom": 5.2 + }, + { + "id": "place_capital", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "all", + [ + "==", + "capital", + 2 + ], + [ + "==", + "class", + "city" + ] + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "star-11" + ], + [ + 8, + "" + ] + ] + }, + "icon-size": 1, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 15.5, + "visibility": "visible", + "text-letter-spacing": 0.01 + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "#273a4a", + "text-halo-blur": 1, + "text-halo-color": "#f8f6ef", + "text-halo-width": 1.5 + }, + "minzoom": 3.5 + }, + { + "id": "place_city_large", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "all", + [ + "!=", + "capital", + 2 + ], + [ + "<=", + "rank", + 3 + ], + [ + "==", + "class", + "city" + ] + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 8, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 14.5, + "visibility": "visible", + "text-letter-spacing": 0.01 + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "#2e4051", + "text-halo-blur": 1, + "text-halo-color": "#f8f6ef", + "text-halo-width": 1.5 + }, + "minzoom": 4.2 + }, + { + "id": "place_state", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "state" + ] + ], + "layout": { + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-size": 10.8, + "text-transform": "uppercase", + "visibility": "visible", + "text-letter-spacing": 0.03 + }, + "paint": { + "text-color": "#526273", + "text-halo-blur": 1, + "text-halo-color": "#f8f6ef", + "text-halo-width": 1.5 + }, + "minzoom": 3.2 + }, + { + "id": "place_country_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + "!has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Italic" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 9 + ], + [ + 6, + 11 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": { + "base": 1, + "stops": [ + [ + 3, + "#8092a1" + ], + [ + 4, + "#6d7d8d" + ] + ] + }, + "text-halo-color": "rgba(248,246,239,0.85)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_minor", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + ">=", + "rank", + 2 + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 10 + ], + [ + 6, + 12 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": { + "base": 1, + "stops": [ + [ + 3, + "#7b8d9d" + ], + [ + 4, + "#67798a" + ] + ] + }, + "text-halo-color": "rgba(248,246,239,0.85)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_major", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "<=", + "rank", + 1 + ], + [ + "==", + "class", + "country" + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Regular" + ], + "text-size": { + "base": 1.4, + "stops": [ + [ + 0, + 10 + ], + [ + 3, + 12 + ], + [ + 4, + 14 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": { + "base": 1, + "stops": [ + [ + 3, + "#667a8f" + ], + [ + 4, + "#52667c" + ] + ] + }, + "text-halo-color": "rgba(248,246,239,0.9)", + "text-halo-width": 1.4 + } + } + ], + "id": "positron" +} diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..484c3c0 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,266 @@ +import { Suspense, lazy, type ComponentType, useEffect } from "react"; +import { Route, Switch, useLocation } from "wouter"; + +import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import ErrorBoundary from "./components/ErrorBoundary"; +import DashboardLayout from "./components/DashboardLayout"; +import { ThemeProvider, useTheme } from "./contexts/ThemeContext"; +import { + getBackgroundScopeAliases, + getBackgroundScopeForPath, + isDecorationScopeActive, + resolveBackgroundBlurForScope, + resolveBackgroundImageForScope, + resolveBackgroundOverlayOpacityForScope, + resolveBackgroundPositionForScope, + resolveBackgroundPresentationModeForScope, +} from "./lib/appearanceScopes"; + +const UserOperatingTour = lazy(() => import("@/components/UserOperatingTour")); +const NotFound = lazy(() => import("@/pages/NotFound")); +const Home = lazy(() => import("@/pages/Home")); +const Dashboard = lazy(() => import("@/pages/Dashboard")); +const Profile = lazy(() => import("@/pages/Profile")); +const Documents = lazy(() => import("@/pages/Documents")); +const NewRequest = lazy(() => import("@/pages/NewRequest")); +const Requests = lazy(() => import("@/pages/Requests")); +const RequestDetail = lazy(() => import("@/pages/RequestDetail")); +const EditRequest = lazy(() => import("@/pages/EditRequest")); +const Admin = lazy(() => import("@/pages/Admin")); +const FAQ = lazy(() => import("@/pages/FAQ")); +const ReservationSalle = lazy(() => import("@/pages/ReservationSalle")); +const Login = lazy(() => import("@/pages/Login")); +const DemandeMaterielEvenementiel = lazy(() => import("@/pages/DemandeMaterielEvenementiel")); +const AssociationDirectoryPortal = lazy(() => import("@/pages/AssociationDirectoryPortal")); +const AssociationInvitation = lazy(() => import("@/pages/AssociationInvitation")); +const AssociationMapPublic = lazy(() => import("@/pages/AssociationMapPublic")); +const AssociationPublicProfile = lazy(() => import("@/pages/AssociationPublicProfile")); +const MaterialReturnUpload = lazy(() => import("@/pages/MaterialReturnUpload")); +const MaterialReturnFinalPdfView = lazy(() => import("@/pages/MaterialReturnFinalPdfView")); +const MaterialConventionDownload = lazy(() => import("@/pages/MaterialConventionDownload")); +const SalleWorkflowDocumentDownload = lazy(() => import("@/pages/SalleWorkflowDocumentDownload")); +const StatsReportArchiveView = lazy(() => import("@/pages/StatsReportArchiveView")); +const TerrainMissions = lazy(() => import("@/pages/TerrainMissions")); +const PrivacyPolicy = lazy(() => import("@/pages/PrivacyPolicy")); +const LegalNotice = lazy(() => import("@/pages/LegalNotice")); +const CookiesPolicy = lazy(() => import("@/pages/CookiesPolicy")); +const TermsOfService = lazy(() => import("@/pages/TermsOfService")); + +function PageLoader({ compact = false }: { compact?: boolean }) { + return ( + + + + +`; +} + +function renderPage(title: string, message: string, type: "success" | "error" | "warning"): string { + const colors = { + success: { bg: '#B8FEC9', border: '#18753C', icon: '✅' }, + error: { bg: '#FFE9E6', border: '#CE0500', icon: '❌' }, + warning: { bg: '#FEF7DA', border: '#D64D00', icon: '⚠️' }, + }; + const c = colors[type]; + + return ` + + + + + ${title} - Portail Associations + + + +
+
+

Portail Associations

+
+
+
${c.icon}
+
+

${escapeHtml(title)}

+

${escapeHtml(message)}

+
+ Retour au portail +
+
+ +`; +} + +function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/server/emailRecap.ts b/server/emailRecap.ts new file mode 100644 index 0000000..532d025 --- /dev/null +++ b/server/emailRecap.ts @@ -0,0 +1,343 @@ +/** + * Génère un récapitulatif complet d'une demande traitée pour diffusion interne. + */ + +const requestTypeLabels: Record = { + subvention_fonctionnement: "Subvention de fonctionnement", + subvention_projet: "Subvention de projet", + agrement_jeunesse_education: "Agrément Jeunesse et Éducation Populaire", + agrement_sport: "Agrément Sport", + autorisation_occupation: "Autorisation d'occupation", + demande_salle: "Demande de salle / local", + demande_materiel_evenementiel: "Demande de matériel événementiel", + autre: "Autre demande", +}; + +const statusLabels: Record = { + validee: "Validée", + refusee: "Refusée", + en_cours_traitement: "En cours de traitement", + information_complementaire: "Information complémentaire demandée", +}; + +const statusBadgeStyles: Record = { + validee: { label: "VALIDÉE", bg: "#ecfdf3", border: "#a7f3d0", text: "#166534" }, + refusee: { label: "REFUSÉE", bg: "#fef2f2", border: "#fecaca", text: "#b91c1c" }, + en_cours_traitement: { label: "EN COURS", bg: "#eff6ff", border: "#bfdbfe", text: "#1d4ed8" }, + information_complementaire: { label: "INFO REQUISE", bg: "#fff7ed", border: "#fed7aa", text: "#c2410c" }, +}; + +interface RecapData { + request: { + id: number; + titre: string; + type: string; + status: string; + description: string | null; + montantDemande: number | null; + montantAccorde: number | null; + commentaireAdmin: string | null; + formData: string | null; + dateSubmission: Date | null; + dateTraitement: Date | null; + createdAt: Date | null; + }; + association: { + nomAssociation: string; + siret: string | null; + adresse: string | null; + codePostal: string | null; + ville: string | null; + telephone: string | null; + emailContact: string | null; + nomRepresentant: string | null; + } | null; + traitePar: string; + documents?: { name: string; type: string }[]; + serviceLabel?: string | null; +} + +function formatDateTimeFr(date: Date | string | null | undefined) { + if (!date) return "Non renseignée"; + return new Date(date).toLocaleString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatDateFr(date: string | null | undefined) { + if (!date) return "Non renseignée"; + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +} + +function formatAmount(amount: number | null | undefined) { + if (amount == null) return "Non renseigné"; + return `${(amount / 100).toLocaleString("fr-FR")} €`; +} + +function escapeHtml(value: string) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function parseRequestHighlights(requestType: string, formData: any): Array<{ label: string; value: string }> { + if (!formData || typeof formData !== "object") return []; + + if (requestType === "demande_salle") { + const salles = Array.isArray(formData.sallesNoms) + ? formData.sallesNoms.join(", ") + : Array.isArray(formData.sallesSelectionnees) + ? formData.sallesSelectionnees.join(", ") + : ""; + + const highlights = [ + salles ? { label: "Salle(s)", value: salles } : null, + formData.dateReservation ? { label: "Date de début", value: formatDateFr(formData.dateReservation) } : null, + formData.dateFinReservation ? { label: "Date de fin", value: formatDateFr(formData.dateFinReservation) } : null, + formData.heureDebut || formData.heureFin + ? { label: "Créneau", value: `${formData.heureDebut || "?"} - ${formData.heureFin || "?"}` } + : null, + formData.nombreParticipants ? { label: "Participants", value: String(formData.nombreParticipants) } : null, + formData.motifReservation ? { label: "Motif", value: String(formData.motifReservation) } : null, + ]; + return highlights.filter(Boolean) as Array<{ label: string; value: string }>; + } + + if (requestType === "demande_materiel_evenementiel") { + const highlights = [ + formData.commune ? { label: "Commune", value: String(formData.commune) } : null, + formData.dateDebutManifestation || formData.dateManifestation + ? { label: "Début manifestation", value: formatDateFr(formData.dateDebutManifestation || formData.dateManifestation) } + : null, + formData.dateFinManifestation + ? { label: "Fin manifestation", value: formatDateFr(formData.dateFinManifestation) } + : null, + formData.dateRestitution ? { label: "Restitution", value: formatDateFr(formData.dateRestitution) } : null, + formData.motifDemande ? { label: "Motif", value: String(formData.motifDemande) } : null, + ]; + return highlights.filter(Boolean) as Array<{ label: string; value: string }>; + } + + return [ + formData.dateDebut ? { label: "Date de début", value: formatDateFr(formData.dateDebut) } : null, + formData.dateFin ? { label: "Date de fin", value: formatDateFr(formData.dateFin) } : null, + formData.objetProjet ? { label: "Objet", value: String(formData.objetProjet) } : null, + ].filter(Boolean) as Array<{ label: string; value: string }>; +} + +export function generateRequestRecapEmail(data: RecapData): { title: string; content: string; html: string } { + const { request, association, traitePar, documents, serviceLabel } = data; + const typeLabel = requestTypeLabels[request.type] || request.type; + const statusLabel = statusLabels[request.status] || request.status; + const badge = statusBadgeStyles[request.status] || { + label: statusLabel.toUpperCase(), + bg: "#f3f4f6", + border: "#d1d5db", + text: "#374151", + }; + + let formData: any = {}; + try { + formData = request.formData ? JSON.parse(request.formData) : {}; + } catch { + formData = {}; + } + + const highlights = parseRequestHighlights(request.type, formData); + const subject = `Demande traitée #${request.id} - ${statusLabel} - ${request.titre}`; + + const textSections: string[] = [ + "Récapitulatif de demande traitée", + "", + `Demande n°${request.id} - ${statusLabel}`, + `Type : ${typeLabel}`, + `Association : ${association?.nomAssociation || "Non renseignée"}`, + `Traitée par : ${traitePar}`, + `Date de traitement : ${formatDateTimeFr(request.dateTraitement)}`, + serviceLabel ? `Service concerné : ${serviceLabel}` : "", + request.commentaireAdmin ? `Commentaire : ${request.commentaireAdmin}` : "", + request.montantAccorde != null ? `Montant accordé : ${formatAmount(request.montantAccorde)}` : "", + "", + "Informations de l'association", + `- Nom : ${association?.nomAssociation || "Non renseigné"}`, + association?.siret ? `- SIRET : ${association.siret}` : "", + association?.adresse ? `- Adresse : ${association.adresse}${association.codePostal ? `, ${association.codePostal}` : ""}${association.ville ? ` ${association.ville}` : ""}` : "", + association?.nomRepresentant ? `- Représentant : ${association.nomRepresentant}` : "", + association?.telephone ? `- Téléphone : ${association.telephone}` : "", + association?.emailContact ? `- Email : ${association.emailContact}` : "", + "", + "Points clés du dossier", + ...highlights.map((item) => `- ${item.label} : ${item.value}`), + request.description ? `- Description : ${request.description}` : "", + request.montantDemande != null ? `- Montant demandé : ${formatAmount(request.montantDemande)}` : "", + ]; + + if (documents?.length) { + textSections.push("", "Documents joints au dossier"); + textSections.push(...documents.map((doc, index) => `${index + 1}. ${doc.name} (${doc.type})`)); + } + + textSections.push( + "", + "Le PDF récapitulatif est joint à cet email lorsqu'il est disponible pour ce type de demande.", + "", + "Ce message a été généré automatiquement par le Portail Associations." + ); + + const htmlHighlights = highlights + .map( + (item) => ` + + ${escapeHtml(item.label)} + ${escapeHtml(item.value)} + ` + ) + .join(""); + + const htmlDocuments = documents?.length + ? ` +
+

Documents du dossier

+
    + ${documents.map((doc) => `
  • ${escapeHtml(doc.name)} (${escapeHtml(doc.type)})
  • `).join("")} +
+
` + : ""; + + const html = ` +
+
+
+
Portail Associations
+

Transmission d'une demande traitée

+

+ Ce message reprend les informations essentielles du dossier et le PDF récapitulatif est joint lorsqu'il est disponible. +

+
+ +
+
+ ${escapeHtml(badge.label)} +
+ +

${escapeHtml(request.titre)}

+

+ Demande #${request.id} • ${escapeHtml(typeLabel)} +

+ +
+

Décision et transmission

+ + + + + + + + + + ${serviceLabel ? ` + + + + ` : ""} + ${request.montantAccorde != null ? ` + + + + ` : ""} +
Traitée par${escapeHtml(traitePar)}
Date de traitement${escapeHtml(formatDateTimeFr(request.dateTraitement))}
Service concerné${escapeHtml(serviceLabel)}
Montant accordé${escapeHtml(formatAmount(request.montantAccorde))}
+ ${request.commentaireAdmin ? ` +
+
Commentaire administratif
+
${escapeHtml(request.commentaireAdmin).replace(/\n/g, "
")}
+
` : ""} +
+ +
+

Association

+ + + + + + ${association?.siret ? ` + + + + ` : ""} + ${(association?.adresse || association?.ville || association?.codePostal) ? ` + + + + ` : ""} + ${association?.nomRepresentant ? ` + + + + ` : ""} + ${association?.telephone ? ` + + + + ` : ""} + ${association?.emailContact ? ` + + + + ` : ""} +
Nom${escapeHtml(association?.nomAssociation || "Non renseignée")}
SIRET${escapeHtml(association.siret)}
Adresse${escapeHtml( + [association?.adresse, association?.codePostal, association?.ville].filter(Boolean).join(" ") + )}
Représentant${escapeHtml(association.nomRepresentant)}
Téléphone${escapeHtml(association.telephone)}
Email${escapeHtml(association.emailContact)}
+
+ +
+

Points clés du dossier

+ + + + + + ${request.montantDemande != null ? ` + + + + ` : ""} + ${htmlHighlights} + ${request.description ? ` + + + + ` : ""} +
Date de soumission${escapeHtml(formatDateTimeFr(request.dateSubmission))}
Montant demandé${escapeHtml(formatAmount(request.montantDemande))}
Description${escapeHtml(request.description).replace(/\n/g, "
")}
+
+ + ${htmlDocuments} + +
+

+ Le PDF récapitulatif est joint à cet email lorsqu’il est disponible pour ce type de demande. Tu peux l’utiliser pour la transmission interne, l’archivage ou l’impression. +

+
+
+
+
+ `; + + return { + title: subject, + content: textSections.filter(Boolean).join("\n"), + html, + }; +} diff --git a/server/internalAccess.ts b/server/internalAccess.ts new file mode 100644 index 0000000..01f0835 --- /dev/null +++ b/server/internalAccess.ts @@ -0,0 +1,108 @@ +import type { User } from "../drizzle/schema"; +import * as db from "./db"; +import { userHasLogisticsAccess } from "./logisticsGroup"; +import { + getDelegatedSignerIdsForDirectrice, + getDirectriceIdsDelegatingToUser, +} from "./salleSignatureDelegation"; + +export type AuthenticatedPortalUser = User & { + canManageLogistics: boolean; + canSignSalle: boolean; + salleSignatureDelegatedByUserIds: number[]; +}; + +type MinimalUser = Pick; + +export async function userHasSalleSignatureAccess(user: Pick) { + if (user.role === "super_admin" || user.role === "directrice") { + return true; + } + + const [internalUsers, delegatedBySetting] = await Promise.all([ + db.getAdminUsers(), + getDirectriceIdsDelegatingToUser(user.id), + ]); + + return internalUsers.some((entry) => { + if (entry.role !== "directrice" || !entry.isActive) return false; + return entry.delegatedSalleSignerUserId === user.id || delegatedBySetting.includes(entry.id); + }); +} + +export async function getSalleSignatureDelegatedByUserIds(userId: number) { + const [internalUsers, delegatedBySetting] = await Promise.all([ + db.getAdminUsers(), + getDirectriceIdsDelegatingToUser(userId), + ]); + + return Array.from( + new Set( + internalUsers + .filter( + (entry) => + entry.role === "directrice" + && entry.isActive + && entry.delegatedSalleSignerUserId === userId + ) + .map((entry) => entry.id) + .concat(delegatedBySetting) + ) + ); +} + +export async function withEffectiveInternalAccess( + user: TUser +): Promise<(AuthenticatedPortalUser & NonNullable) | null> { + if (!user) return null; + + const [effectiveLogisticsAccess, effectiveSalleSignatureAccess, delegatedByUserIds] = + await Promise.all([ + userHasLogisticsAccess(user as MinimalUser), + userHasSalleSignatureAccess(user), + getSalleSignatureDelegatedByUserIds(user.id), + ]); + + return { + ...user, + canManageLogistics: effectiveLogisticsAccess, + canSignSalle: effectiveSalleSignatureAccess, + salleSignatureDelegatedByUserIds: delegatedByUserIds, + } as AuthenticatedPortalUser & NonNullable; +} + +export async function getSalleSignatureDelegateCandidates(currentUserId: number) { + const internalUsers = await db.getAdminUsers(); + return internalUsers + .filter( + (entry) => + entry.isActive + && entry.id !== currentUserId + && ( + entry.role === "accueil" + || entry.role === "admin" + || entry.role === "directrice" + || entry.role === "super_admin" + ) + ) + .map((entry) => ({ + id: entry.id, + name: entry.name || "Sans nom", + email: entry.email || "", + role: entry.role, + })); +} + +export async function getSalleSignatureDelegates(directriceUserId: number) { + const delegateIds = await getDelegatedSignerIdsForDirectrice(directriceUserId); + const delegates = await Promise.all(delegateIds.map((id) => db.getUserById(id))); + + return delegates + .filter((entry): entry is NonNullable => Boolean(entry?.isActive)) + .map((entry) => ({ + id: entry.id, + name: entry.name || "Sans nom", + email: entry.email || "", + role: entry.role, + })); +} diff --git a/server/logisticsGroup.ts b/server/logisticsGroup.ts new file mode 100644 index 0000000..477731b --- /dev/null +++ b/server/logisticsGroup.ts @@ -0,0 +1,82 @@ +import type { User } from "../drizzle/schema"; +import * as db from "./db"; + +export const LOGISTICS_GROUP_SETTING_KEY = "system.logistics.group"; + +export type LogisticsGroupSettings = { + label: string; + memberUserIds: number[]; + updatedAt: string | null; +}; + +const defaultLogisticsGroup: LogisticsGroupSettings = { + label: "Groupe interne matériel CCDS - logistique et contrôle", + memberUserIds: [], + updatedAt: null, +}; + +function sanitizeMemberUserIds(value: unknown) { + if (!Array.isArray(value)) return []; + return Array.from( + new Set( + value + .map((entry) => Number(entry)) + .filter((entry) => Number.isInteger(entry) && entry > 0) + ) + ); +} + +export async function getLogisticsGroupSettings(): Promise { + const storedValue = await db.getPortalSetting(LOGISTICS_GROUP_SETTING_KEY); + if (!storedValue) return defaultLogisticsGroup; + + try { + const parsed = JSON.parse(storedValue) as Partial; + return { + label: typeof parsed.label === "string" && parsed.label.trim() + ? parsed.label.trim() + : defaultLogisticsGroup.label, + memberUserIds: sanitizeMemberUserIds(parsed.memberUserIds), + updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : null, + }; + } catch { + return defaultLogisticsGroup; + } +} + +export async function saveLogisticsGroupSettings(input: { + label?: string; + memberUserIds?: number[]; +}) { + const nextValue: LogisticsGroupSettings = { + label: input.label?.trim() || defaultLogisticsGroup.label, + memberUserIds: sanitizeMemberUserIds(input.memberUserIds), + updatedAt: new Date().toISOString(), + }; + + await db.setPortalSetting( + LOGISTICS_GROUP_SETTING_KEY, + JSON.stringify(nextValue), + "Groupe interne à accès limité autorisé à traiter les demandes de matériel CCDS, piloter les restitutions et contrôler les retours" + ); + + return nextValue; +} + +export async function userHasLogisticsAccess(user: Pick) { + if (user.role === "super_admin" || user.canManageLogistics) { + return true; + } + + const group = await getLogisticsGroupSettings(); + return group.memberUserIds.includes(user.id); +} + +export async function withEffectiveLogisticsAccess(user: TUser): Promise { + if (!user) return user; + const effectiveAccess = await userHasLogisticsAccess(user); + return { + ...user, + canManageLogistics: effectiveAccess, + } as TUser; +} diff --git a/server/logisticsSettings.ts b/server/logisticsSettings.ts new file mode 100644 index 0000000..594b492 --- /dev/null +++ b/server/logisticsSettings.ts @@ -0,0 +1,161 @@ +import * as db from "./db"; +import { + materialEventInventory, + materialEventItems, + materialEventReplacementValues, + type MaterialEventItemKey, +} from "@shared/materialEvent"; + +export const LOGISTICS_SETTINGS_KEY = "system.logistics.settings"; + +export type LogisticsSettings = { + materialReturnLeadDays: number; + materialReturnGraceDays: number; + inventory: Record; + replacementValues: Record; + updatedAt: string | null; +}; + +export const defaultLogisticsSettings: Omit = { + materialReturnLeadDays: 4, + materialReturnGraceDays: 3, + inventory: { + ...materialEventInventory, + }, + replacementValues: { + ...materialEventReplacementValues, + }, +}; + +function clampWholeNumber(value: unknown, fallback: number, min: number, max: number) { + const parsed = Number.parseInt(String(value ?? ""), 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.min(max, Math.max(min, parsed)); +} + +function sanitizeInventory(rawInventory: unknown) { + const nextInventory = { ...defaultLogisticsSettings.inventory } as Record; + + if (!rawInventory || typeof rawInventory !== "object") { + return nextInventory; + } + + for (const item of materialEventItems) { + const rawValue = (rawInventory as Record)[item.key]; + if (rawValue === null || rawValue === "") { + nextInventory[item.key] = null; + continue; + } + + const parsed = Number.parseInt(String(rawValue ?? ""), 10); + if (Number.isFinite(parsed) && parsed >= 0) { + nextInventory[item.key] = parsed; + } + } + + return nextInventory; +} + +function sanitizeReplacementValues(rawReplacementValues: unknown) { + const nextReplacementValues = { + ...defaultLogisticsSettings.replacementValues, + } as Record; + + if (!rawReplacementValues || typeof rawReplacementValues !== "object") { + return nextReplacementValues; + } + + for (const item of materialEventItems) { + const rawValue = (rawReplacementValues as Record)[item.key]; + if (rawValue === null || rawValue === "") { + nextReplacementValues[item.key] = null; + continue; + } + + const parsed = Number.parseInt(String(rawValue ?? ""), 10); + if (Number.isFinite(parsed) && parsed >= 0) { + nextReplacementValues[item.key] = parsed; + } + } + + return nextReplacementValues; +} + +function sanitizeLogisticsSettings(rawValue: unknown, updatedAt: string | null): LogisticsSettings { + const source = rawValue && typeof rawValue === "object" ? rawValue as Record : {}; + + return { + materialReturnLeadDays: clampWholeNumber( + source.materialReturnLeadDays, + defaultLogisticsSettings.materialReturnLeadDays, + 1, + 30 + ), + materialReturnGraceDays: clampWholeNumber( + source.materialReturnGraceDays, + defaultLogisticsSettings.materialReturnGraceDays, + 0, + 30 + ), + inventory: sanitizeInventory(source.inventory), + replacementValues: sanitizeReplacementValues(source.replacementValues), + updatedAt, + }; +} + +export async function getLogisticsSettings(): Promise { + const record = await db.getPortalSettingRecord(LOGISTICS_SETTINGS_KEY); + if (!record?.valeur) { + return { + ...defaultLogisticsSettings, + updatedAt: null, + }; + } + + try { + const parsed = JSON.parse(record.valeur); + return sanitizeLogisticsSettings(parsed, record.updatedAt?.toISOString?.() || null); + } catch { + return { + ...defaultLogisticsSettings, + updatedAt: record.updatedAt?.toISOString?.() || null, + }; + } +} + +export async function saveLogisticsSettings(input: { + materialReturnLeadDays?: number; + materialReturnGraceDays?: number; + inventory?: Partial>; + replacementValues?: Partial>; +}) { + const current = await getLogisticsSettings(); + const next = sanitizeLogisticsSettings( + { + materialReturnLeadDays: input.materialReturnLeadDays ?? current.materialReturnLeadDays, + materialReturnGraceDays: input.materialReturnGraceDays ?? current.materialReturnGraceDays, + inventory: { + ...current.inventory, + ...(input.inventory || {}), + }, + replacementValues: { + ...current.replacementValues, + ...(input.replacementValues || {}), + }, + }, + new Date().toISOString() + ); + + await db.setPortalSetting( + LOGISTICS_SETTINGS_KEY, + JSON.stringify({ + materialReturnLeadDays: next.materialReturnLeadDays, + materialReturnGraceDays: next.materialReturnGraceDays, + inventory: next.inventory, + replacementValues: next.replacementValues, + }), + "Réglages logistiques globaux du portail" + ); + + return getLogisticsSettings(); +} diff --git a/server/mailSettings.ts b/server/mailSettings.ts new file mode 100644 index 0000000..56055d8 --- /dev/null +++ b/server/mailSettings.ts @@ -0,0 +1,256 @@ +import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto"; +import * as db from "./db"; +import { ENV } from "./_core/env"; +import { providerDefaults, resolveMailConfig, type MailEnvInput, type SupportedMailProvider } from "@shared/mailProviders"; + +export const MAIL_SETTINGS_KEY = "system.mail.settings"; + +type StoredMailSettings = { + smtpProvider: SupportedMailProvider; + smtpHost?: string; + smtpPort?: string; + smtpUser?: string; + smtpFrom?: string; + smtpSecure?: boolean; + smtpRequireTls?: boolean; + smtpPassEncrypted?: string; + updatedByUserId?: number; + updatedByUserName?: string | null; +}; + +export type MailSettingsFormInput = { + smtpProvider: SupportedMailProvider; + smtpHost?: string; + smtpPort?: string; + smtpUser?: string; + smtpFrom?: string; + smtpSecure?: boolean; + smtpRequireTls?: boolean; + smtpPass?: string; +}; + +export type AdminMailSettings = { + smtpProvider: SupportedMailProvider; + smtpHost: string; + smtpPort: string; + smtpUser: string; + smtpFrom: string; + smtpSecure: boolean; + smtpRequireTls: boolean; + smtpPassConfigured: boolean; + ready: boolean; + missing: string[]; + source: "environment" | "admin" | "admin+environment"; + hasSavedConfig: boolean; + updatedAt: string | null; + updatedByUserId: number | null; + updatedByUserName: string | null; +}; + +function buildCipherKey() { + return createHash("sha256") + .update(ENV.cookieSecret || "local-dev-mail-settings") + .digest(); +} + +function encryptSecret(value: string) { + const iv = randomBytes(12); + const key = buildCipherKey(); + const cipher = createCipheriv("aes-256-gcm", key, iv); + const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]); + const tag = cipher.getAuthTag(); + return `${iv.toString("base64")}.${tag.toString("base64")}.${encrypted.toString("base64")}`; +} + +function decryptSecret(value?: string) { + if (!value) return ""; + + const [ivBase64, tagBase64, encryptedBase64] = value.split("."); + if (!ivBase64 || !tagBase64 || !encryptedBase64) { + return ""; + } + + try { + const key = buildCipherKey(); + const decipher = createDecipheriv( + "aes-256-gcm", + key, + Buffer.from(ivBase64, "base64") + ); + decipher.setAuthTag(Buffer.from(tagBase64, "base64")); + const decrypted = Buffer.concat([ + decipher.update(Buffer.from(encryptedBase64, "base64")), + decipher.final(), + ]); + return decrypted.toString("utf8"); + } catch { + return ""; + } +} + +function trimOrEmpty(value?: string | null) { + return String(value || "").trim(); +} + +function normalizeStoredSettings(input: Partial | null | undefined): StoredMailSettings | null { + if (!input?.smtpProvider) return null; + + return { + smtpProvider: input.smtpProvider, + smtpHost: trimOrEmpty(input.smtpHost), + smtpPort: trimOrEmpty(input.smtpPort), + smtpUser: trimOrEmpty(input.smtpUser), + smtpFrom: trimOrEmpty(input.smtpFrom), + smtpSecure: Boolean(input.smtpSecure), + smtpRequireTls: Boolean(input.smtpRequireTls), + smtpPassEncrypted: trimOrEmpty(input.smtpPassEncrypted), + updatedByUserId: input.updatedByUserId ? Number(input.updatedByUserId) : undefined, + updatedByUserName: input.updatedByUserName?.trim() || null, + }; +} + +async function getStoredMailSettingsRecord() { + const setting = await db.getPortalSettingRecord(MAIL_SETTINGS_KEY); + if (!setting?.valeur) { + return { setting: null, payload: null as StoredMailSettings | null }; + } + + try { + const parsed = JSON.parse(setting.valeur); + return { + setting, + payload: normalizeStoredSettings(parsed), + }; + } catch { + return { + setting, + payload: null, + }; + } +} + +function mergeMailInput(stored: StoredMailSettings | null) { + const decryptedPassword = decryptSecret(stored?.smtpPassEncrypted); + const sourceEntries: Array = [ + "smtpProvider", + "smtpHost", + "smtpPort", + "smtpUser", + "smtpPass", + "smtpFrom", + "smtpSecure", + "smtpRequireTls", + ]; + + const storedInput: MailEnvInput = { + smtpProvider: stored?.smtpProvider, + smtpHost: stored?.smtpHost, + smtpPort: stored?.smtpPort, + smtpUser: stored?.smtpUser, + smtpPass: decryptedPassword, + smtpFrom: stored?.smtpFrom, + smtpSecure: stored ? String(Boolean(stored.smtpSecure)) : "", + smtpRequireTls: stored ? String(Boolean(stored.smtpRequireTls)) : "", + }; + + const envInput: MailEnvInput = { + smtpProvider: ENV.smtpProvider, + smtpHost: ENV.smtpHost, + smtpPort: ENV.smtpPort, + smtpUser: ENV.smtpUser, + smtpPass: ENV.smtpPass, + smtpFrom: ENV.smtpFrom, + smtpSecure: ENV.smtpSecure, + smtpRequireTls: ENV.smtpRequireTls, + }; + + const merged: MailEnvInput = {}; + let usesEnvironmentFallback = false; + for (const key of sourceEntries) { + const storedValue = trimOrEmpty(storedInput[key]); + const envValue = trimOrEmpty(envInput[key]); + if (storedValue) { + merged[key] = storedInput[key]; + } else if (envValue) { + merged[key] = envInput[key]; + if (stored) { + usesEnvironmentFallback = true; + } + } + } + + const source: AdminMailSettings["source"] = stored + ? usesEnvironmentFallback + ? "admin+environment" + : "admin" + : "environment"; + + return { merged, source }; +} + +export async function getResolvedMailConfiguration() { + const { setting, payload } = await getStoredMailSettingsRecord(); + const { merged, source } = mergeMailInput(payload); + return { + config: resolveMailConfig(merged), + source, + stored: payload, + updatedAt: setting?.updatedAt?.toISOString?.() ?? null, + }; +} + +export async function getAdminMailSettings(): Promise { + const { config, source, stored, updatedAt } = await getResolvedMailConfiguration(); + const defaults = stored?.smtpProvider && stored.smtpProvider !== "custom" + ? providerDefaults[stored.smtpProvider] + : null; + + return { + smtpProvider: config.provider, + smtpHost: stored?.smtpHost || config.host || defaults?.host || "", + smtpPort: stored?.smtpPort || (config.port ? String(config.port) : defaults?.port ? String(defaults.port) : ""), + smtpUser: stored?.smtpUser || config.user || "", + smtpFrom: stored?.smtpFrom || config.from || "", + smtpSecure: stored ? Boolean(stored.smtpSecure) : config.secure, + smtpRequireTls: stored ? Boolean(stored.smtpRequireTls) : config.requireTLS, + smtpPassConfigured: Boolean(stored?.smtpPassEncrypted || ENV.smtpPass), + ready: config.ready, + missing: config.missing, + source, + hasSavedConfig: Boolean(stored), + updatedAt, + updatedByUserId: stored?.updatedByUserId ?? null, + updatedByUserName: stored?.updatedByUserName ?? null, + }; +} + +export async function saveAdminMailSettings( + input: MailSettingsFormInput, + actor?: { id: number; name?: string | null } +) { + const existing = await getStoredMailSettingsRecord(); + const nextPassword = trimOrEmpty(input.smtpPass) + ? encryptSecret(trimOrEmpty(input.smtpPass)) + : existing.payload?.smtpPassEncrypted || ""; + + const payload: StoredMailSettings = { + smtpProvider: input.smtpProvider, + smtpHost: trimOrEmpty(input.smtpHost), + smtpPort: trimOrEmpty(input.smtpPort), + smtpUser: trimOrEmpty(input.smtpUser), + smtpFrom: trimOrEmpty(input.smtpFrom), + smtpSecure: Boolean(input.smtpSecure), + smtpRequireTls: Boolean(input.smtpRequireTls), + smtpPassEncrypted: nextPassword, + updatedByUserId: actor?.id, + updatedByUserName: actor?.name?.trim() || null, + }; + + await db.setPortalSetting( + MAIL_SETTINGS_KEY, + JSON.stringify(payload), + "Configuration SMTP et messagerie du portail" + ); + + return getAdminMailSettings(); +} diff --git a/server/mailer.ts b/server/mailer.ts new file mode 100644 index 0000000..2dd9344 --- /dev/null +++ b/server/mailer.ts @@ -0,0 +1,58 @@ +import nodemailer from "nodemailer"; +import { getResolvedMailConfiguration } from "./mailSettings"; + +type SendMailInput = { + to: string[]; + subject: string; + text: string; + html?: string; + replyTo?: string; + fromName?: string; + attachments?: Array<{ + filename: string; + content: Buffer; + contentType?: string; + }>; +}; + +export async function canSendOperationalEmails() { + const { config } = await getResolvedMailConfiguration(); + return config.ready; +} + +export async function sendOperationalEmail(input: SendMailInput): Promise<{ sent: boolean; reason?: string }> { + const { config } = await getResolvedMailConfiguration(); + if (!config.ready) { + return { + sent: false, + reason: config.missing.length > 0 + ? `Configuration SMTP incomplète: ${config.missing.join(", ")}` + : "SMTP non configuré", + }; + } + + const transporter = nodemailer.createTransport({ + host: config.host, + port: Number(config.port), + secure: config.secure, + requireTLS: config.requireTLS, + auth: config.user && config.pass + ? { + user: config.user, + pass: config.pass, + } + : undefined, + }); + + await transporter.sendMail({ + from: input.fromName ? `${input.fromName} <${config.from}>` : config.from, + to: input.to.join(", "), + replyTo: input.replyTo, + subject: input.subject, + text: input.text, + html: input.html, + attachments: input.attachments, + }); + + return { sent: true }; +} diff --git a/server/materialConventionPdf.ts b/server/materialConventionPdf.ts new file mode 100644 index 0000000..37df03c --- /dev/null +++ b/server/materialConventionPdf.ts @@ -0,0 +1,288 @@ +import PDFDocument from "pdfkit"; +import fs from "node:fs"; +import path from "node:path"; + +const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png"); + +export type MaterialContractFinancialMode = "gratuite" | "gratuite_avec_caution" | "location_payante"; +export type MaterialContractStatus = "a_generer" | "generee" | "signee" | "refusee" | "annulee"; + +type ContractRequestLike = { + id: number; + titre: string; + status: string; + formData?: string | null; +}; + +type ContractAssociationLike = { + nomAssociation?: string | null; + adresse?: string | null; + codePostal?: string | null; + ville?: string | null; + telephone?: string | null; + emailContact?: string | null; + nomRepresentant?: string | null; +} | null | undefined; + +type ContractDecision = { + financialMode: MaterialContractFinancialMode; + depositRequired: boolean; + depositAmountCents: number; + rentalAmountCents: number; + pricingNotes?: string | null; + contractStatus: MaterialContractStatus; + contractGeneratedAt?: string | Date | null; + contractValidatedByUserId?: number | null; +}; + +function drawLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) { + if (!fs.existsSync(CCDS_LOGO_PATH)) return; + try { + doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: "center", valign: "center" }); + } catch (error) { + console.warn("[MaterialConventionPdf] Impossible de charger le logo CCDS:", error); + } +} + +function formatDateFr(date: string | Date | null | undefined) { + if (!date) return "-"; + try { + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(date); + } +} + +function formatCurrency(cents: number | null | undefined) { + return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format((cents || 0) / 100); +} + +function getFinancialModeLabel(mode: MaterialContractFinancialMode) { + switch (mode) { + case "gratuite": + return "Mise à disposition gratuite"; + case "gratuite_avec_caution": + return "Mise à disposition gratuite avec caution"; + case "location_payante": + return "Location payante"; + default: + return mode; + } +} + +export async function generateMaterialConventionPdf(input: { + request: ContractRequestLike; + association?: ContractAssociationLike; + decision: ContractDecision; +}) { + const formData = (() => { + try { + return input.request.formData ? JSON.parse(input.request.formData) : {}; + } catch { + return {}; + } + })(); + + const requestedRows = Object.entries(formData.cadreDSU?.materielEvent?.itemsAccordes || {}) + .filter(([, granted]) => Boolean(granted)) + .map(([key]) => ({ + key, + label: + key === "tente3x3" + ? "Tente 3x3" + : key === "chapiteau5x5" + ? "Chapiteau 5x5" + : key === "podium" + ? "Podium" + : "Autres", + quantity: formData.cadreDSU?.materielEvent?.quantitesAccordees?.[key] || "-", + extra: key === "autres" ? formData.cadreDSU?.materielEvent?.autresPrecisions || "" : "", + })); + + const manifestationStart = formData.dateDebutManifestation || formData.dateManifestation; + const manifestationEnd = formData.dateFinManifestation || formData.dateManifestation; + + const pdfBuffer = await new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: "A4", + margins: { top: 38, bottom: 42, left: 42, right: 42 }, + info: { + Title: `Convention matériel CCDS - dossier ${input.request.id}`, + Author: "Communauté de Communes Des Savanes", + Subject: "Convention de mise à disposition / location du matériel événementiel", + }, + }); + + const chunks: Buffer[] = []; + doc.on("data", (chunk: Buffer) => chunks.push(chunk)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + + doc.lineWidth(2).strokeColor("#efb100").moveTo(doc.page.margins.left, y).lineTo(doc.page.margins.left + pageWidth, y).stroke(); + y += 16; + drawLogo(doc, doc.page.margins.left + (pageWidth - 84) / 2, y, 84, 84); + y += 92; + + doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("COMMUNAUTÉ DE COMMUNES DES SAVANES", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 18; + doc.font("Helvetica-Bold").fontSize(17).fillColor("#0f172a").text( + "CONVENTION DE MISE À DISPOSITION / LOCATION", + doc.page.margins.left, + y, + { width: pageWidth, align: "center" } + ); + y += 20; + doc.font("Helvetica-Bold").fontSize(14).text("DU MATÉRIEL ÉVÉNEMENTIEL DE LA CCDS", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 28; + + doc.roundedRect(doc.page.margins.left, y, pageWidth, 42, 8).fillAndStroke("#eef5ff", "#bfd2ef"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f2d63").text( + "Document généré à partir de la décision administrative et de l'annexe de référence CCDS", + doc.page.margins.left + 14, + y + 9, + { width: pageWidth - 28, align: "center" } + ); + doc.font("Helvetica").fontSize(9).fillColor("#334155").text( + `Dossier #${input.request.id} — statut contrat : ${input.decision.contractStatus}`, + doc.page.margins.left + 14, + y + 23, + { width: pageWidth - 28, align: "center" } + ); + y += 56; + + const infoRows: Array<[string, string]> = [ + ["Association", input.association?.nomAssociation || formData.nomAssociation || "-"], + ["Représentant", input.association?.nomRepresentant || formData.demandeurNomPrenom || "-"], + ["Adresse", [input.association?.adresse, input.association?.codePostal, input.association?.ville].filter(Boolean).join(" ") || "-"], + ["Téléphone", input.association?.telephone || formData.telephoneAssociation || "-"], + ["Email", input.association?.emailContact || formData.emailAssociation || "-"], + ["Commune / manifestation", formData.commune || "-"], + [ + "Période d'utilisation", + manifestationStart || manifestationEnd + ? `${formatDateFr(manifestationStart)}${manifestationEnd ? ` au ${formatDateFr(manifestationEnd)}` : ""}` + : "-", + ], + ["Prise en charge", formatDateFr(formData.datePriseEnCharge)], + ["Restitution", formatDateFr(formData.dateRestitution)], + ["Objet", formData.motifDemande || input.request.titre || "-"], + ]; + + for (const [label, value] of infoRows) { + doc.font("Helvetica-Bold").fontSize(9).fillColor("#334155").text(`${label} :`, doc.page.margins.left, y, { width: 140 }); + doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text(value, doc.page.margins.left + 145, y - 1, { + width: pageWidth - 145, + }); + y += 18; + } + + y += 6; + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Matériel accordé", doc.page.margins.left, y); + y += 16; + + const cols = [pageWidth * 0.56, pageWidth * 0.14, pageWidth * 0.30]; + let x = doc.page.margins.left; + ["Équipement", "Qté", "Observation"].forEach((header, index) => { + doc.rect(x, y, cols[index], 22).fillAndStroke("#dce8f8", "#bfd2ef"); + doc.font("Helvetica-Bold").fontSize(9).fillColor("#0f2d63").text(header, x + 6, y + 7, { + width: cols[index] - 12, + align: index === 0 ? "left" : "center", + }); + x += cols[index]; + }); + y += 22; + + const rows = requestedRows.length > 0 ? requestedRows : [{ key: "none", label: "Aucun matériel accordé", quantity: "-", extra: "" }]; + rows.forEach((row) => { + const rowHeight = row.extra ? 34 : 26; + let rowX = doc.page.margins.left; + [row.label, row.quantity, row.extra || "—"].forEach((value, index) => { + doc.rect(rowX, y, cols[index], rowHeight).stroke("#cbd5e1"); + doc.font("Helvetica").fontSize(9).fillColor("#0f172a").text(String(value), rowX + 6, y + 7, { + width: cols[index] - 12, + align: index === 0 ? "left" : "center", + }); + rowX += cols[index]; + }); + y += rowHeight; + }); + + y += 18; + doc.roundedRect(doc.page.margins.left, y, pageWidth, 108, 8).fillAndStroke("#f8fafc", "#cbd5e1"); + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Conditions financières", doc.page.margins.left + 14, y + 12); + doc.font("Helvetica").fontSize(10).fillColor("#334155") + .text(`Régime retenu : ${getFinancialModeLabel(input.decision.financialMode)}`, doc.page.margins.left + 14, y + 34, { + width: pageWidth - 28, + }) + .text(`Montant de location : ${formatCurrency(input.decision.rentalAmountCents)}`, doc.page.margins.left + 14, y + 52, { + width: pageWidth - 28, + }) + .text( + `Caution : ${input.decision.depositRequired ? formatCurrency(input.decision.depositAmountCents) : "Aucune caution exigée"}`, + doc.page.margins.left + 14, + y + 70, + { width: pageWidth - 28 } + ); + y += 122; + + if (input.decision.pricingNotes?.trim()) { + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Clauses / réserves spécifiques", doc.page.margins.left, y); + y += 16; + doc.roundedRect(doc.page.margins.left, y, pageWidth, 70, 8).stroke("#cbd5e1"); + doc.font("Helvetica").fontSize(10).fillColor("#334155").text(input.decision.pricingNotes.trim(), doc.page.margins.left + 12, y + 12, { + width: pageWidth - 24, + }); + y += 84; + } + + y += 6; + doc.font("Helvetica").fontSize(9).fillColor("#475569").text( + "Cette convention formalise les conditions administratives retenues par la CCDS pour la mise à disposition ou la location du matériel événementiel.", + doc.page.margins.left, + y, + { width: pageWidth } + ); + y += 34; + + const signatureWidth = (pageWidth - 20) / 2; + doc.roundedRect(doc.page.margins.left, y, signatureWidth, 90, 8).stroke("#cbd5e1"); + doc.roundedRect(doc.page.margins.left + signatureWidth + 20, y, signatureWidth, 90, 8).stroke("#cbd5e1"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a") + .text("Pour l'association", doc.page.margins.left + 12, y + 12, { width: signatureWidth - 24 }) + .text("Pour la CCDS / DSU", doc.page.margins.left + signatureWidth + 32, y + 12, { width: signatureWidth - 24 }); + doc.font("Helvetica").fontSize(9).fillColor("#64748b") + .text("Nom, qualité, signature", doc.page.margins.left + 12, y + 32, { width: signatureWidth - 24 }) + .text("Nom, qualité, signature", doc.page.margins.left + signatureWidth + 32, y + 32, { width: signatureWidth - 24 }); + + doc.font("Helvetica").fontSize(8).fillColor("#64748b").text( + `Convention générée le ${formatDateFr(input.decision.contractGeneratedAt || new Date())}.`, + doc.page.margins.left, + doc.page.height - doc.page.margins.bottom - 12, + { width: pageWidth, align: "center" } + ); + + doc.end(); + } catch (error) { + reject(error); + } + }); + + return { + buffer: pdfBuffer, + fileName: `Convention_Materiel_CCDS_${input.request.id}.pdf`, + }; +} diff --git a/server/materialReturnEmail.ts b/server/materialReturnEmail.ts new file mode 100644 index 0000000..f64e3df --- /dev/null +++ b/server/materialReturnEmail.ts @@ -0,0 +1,238 @@ +function formatDateFr(date: Date | string | null | undefined) { + if (!date) return "Non renseignée"; + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "long", + year: "numeric", + }); +} + +function escapeHtml(value: string) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +export function generateMaterialReturnReminderEmail(input: { + associationName: string; + serviceLabel?: string | null; + restitutionDate: Date | string; + uploadLink: string; + requestTitle: string; + tone?: "assignment" | "scheduled" | "overdue" | "reactivation"; +}) { + const restitutionLabel = formatDateFr(input.restitutionDate); + const serviceLabel = input.serviceLabel?.trim() || "service technique / logistique"; + const tone = input.tone || "scheduled"; + const toneContent = { + assignment: { + subject: `Mission de récupération assignée - ${input.associationName} - restitution prévue le ${restitutionLabel}`, + title: "Mission de récupération assignée", + intro: `Une mission de récupération du matériel a été attribuée à ${escapeHtml(serviceLabel)} pour ${escapeHtml(input.associationName)}.`, + actionTitle: "Action à préparer", + actionBody: + "La fiche d'état des lieux préremplie CCDS est jointe à cet email. Conservez ce lien terrain : il vous permettra de constater la restitution, signer sur smartphone et clôturer le dossier le moment venu.", + accent: "#0f2d63", + alertBg: "#f5f9ff", + alertBorder: "#cfe0ff", + alertTitleColor: "#0f2d63", + }, + scheduled: { + subject: `Récupération matériel à organiser - ${input.associationName} - restitution prévue le ${restitutionLabel}`, + title: "Récupération du matériel à organiser", + intro: `La restitution du matériel pour ${escapeHtml(input.associationName)} approche.`, + actionTitle: "Action attendue", + actionBody: + "La fiche d'état des lieux préremplie CCDS est jointe à cet email. Votre service dispose d'un délai maximal de 3 jours pour effectuer la récupération, compléter la fiche contradictoire puis la déposer sur le portail.", + accent: "#0f2d63", + alertBg: "#fff8e6", + alertBorder: "#efb100", + alertTitleColor: "#7c5a00", + }, + overdue: { + subject: `Retard de restitution - action requise - ${input.associationName} - ${restitutionLabel}`, + title: "Alerte retard de restitution", + intro: `La restitution du matériel pour ${escapeHtml(input.associationName)} n'a pas encore été validée dans les délais.`, + actionTitle: "Relance automatique", + actionBody: + "Merci d'intervenir en priorité. Le délai de 3 jours après la date de restitution est dépassé. Utilisez le lien ci-dessous pour finaliser le constat terrain et débloquer la clôture du dossier.", + accent: "#b45309", + alertBg: "#fff7ed", + alertBorder: "#fdba74", + alertTitleColor: "#9a3412", + }, + reactivation: { + subject: `Nouveau lien de restitution actif - ${input.associationName} - ${restitutionLabel}`, + title: "Lien de restitution réactivé", + intro: `Un nouveau lien terrain actif a été généré pour ${escapeHtml(input.associationName)}.`, + actionTitle: "Action immédiate", + actionBody: + "Utilisez exclusivement le nouveau lien ci-dessous pour constater la restitution, signer sur smartphone et clôturer le dossier. Les anciens liens de restitution ne sont plus valides.", + accent: "#0f2d63", + alertBg: "#eef6ff", + alertBorder: "#93c5fd", + alertTitleColor: "#1d4ed8", + }, + }[tone]; + const subject = toneContent.subject; + + const text = [ + tone === "assignment" + ? "Assignation logistique - récupération du matériel événementiel" + : tone === "overdue" + ? "Alerte automatique - restitution du matériel en retard" + : "Rappel automatique - récupération du matériel événementiel", + "", + `Association : ${input.associationName}`, + `Demande : ${input.requestTitle}`, + `Service concerné : ${serviceLabel}`, + `Date de restitution prévue : ${restitutionLabel}`, + "", + "La fiche d'état des lieux préremplie CCDS est jointe à cet email.", + tone === "assignment" + ? "Ce lien terrain devra être utilisé par l'agent mandaté pour effectuer le constat contradictoire, signer et clôturer la restitution." + : tone === "reactivation" + ? "Un nouveau lien actif vient d'être généré. Les anciens liens de restitution ne sont plus valides." + : tone === "overdue" + ? "Le délai de 3 jours après la restitution est dépassé. Merci de finaliser la récupération et de valider immédiatement la fiche sur le portail." + : "Vous disposez d'un délai maximal de 3 jours pour effectuer la récupération, compléter la fiche de manière contradictoire puis la téléverser sur le portail.", + "", + `Téléverser la fiche signée : ${input.uploadLink}`, + "", + "Ce message a été généré automatiquement par le Portail Associations.", + ].join("\n"); + + const html = ` +
+
+
+
Portail Associations
+

${toneContent.title}

+

+ ${toneContent.intro} +

+
+ +
+
+
+
Association
+
${escapeHtml(input.associationName)}
+
Demande
+
${escapeHtml(input.requestTitle)}
+
Service concerné
+
${escapeHtml(serviceLabel)}
+
Restitution prévue
+
${escapeHtml(restitutionLabel)}
+
+
+ +
+

${toneContent.actionTitle}

+

+ ${toneContent.actionBody} +

+
+ + + +
+

+ Lien terrain à utiliser +

+

+ ${escapeHtml(input.uploadLink)} +

+
+ +

+ Ce message a été généré automatiquement par le Portail Associations afin de sécuriser le retour du matériel événementiel. +

+
+
+
+ `; + + return { subject, text, html }; +} + +export function generateMaterialReturnCompletionEmail(input: { + associationName: string; + requestTitle: string; + restitutionDate: Date | string; + serviceLabel?: string | null; + issueFlag: boolean; + compliance: "conforme" | "non_conforme"; + discrepancyDetails?: string | null; +}) { + const restitutionLabel = formatDateFr(input.restitutionDate); + const serviceLabel = input.serviceLabel?.trim() || "service technique / logistique"; + const statusLabel = input.issueFlag ? "Alerte dégradation / litige" : "Matériel récupéré / dossier clôturé"; + const subject = `${input.issueFlag ? "Alerte restitution" : "Restitution clôturée"} - ${input.associationName} - ${restitutionLabel}`; + + const text = [ + "Clôture de restitution du matériel événementiel", + "", + `Association : ${input.associationName}`, + `Demande : ${input.requestTitle}`, + `Service concerné : ${serviceLabel}`, + `Restitution : ${restitutionLabel}`, + `Résultat : ${statusLabel}`, + `Conformité : ${input.compliance === "conforme" ? "Conforme" : "Non conforme / partielle"}`, + input.discrepancyDetails ? `Réserves : ${input.discrepancyDetails}` : "", + "", + "Le PDF officiel finalisé est joint à cet email pour archivage et suivi.", + "", + "Ce message a été généré automatiquement par le Portail Associations.", + ] + .filter(Boolean) + .join("\n"); + + const html = ` +
+
+
+
Portail Associations
+

${input.issueFlag ? "Alerte restitution" : "Restitution clôturée"}

+

+ ${input.issueFlag ? "Des réserves ont été constatées lors de la restitution." : "Le matériel a été récupéré et la fiche contradictoire est finalisée."} +

+
+
+
+
Association
+
${escapeHtml(input.associationName)}
+
Demande
+
${escapeHtml(input.requestTitle)}
+
Service concerné
+
${escapeHtml(serviceLabel)}
+
Restitution
+
${escapeHtml(restitutionLabel)}
+
Résultat
+
${escapeHtml(statusLabel)}
+
+
+

+ ${input.compliance === "conforme" ? "Restitution conforme" : "Restitution non conforme / partielle"} +

+

+ ${escapeHtml(input.discrepancyDetails || "Aucune réserve particulière n'a été déclarée.")} +

+
+

+ Le PDF officiel finalisé est joint à cet email pour archivage et suivi. +

+
+
+
+ `; + + return { subject, text, html }; +} diff --git a/server/materialReturnLitigationPdf.ts b/server/materialReturnLitigationPdf.ts new file mode 100644 index 0000000..acb6af8 --- /dev/null +++ b/server/materialReturnLitigationPdf.ts @@ -0,0 +1,202 @@ +import PDFDocument from "pdfkit"; +import fs from "node:fs"; +import path from "node:path"; +import { getMaterialEventLabel, materialEventItems, sanitizeMaterialEventQuantityMap, type MaterialEventItemKey } from "@shared/materialEvent"; + +const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png"); + +function drawLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) { + if (!fs.existsSync(CCDS_LOGO_PATH)) return; + try { + doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: "center", valign: "center" }); + } catch (error) { + console.warn("[MaterialReturnLitigationPdf] Impossible de charger le logo CCDS:", error); + } +} + +function formatDateFr(date: string | Date | null | undefined) { + if (!date) return "-"; + try { + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(date); + } +} + +function formatCurrency(amountCents: number | null | undefined) { + if (!Number.isFinite(amountCents as number)) return "-"; + return new Intl.NumberFormat("fr-FR", { + style: "currency", + currency: "EUR", + }).format((amountCents || 0) / 100); +} + +export async function generateMaterialReturnLitigationLetterPdf(input: { + request: { + id: number; + titre: string; + formData?: string | null; + }; + association?: { + nomAssociation?: string | null; + emailContact?: string | null; + telephone?: string | null; + ville?: string | null; + } | null; + followup: { + restitutionDate?: string | Date | null; + discrepancyCategories?: string[]; + discrepancyDetails?: string | null; + }; + arbitration: { + blockedItems: Record; + decision: "partial_retention" | "full_retention" | "dismissed"; + amountCents: number; + notes?: string | null; + }; +}) { + const formData = (() => { + try { + return input.request.formData ? JSON.parse(input.request.formData) : {}; + } catch { + return {}; + } + })(); + + const blockedItems = sanitizeMaterialEventQuantityMap(input.arbitration.blockedItems); + const blockedRows = materialEventItems + .map((item) => ({ ...item, quantity: blockedItems[item.key] || 0 })) + .filter((item) => item.quantity > 0); + + const decisionLabel = + input.arbitration.decision === "full_retention" + ? "Encaissement total de la caution" + : input.arbitration.decision === "partial_retention" + ? "Retenue partielle sur caution" + : "Classement sans suite"; + + const pdfBuffer = await new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: "A4", + margins: { top: 38, bottom: 42, left: 46, right: 46 }, + info: { + Title: `Courrier de litige - Demande ${input.request.id}`, + Author: "Communauté de Communes Des Savanes", + Subject: "Notification de litige sur restitution de matériel", + }, + }); + + const chunks: Buffer[] = []; + doc.on("data", (chunk: Buffer) => chunks.push(chunk)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + + drawLogo(doc, doc.page.margins.left, y, 74, 74); + doc.font("Helvetica-Bold").fontSize(16).fillColor("#0f172a").text( + "Notification de litige et arbitrage matériel", + doc.page.margins.left + 92, + y + 14, + { width: pageWidth - 92 } + ); + doc.font("Helvetica").fontSize(10).fillColor("#475569").text( + "Communauté de Communes Des Savanes - Direction des Services aux Usagers", + doc.page.margins.left + 92, + y + 40, + { width: pageWidth - 92 } + ); + y += 96; + + const lines = [ + ["Association", input.association?.nomAssociation || "-"], + ["Demande", input.request.titre || `Dossier #${input.request.id}`], + ["Commune", formData.commune || input.association?.ville || "-"], + ["Date de restitution", formatDateFr(input.followup.restitutionDate)], + ["Décision d'arbitrage", decisionLabel], + ["Montant retenu", formatCurrency(input.arbitration.amountCents)], + ] as const; + + lines.forEach(([label, value]) => { + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text(`${label} :`, doc.page.margins.left, y, { + width: 170, + }); + doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text(String(value || "-"), doc.page.margins.left + 170, y, { + width: pageWidth - 170, + }); + y += 18; + }); + + y += 8; + doc.roundedRect(doc.page.margins.left, y, pageWidth, 88, 10).fillAndStroke("#fff7ed", "#fdba74"); + doc.font("Helvetica-Bold").fontSize(11).fillColor("#9a3412").text("Constat de terrain", doc.page.margins.left + 14, y + 12); + doc.font("Helvetica").fontSize(10).fillColor("#7c2d12").text( + [ + input.followup.discrepancyCategories?.length + ? `Réserves : ${input.followup.discrepancyCategories.join(", ")}.` + : null, + input.followup.discrepancyDetails?.trim() || null, + ].filter(Boolean).join(" "), + doc.page.margins.left + 14, + y + 32, + { width: pageWidth - 28 } + ); + y += 108; + + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Matériel provisoirement bloqué", doc.page.margins.left, y); + y += 18; + + if (blockedRows.length === 0) { + doc.font("Helvetica").fontSize(10).fillColor("#475569").text( + "Aucun équipement n'est maintenu en indisponibilité après arbitrage.", + doc.page.margins.left, + y, + { width: pageWidth } + ); + y += 22; + } else { + blockedRows.forEach((item) => { + doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text( + `- ${getMaterialEventLabel(item.key)} : ${item.quantity}`, + doc.page.margins.left + 8, + y, + { width: pageWidth - 8 } + ); + y += 16; + }); + } + + if (input.arbitration.notes?.trim()) { + y += 12; + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Observations administratives", doc.page.margins.left, y); + y += 18; + doc.font("Helvetica").fontSize(10).fillColor("#334155").text(input.arbitration.notes.trim(), doc.page.margins.left, y, { + width: pageWidth, + }); + y = doc.y + 10; + } + + doc.font("Helvetica").fontSize(9).fillColor("#64748b").text( + `Document généré automatiquement pour le dossier #${input.request.id}.`, + doc.page.margins.left, + Math.max(y + 28, doc.page.height - doc.page.margins.bottom - 18), + { width: pageWidth, align: "center" } + ); + + doc.end(); + } catch (error) { + reject(error); + } + }); + + return { + buffer: pdfBuffer, + fileName: `Courrier_Litige_${input.request.id}.pdf`, + }; +} diff --git a/server/materialReturnPdf.ts b/server/materialReturnPdf.ts new file mode 100644 index 0000000..5bc9814 --- /dev/null +++ b/server/materialReturnPdf.ts @@ -0,0 +1,462 @@ +import PDFDocument from "pdfkit"; +import fs from "node:fs"; +import path from "node:path"; + +type MaterialReturnRequest = { + id: number; + titre: string; + formData?: string | null; +}; + +type MaterialReturnAssociation = { + nomAssociation?: string | null; + telephone?: string | null; + emailContact?: string | null; + ville?: string | null; +} | null | undefined; + +const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png"); + +function drawLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) { + if (!fs.existsSync(CCDS_LOGO_PATH)) return; + try { + doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: "center", valign: "center" }); + } catch (error) { + console.warn("[MaterialReturnPdf] Impossible de charger le logo CCDS:", error); + } +} + +function formatDateFr(date: string | Date | null | undefined) { + if (!date) return ""; + try { + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(date); + } +} + +function addLabeledValue(doc: PDFKit.PDFDocument, label: string, value: string, x: number, y: number, width: number) { + doc.font("Helvetica-Bold").fontSize(9).fillColor("#334155").text(label, x, y, { width }); + doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text(value || "........................................................", x, y + 12, { width }); +} + +function addSignatureBlock(doc: PDFKit.PDFDocument, title: string, x: number, y: number, width: number) { + doc.roundedRect(x, y, width, 90, 8).stroke("#cbd5e1"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text(title, x + 12, y + 12, { width: width - 24 }); + doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("Nom, qualité, signature et date", x + 12, y + 32, { width: width - 24 }); +} + +export async function generateMaterialReturnStatementPdf(input: { + request: MaterialReturnRequest; + association?: MaterialReturnAssociation; +}) { + const formData = (() => { + try { + return input.request.formData ? JSON.parse(input.request.formData) : {}; + } catch { + return {}; + } + })(); + + const manifestationStart = formData.dateDebutManifestation || formData.dateManifestation; + const manifestationEnd = formData.dateFinManifestation || formData.dateManifestation; + const requestedItems = Object.entries(formData.materielsDemandes || {}) + .filter(([, checked]) => Boolean(checked)) + .map(([key]) => ({ + key, + label: + key === "tente3x3" + ? "Tente 3x3" + : key === "chapiteau5x5" + ? "Chapiteau 5x5" + : key === "podium" + ? "Podium" + : "Autres", + quantity: formData.quantitesDemandees?.[key] || "-", + extra: key === "autres" ? formData.autreMaterielPrecisions || "" : "", + })); + + const pdfBuffer = await new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: "A4", + margins: { top: 36, bottom: 40, left: 42, right: 42 }, + info: { + Title: `Fiche d'état des lieux - Demande ${input.request.id}`, + Author: "Communauté de Communes Des Savanes", + Subject: "Fiche d'état des lieux du matériel événementiel", + }, + }); + + const chunks: Buffer[] = []; + doc.on("data", (chunk: Buffer) => chunks.push(chunk)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + + doc.moveTo(doc.page.margins.left, y).lineTo(doc.page.margins.left + pageWidth, y).lineWidth(2).stroke("#efb100"); + y += 14; + drawLogo(doc, doc.page.margins.left + (pageWidth - 76) / 2, y, 76, 76); + y += 82; + + doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("COMMUNAUTÉ DE COMMUNES DES SAVANES", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 16; + doc.font("Helvetica-Bold").fontSize(18).fillColor("#0f172a").text("FICHE D'ÉTAT DES LIEUX", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 18; + doc.font("Helvetica-Bold").fontSize(14).text("DU MATÉRIEL ÉVÉNEMENTIEL DE LA CCDS", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 22; + + doc.roundedRect(doc.page.margins.left, y, pageWidth, 34, 8).fillAndStroke("#eef5ff", "#bfd2ef"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f2d63").text("Document de suivi prérempli à compléter contradictoirement", doc.page.margins.left + 14, y + 9, { + width: pageWidth - 28, + align: "center", + }); + y += 48; + + const leftColWidth = (pageWidth - 16) / 2; + addLabeledValue(doc, "Commune", formData.commune || input.association?.ville || "", doc.page.margins.left, y, leftColWidth); + addLabeledValue(doc, "Demandeur", formData.demandeurNomPrenom || input.association?.nomAssociation || "", doc.page.margins.left + leftColWidth + 16, y, leftColWidth); + y += 40; + addLabeledValue(doc, "Téléphone", formData.telephoneAssociation || input.association?.telephone || "", doc.page.margins.left, y, leftColWidth); + addLabeledValue(doc, "Intitulé de la manifestation", formData.motifDemande || input.request.titre || "", doc.page.margins.left + leftColWidth + 16, y, leftColWidth); + y += 40; + addLabeledValue( + doc, + "Date prévue du / au", + manifestationStart || manifestationEnd + ? `${formatDateFr(manifestationStart)}${manifestationEnd ? ` au ${formatDateFr(manifestationEnd)}` : ""}` + : "", + doc.page.margins.left, + y, + leftColWidth + ); + addLabeledValue(doc, "Date de restitution prévue", formatDateFr(formData.dateRestitution), doc.page.margins.left + leftColWidth + 16, y, leftColWidth); + y += 48; + + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Matériel concerné", doc.page.margins.left, y); + y += 14; + + const tableX = doc.page.margins.left; + const cols = [pageWidth * 0.46, pageWidth * 0.16, pageWidth * 0.19, pageWidth * 0.19]; + const headers = ["Équipement", "Qté", "Mise à disposition", "Restitution"]; + let cursorX = tableX; + headers.forEach((header, index) => { + doc.rect(cursorX, y, cols[index], 22).fillAndStroke("#dce8f8", "#bfd2ef"); + doc.font("Helvetica-Bold").fontSize(9).fillColor("#0f2d63").text(header, cursorX + 6, y + 7, { + width: cols[index] - 12, + align: index === 0 ? "left" : "center", + }); + cursorX += cols[index]; + }); + y += 22; + + const items = requestedItems.length > 0 ? requestedItems : [{ key: "none", label: "Aucun matériel renseigné", quantity: "-", extra: "" }]; + items.forEach((item) => { + const rowHeight = item.extra ? 36 : 28; + let rowX = tableX; + const values = [ + item.extra ? `${item.label} — ${item.extra}` : item.label, + item.quantity, + "À compléter", + "À compléter", + ]; + + values.forEach((value, index) => { + doc.rect(rowX, y, cols[index], rowHeight).stroke("#cbd5e1"); + doc.font(index < 2 ? "Helvetica" : "Helvetica-Oblique") + .fontSize(9) + .fillColor(index < 2 ? "#0f172a" : "#64748b") + .text(value, rowX + 6, y + 8, { + width: cols[index] - 12, + align: index === 0 ? "left" : "center", + }); + rowX += cols[index]; + }); + y += rowHeight; + }); + + y += 20; + doc.roundedRect(doc.page.margins.left, y, pageWidth, 76, 8).stroke("#cbd5e1"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text("Observations contradictoires", doc.page.margins.left + 12, y + 12); + doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("Préciser l'état général du matériel, les éventuelles anomalies, manques ou réserves constatées.", doc.page.margins.left + 12, y + 28, { + width: pageWidth - 24, + }); + y += 92; + + addSignatureBlock(doc, "L'agent CCDS / service récupérateur", doc.page.margins.left, y, (pageWidth - 20) / 2); + addSignatureBlock(doc, "L'emprunteur / représentant de l'association", doc.page.margins.left + (pageWidth - 20) / 2 + 20, y, (pageWidth - 20) / 2); + y += 104; + + doc.font("Helvetica").fontSize(8).fillColor("#64748b").text( + `Document préparé automatiquement pour la demande #${input.request.id} — à vérifier et compléter sur site lors de la restitution.`, + doc.page.margins.left, + y, + { width: pageWidth, align: "center" } + ); + + doc.end(); + } catch (error) { + reject(error); + } + }); + + return { + buffer: pdfBuffer, + fileName: `Fiche_Etat_Des_Lieux_${input.request.id}.pdf`, + }; +} + +export async function generateCompletedMaterialReturnStatementPdf(input: { + request: MaterialReturnRequest; + association?: MaterialReturnAssociation; + completion: { + compliance: "conforme" | "non_conforme"; + discrepancyLabels: string[]; + discrepancyDetails?: string | null; + agentName: string; + agentRole: string; + borrowerName: string; + borrowerRole: string; + validatedAt: Date; + geoLatitude?: string | null; + geoLongitude?: string | null; + geoStatus?: string | null; + geoFailureReason?: string | null; + locationReference?: string | null; + agentSignatureBuffer?: Buffer; + borrowerSignatureBuffer?: Buffer; + }; +}) { + const formData = (() => { + try { + return input.request.formData ? JSON.parse(input.request.formData) : {}; + } catch { + return {}; + } + })(); + + const manifestationStart = formData.dateDebutManifestation || formData.dateManifestation; + const manifestationEnd = formData.dateFinManifestation || formData.dateManifestation; + const requestedItems = Object.entries(formData.materielsDemandes || {}) + .filter(([, checked]) => Boolean(checked)) + .map(([key]) => ({ + key, + label: + key === "tente3x3" + ? "Tente 3x3" + : key === "chapiteau5x5" + ? "Chapiteau 5x5" + : key === "podium" + ? "Podium" + : "Autres", + quantity: formData.quantitesDemandees?.[key] || "-", + extra: key === "autres" ? formData.autreMaterielPrecisions || "" : "", + })); + + const pdfBuffer = await new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: "A4", + margins: { top: 36, bottom: 40, left: 42, right: 42 }, + info: { + Title: `Fiche d'état des lieux finalisée - Demande ${input.request.id}`, + Author: "Communauté de Communes Des Savanes", + Subject: "Fiche d'état des lieux contradictoire du matériel événementiel", + }, + }); + + const chunks: Buffer[] = []; + doc.on("data", (chunk: Buffer) => chunks.push(chunk)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + + doc.moveTo(doc.page.margins.left, y).lineTo(doc.page.margins.left + pageWidth, y).lineWidth(2).stroke("#efb100"); + y += 14; + drawLogo(doc, doc.page.margins.left + (pageWidth - 76) / 2, y, 76, 76); + y += 82; + + doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("COMMUNAUTÉ DE COMMUNES DES SAVANES", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 16; + doc.font("Helvetica-Bold").fontSize(18).fillColor("#0f172a").text("FICHE D'ÉTAT DES LIEUX FINALISÉE", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 18; + doc.font("Helvetica-Bold").fontSize(14).text("DU MATÉRIEL ÉVÉNEMENTIEL DE LA CCDS", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 22; + + doc.roundedRect(doc.page.margins.left, y, pageWidth, 44, 8).fillAndStroke( + input.completion.compliance === "conforme" ? "#ecfdf3" : "#fff7ed", + input.completion.compliance === "conforme" ? "#a7f3d0" : "#fdba74" + ); + doc.font("Helvetica-Bold").fontSize(11).fillColor(input.completion.compliance === "conforme" ? "#166534" : "#9a3412").text( + input.completion.compliance === "conforme" ? "Restitution conforme" : "Restitution non conforme / partielle", + doc.page.margins.left + 14, + y + 10, + { width: pageWidth - 28, align: "center" } + ); + doc.font("Helvetica").fontSize(9).fillColor("#475569").text( + `Validée le ${formatDateFr(input.completion.validatedAt)}`, + doc.page.margins.left + 14, + y + 25, + { width: pageWidth - 28, align: "center" } + ); + y += 58; + + const leftColWidth = (pageWidth - 16) / 2; + addLabeledValue(doc, "Commune", formData.commune || input.association?.ville || "", doc.page.margins.left, y, leftColWidth); + addLabeledValue(doc, "Association / demandeur", input.association?.nomAssociation || formData.demandeurNomPrenom || "", doc.page.margins.left + leftColWidth + 16, y, leftColWidth); + y += 40; + addLabeledValue(doc, "Téléphone", formData.telephoneAssociation || input.association?.telephone || "", doc.page.margins.left, y, leftColWidth); + addLabeledValue(doc, "Manifestation", formData.motifDemande || input.request.titre || "", doc.page.margins.left + leftColWidth + 16, y, leftColWidth); + y += 40; + addLabeledValue( + doc, + "Période", + manifestationStart || manifestationEnd + ? `${formatDateFr(manifestationStart)}${manifestationEnd ? ` au ${formatDateFr(manifestationEnd)}` : ""}` + : "", + doc.page.margins.left, + y, + leftColWidth + ); + addLabeledValue(doc, "Restitution prévue", formatDateFr(formData.dateRestitution), doc.page.margins.left + leftColWidth + 16, y, leftColWidth); + y += 48; + + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Matériel restitué", doc.page.margins.left, y); + y += 14; + requestedItems.forEach((item) => { + doc.rect(doc.page.margins.left, y, pageWidth, 24).stroke("#cbd5e1"); + doc.font("Helvetica").fontSize(9).fillColor("#0f172a").text( + `${item.label}${item.extra ? ` — ${item.extra}` : ""}`, + doc.page.margins.left + 8, + y + 7, + { width: pageWidth * 0.65 } + ); + doc.font("Helvetica-Bold").fontSize(9).text( + `Qté : ${item.quantity}`, + doc.page.margins.left + pageWidth - 120, + y + 7, + { width: 110, align: "right" } + ); + y += 24; + }); + + y += 16; + doc.roundedRect(doc.page.margins.left, y, pageWidth, 100, 8).stroke("#cbd5e1"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text("Observations contradictoires", doc.page.margins.left + 12, y + 12); + doc.font("Helvetica").fontSize(9).fillColor("#475569").text( + input.completion.discrepancyLabels.length > 0 + ? input.completion.discrepancyLabels.join(" • ") + : "Aucune réserve relevée.", + doc.page.margins.left + 12, + y + 30, + { width: pageWidth - 24 } + ); + doc.text( + input.completion.discrepancyDetails || "Aucune précision complémentaire.", + doc.page.margins.left + 12, + y + 50, + { width: pageWidth - 24 } + ); + y += 118; + + const geoLine = input.completion.geoLatitude && input.completion.geoLongitude + ? `Géolocalisation : ${input.completion.geoLatitude}, ${input.completion.geoLongitude}` + : input.completion.locationReference + ? `Référence de lieu : ${input.completion.locationReference}${ + input.completion.geoFailureReason ? ` • GPS indisponible (${input.completion.geoFailureReason})` : "" + }` + : input.completion.geoFailureReason + ? `GPS indisponible (${input.completion.geoFailureReason})` + : ""; + + doc.font("Helvetica").fontSize(8).fillColor("#64748b").text( + `Horodatage serveur : ${new Date(input.completion.validatedAt).toLocaleString("fr-FR")}${geoLine ? ` • ${geoLine}` : ""}`, + doc.page.margins.left, + y, + { width: pageWidth } + ); + y += 18; + + const signatureWidth = (pageWidth - 20) / 2; + doc.roundedRect(doc.page.margins.left, y, signatureWidth, 126, 8).stroke("#cbd5e1"); + doc.roundedRect(doc.page.margins.left + signatureWidth + 20, y, signatureWidth, 126, 8).stroke("#cbd5e1"); + + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text("Agent CCDS / service récupérateur", doc.page.margins.left + 12, y + 12, { + width: signatureWidth - 24, + }); + doc.font("Helvetica").fontSize(9).fillColor("#475569").text( + `${input.completion.agentName}\n${input.completion.agentRole}`, + doc.page.margins.left + 12, + y + 30, + { width: signatureWidth - 24 } + ); + if (input.completion.agentSignatureBuffer) { + doc.image(input.completion.agentSignatureBuffer, doc.page.margins.left + 12, y + 64, { + fit: [signatureWidth - 24, 42], + align: "center", + valign: "center", + }); + } + + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text("Emprunteur / représentant", doc.page.margins.left + signatureWidth + 32, y + 12, { + width: signatureWidth - 24, + }); + doc.font("Helvetica").fontSize(9).fillColor("#475569").text( + `${input.completion.borrowerName}\n${input.completion.borrowerRole}`, + doc.page.margins.left + signatureWidth + 32, + y + 30, + { width: signatureWidth - 24 } + ); + if (input.completion.borrowerSignatureBuffer) { + doc.image(input.completion.borrowerSignatureBuffer, doc.page.margins.left + signatureWidth + 32, y + 64, { + fit: [signatureWidth - 24, 42], + align: "center", + valign: "center", + }); + } + + y += 138; + doc.font("Helvetica").fontSize(8).fillColor("#64748b").text( + `Document finalisé automatiquement pour la demande #${input.request.id}.`, + doc.page.margins.left, + y, + { width: pageWidth, align: "center" } + ); + + doc.end(); + } catch (error) { + reject(error); + } + }); + + return { + buffer: pdfBuffer, + fileName: `Fiche_Etat_Des_Lieux_Finalisee_${input.request.id}.pdf`, + }; +} diff --git a/server/materialReturnWorkflow.test.ts b/server/materialReturnWorkflow.test.ts new file mode 100644 index 0000000..df9cd38 --- /dev/null +++ b/server/materialReturnWorkflow.test.ts @@ -0,0 +1,143 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("./db", () => ({ + getMaterialReturnFollowupByRequestId: vi.fn(), + upsertMaterialReturnFollowup: vi.fn(), +})); + +vi.mock("./mailer", () => ({ + sendOperationalEmail: vi.fn(), +})); + +vi.mock("./materialReturnEmail", () => ({ + generateMaterialReturnReminderEmail: vi.fn(), +})); + +vi.mock("./materialReturnPdf", () => ({ + generateMaterialReturnStatementPdf: vi.fn(), +})); + +vi.mock("./logisticsSettings", () => ({ + getLogisticsSettings: vi.fn(), +})); + +import * as db from "./db"; +import { getLogisticsSettings } from "./logisticsSettings"; +import { scheduleMaterialReturnFollowup } from "./materialReturnWorkflow"; + +describe("scheduleMaterialReturnFollowup", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getLogisticsSettings).mockResolvedValue({ + materialReturnLeadDays: 4, + materialReturnGraceDays: 3, + inventory: { + tente3x3: 12, + chapiteau5x5: 10, + podium: 1, + autres: null, + }, + replacementValues: { + tente3x3: 0, + chapiteau5x5: 0, + podium: 0, + autres: null, + }, + }); + }); + + it("keeps the standard expiry window for a fresh follow-up", async () => { + vi.mocked(db.getMaterialReturnFollowupByRequestId).mockResolvedValue(undefined); + vi.mocked(db.upsertMaterialReturnFollowup).mockResolvedValue(1); + + const request = { + id: 12, + associationId: 3, + formData: JSON.stringify({ dateRestitution: "2026-06-15" }), + } as any; + + await scheduleMaterialReturnFollowup({ + request, + serviceLabel: "Service technique", + recipientEmails: ["agent@example.com"], + }); + + expect(db.upsertMaterialReturnFollowup).toHaveBeenCalledTimes(1); + const payload = vi.mocked(db.upsertMaterialReturnFollowup).mock.calls[0]?.[1]; + expect(payload.status).toBe("planifie"); + expect(new Date(payload.uploadTokenExpiresAt).toISOString().startsWith("2026-06-18")).toBe(true); + }); + + it("reactivates an expired upload window when the follow-up is reassigned", async () => { + const expiredDate = new Date(); + expiredDate.setDate(expiredDate.getDate() - 2); + expiredDate.setHours(23, 59, 59, 999); + + vi.mocked(db.getMaterialReturnFollowupByRequestId).mockResolvedValue({ + id: 4, + requestId: 12, + associationId: 3, + status: "en_cours", + uploadToken: "existing-token", + uploadTokenExpiresAt: expiredDate, + } as any); + vi.mocked(db.upsertMaterialReturnFollowup).mockResolvedValue(4); + + const request = { + id: 12, + associationId: 3, + formData: JSON.stringify({ dateRestitution: "2026-05-01" }), + } as any; + + await scheduleMaterialReturnFollowup({ + request, + serviceLabel: "Service technique", + recipientEmails: ["agent@example.com"], + }); + + const payload = vi.mocked(db.upsertMaterialReturnFollowup).mock.calls[0]?.[1]; + expect(payload.uploadToken).toBe("existing-token"); + expect(new Date(payload.uploadTokenExpiresAt).getTime()).toBeGreaterThan(Date.now()); + expect(payload.status).toBe("planifie"); + }); + + it("generates a new token and a fresh upload window when reactivation is explicit", async () => { + const futureExpiry = new Date("2026-06-18T23:59:59.999Z"); + + vi.mocked(db.getMaterialReturnFollowupByRequestId).mockResolvedValue({ + id: 4, + requestId: 12, + associationId: 3, + status: "en_attente", + uploadToken: "existing-token", + uploadTokenExpiresAt: futureExpiry, + } as any); + vi.mocked(db.upsertMaterialReturnFollowup).mockResolvedValue(4); + + const request = { + id: 12, + associationId: 3, + formData: JSON.stringify({ dateRestitution: "2026-06-15" }), + } as any; + const reactivatedAt = new Date("2026-06-20T10:30:00.000Z"); + + await scheduleMaterialReturnFollowup({ + request, + serviceLabel: "Service technique", + recipientEmails: ["agent@example.com"], + }, { + forceNewToken: true, + forceImmediateSendWindow: true, + reactivatedByUserId: 9, + reactivatedAt, + reactivationReason: "Suivi rouvert", + }); + + const payload = vi.mocked(db.upsertMaterialReturnFollowup).mock.calls[0]?.[1]; + expect(payload.uploadToken).not.toBe("existing-token"); + expect(new Date(payload.uploadTokenExpiresAt).getTime()).toBeGreaterThan(futureExpiry.getTime()); + expect(new Date(payload.uploadTokenExpiresAt).getTime()).toBeGreaterThan(reactivatedAt.getTime()); + expect(payload.reactivatedByUserId).toBe(9); + expect(payload.reactivationReason).toBe("Suivi rouvert"); + }); +}); diff --git a/server/materialReturnWorkflow.ts b/server/materialReturnWorkflow.ts new file mode 100644 index 0000000..5b59b53 --- /dev/null +++ b/server/materialReturnWorkflow.ts @@ -0,0 +1,436 @@ +import { randomBytes } from "node:crypto"; +import * as db from "./db"; +import { sendOperationalEmail } from "./mailer"; +import { generateMaterialReturnReminderEmail } from "./materialReturnEmail"; +import { generateMaterialReturnStatementPdf } from "./materialReturnPdf"; +import { getLogisticsSettings } from "./logisticsSettings"; +import { sanitizeMaterialEventQuantityMap } from "@shared/materialEvent"; + +type RequestLike = Awaited>; + +export const materialReturnStatusLabels = { + planifie: "Planifié", + en_attente: "En attente", + en_cours: "En cours", + cloture: "Clôturé / Récupéré", +} as const; + +export const materialReturnStatusColors = { + planifie: "bg-slate-100 text-slate-700", + en_attente: "bg-red-100 text-red-700", + en_cours: "bg-amber-100 text-amber-700", + cloture: "bg-green-100 text-green-700", +} as const; + +export const materialReturnBoardStateLabels = { + a_attribuer: "À attribuer", + planifie: "Planifié", + terrain: "Sur le terrain", + retard: "Alerte / retard", + conforme: "Retour conforme", + litige: "Litige / dégradation", +} as const; + +export const materialReturnBoardStateColors = { + a_attribuer: "bg-slate-100 text-slate-700 border-slate-200", + planifie: "bg-blue-100 text-blue-700 border-blue-200", + terrain: "bg-amber-100 text-amber-700 border-amber-200", + retard: "bg-red-100 text-red-700 border-red-200", + conforme: "bg-green-100 text-green-700 border-green-200", + litige: "bg-orange-100 text-orange-800 border-orange-200", +} as const; + +export const materialReturnDiscrepancyOptions = [ + { value: "materiel_manquant", label: "Matériel manquant" }, + { value: "accessoires_manquants", label: "Visserie / accessoires manquants" }, + { value: "structure_deformee", label: "Structure déformée / tordue" }, + { value: "toile_dechiree", label: "Toile / bâche déchirée" }, + { value: "choc_important", label: "Choc important" }, + { value: "materiel_sale", label: "Matériel restitué sale" }, + { value: "materiel_humide", label: "Matériel humide (risque moisissure)" }, +] as const; + +export const agentRoleOptions = [ + "Agent technique", + "Responsable logistique", + "Agent MJS", + "Agent DSU", +] as const; + +export const borrowerRoleOptions = [ + "Président d'association", + "Trésorier", + "Régisseur commune", + "Bénévole mandaté", +] as const; + +export function parseRecipientEmails(value: string | null | undefined) { + if (!value) return [] as string[]; + try { + const parsed = JSON.parse(value); + if (!Array.isArray(parsed)) return []; + return parsed.map((entry) => String(entry || "").trim()).filter(Boolean); + } catch { + return []; + } +} + +export function formatMaterialReturnDate(date: Date | string | null | undefined) { + if (!date) return ""; + return new Date(date).toISOString().split("T")[0]; +} + +export function buildMaterialReturnUploadLink(baseUrl: string, token: string) { + return `${baseUrl.replace(/\/$/, "")}/materiel/restitution/${token}`; +} + +export function getMaterialReturnUploadLinkState(input: { + status?: string | null; + uploadTokenExpiresAt?: Date | string | null; + now?: Date; +}) { + if (input.status === "cloture") { + return "closed" as const; + } + if (!input.uploadTokenExpiresAt) { + return "expired" as const; + } + const now = input.now || new Date(); + return now <= new Date(input.uploadTokenExpiresAt) ? "active" as const : "expired" as const; +} + +function getRestitutionDateFromRequest(request: NonNullable) { + try { + const formData = request.formData ? JSON.parse(request.formData) : {}; + const rawDate = formData?.dateRestitution; + if (!rawDate) return null; + const date = new Date(rawDate); + return Number.isNaN(date.getTime()) ? null : date; + } catch { + return null; + } +} + +function getPlannedSendAt(restitutionDate: Date, leadDays: number) { + const planned = new Date(restitutionDate); + planned.setDate(planned.getDate() - leadDays); + planned.setHours(8, 0, 0, 0); + return planned; +} + +function getUploadTokenExpiry(restitutionDate: Date, graceDays: number) { + const expiry = new Date(restitutionDate); + expiry.setDate(expiry.getDate() + graceDays); + expiry.setHours(23, 59, 59, 999); + return expiry; +} + +function getReactivatedUploadTokenExpiry(now: Date, graceDays: number) { + const expiry = new Date(now); + expiry.setDate(expiry.getDate() + Math.max(0, graceDays)); + expiry.setHours(23, 59, 59, 999); + return expiry; +} + +function getDelayAlertDate(restitutionDate: Date | string, graceDays: number) { + const alertDate = new Date(restitutionDate); + alertDate.setDate(alertDate.getDate() + graceDays); + alertDate.setHours(0, 0, 0, 0); + return alertDate; +} + +function hasOperationalAssignment(followup: { + serviceLabel?: string | null; + recipientEmails?: string | null; +} | null | undefined) { + if (!followup) return false; + return Boolean(followup.serviceLabel?.trim()) || parseRecipientEmails(followup.recipientEmails).length > 0; +} + +function mergeRecipientLists(...recipientSets: Array) { + return Array.from( + new Set( + recipientSets + .flatMap((recipientSet) => recipientSet || []) + .map((entry) => String(entry || "").trim()) + .filter(Boolean) + ) + ); +} + +function getNotificationRecipients(followup: { + recipientEmails?: string | null; + supervisionRecipientEmails?: string | null; +}, tone: "assignment" | "scheduled" | "overdue" | "reactivation") { + const terrainRecipients = parseRecipientEmails(followup.recipientEmails); + const supervisionRecipients = parseRecipientEmails(followup.supervisionRecipientEmails); + if (tone === "overdue") { + return mergeRecipientLists(supervisionRecipients, terrainRecipients); + } + return terrainRecipients; +} + +export function computeMaterialReturnBoardState(input: { + requestStatus?: string | null; + restitutionDate?: Date | string | null; + followup?: { + status?: string | null; + issueFlag?: boolean | null; + compliance?: string | null; + litigationStatus?: string | null; + serviceLabel?: string | null; + recipientEmails?: string | null; + } | null; + now?: Date; + graceDays?: number; +}) { + const now = input.now || new Date(); + const followup = input.followup; + const graceDays = Number.isFinite(input.graceDays) ? Math.max(0, input.graceDays || 0) : 3; + + if (followup?.litigationStatus === "pending") { + return "litige" as const; + } + if (followup?.status === "cloture") { + return "conforme" as const; + } + + const restitutionDate = input.restitutionDate ? new Date(input.restitutionDate) : null; + const assigned = hasOperationalAssignment(followup); + if (!assigned) { + return "a_attribuer" as const; + } + + if (restitutionDate) { + const delayAlertDate = getDelayAlertDate(restitutionDate, graceDays); + if (now >= delayAlertDate) { + return "retard" as const; + } + + const restitutionDay = new Date(restitutionDate); + restitutionDay.setHours(0, 0, 0, 0); + const currentDay = new Date(now); + currentDay.setHours(0, 0, 0, 0); + if (currentDay >= restitutionDay) { + return "terrain" as const; + } + } + + return "planifie" as const; +} + +export async function sendMaterialReturnFollowupEmail(input: { + followup: NonNullable>>; + request: NonNullable; + association: Awaited>; + baseUrl: string; + tone?: "assignment" | "scheduled" | "overdue" | "reactivation"; +}) { + const tone = input.tone || "scheduled"; + const recipients = getNotificationRecipients(input.followup, tone); + if (recipients.length === 0) { + return { sent: false as const, reason: "no_recipients" as const }; + } + + const uploadLink = buildMaterialReturnUploadLink(input.baseUrl, input.followup.uploadToken); + const pdf = await generateMaterialReturnStatementPdf({ + request: input.request, + association: input.association, + }); + const email = generateMaterialReturnReminderEmail({ + associationName: input.association?.nomAssociation || "Association", + serviceLabel: input.followup.serviceLabel, + restitutionDate: input.followup.restitutionDate, + uploadLink, + requestTitle: input.request.titre, + tone, + }); + + return sendOperationalEmail({ + to: recipients, + subject: email.subject, + text: email.text, + html: email.html, + attachments: [ + { + filename: pdf.fileName, + content: pdf.buffer, + contentType: "application/pdf", + }, + ], + }); +} + +export async function scheduleMaterialReturnFollowup(input: { + request: NonNullable; + serviceLabel?: string; + recipientEmails: string[]; + supervisionServiceLabel?: string; + supervisionRecipientEmails?: string[]; +}, options?: { + forceNewToken?: boolean; + forceImmediateSendWindow?: boolean; + reactivatedByUserId?: number | null; + reactivatedAt?: Date | null; + reactivationReason?: string | null; +}) { + const logisticsSettings = await getLogisticsSettings(); + const restitutionDate = getRestitutionDateFromRequest(input.request); + if (!restitutionDate) return null; + + const existing = await db.getMaterialReturnFollowupByRequestId(input.request.id); + const uploadToken = options?.forceNewToken || !existing?.uploadToken + ? randomBytes(32).toString("hex") + : existing.uploadToken; + const now = new Date(); + const plannedSendAt = options?.forceImmediateSendWindow ? now : getPlannedSendAt(restitutionDate, logisticsSettings.materialReturnLeadDays); + const defaultUploadTokenExpiry = getUploadTokenExpiry(restitutionDate, logisticsSettings.materialReturnGraceDays); + const shouldReactivateWindow = Boolean(options?.forceNewToken || options?.forceImmediateSendWindow); + const uploadTokenExpiresAt = shouldReactivateWindow + ? getReactivatedUploadTokenExpiry(options?.reactivatedAt || now, logisticsSettings.materialReturnGraceDays) + : existing + && existing.status !== "cloture" + && existing.uploadTokenExpiresAt + && new Date(existing.uploadTokenExpiresAt) < now + ? getReactivatedUploadTokenExpiry(now, logisticsSettings.materialReturnGraceDays) + : defaultUploadTokenExpiry; + + await db.upsertMaterialReturnFollowup(input.request.id, { + associationId: input.request.associationId, + serviceLabel: input.serviceLabel || null, + recipientEmails: JSON.stringify(input.recipientEmails), + supervisionServiceLabel: input.supervisionServiceLabel || null, + supervisionRecipientEmails: JSON.stringify(input.supervisionRecipientEmails || []), + restitutionDate, + plannedSendAt, + status: existing?.status === "cloture" ? "cloture" : "planifie", + uploadToken, + uploadTokenExpiresAt, + signedFileKey: existing?.signedFileKey || null, + signedFileUrl: existing?.signedFileUrl || null, + signedFileName: existing?.signedFileName || null, + signedMimeType: existing?.signedMimeType || null, + uploadedByName: existing?.uploadedByName || null, + uploadedByEmail: existing?.uploadedByEmail || null, + uploadedAt: existing?.uploadedAt || null, + closedAt: existing?.closedAt || null, + lastReminderSentAt: existing?.lastReminderSentAt || null, + reactivatedByUserId: options?.reactivatedByUserId ?? existing?.reactivatedByUserId ?? null, + reactivatedAt: options?.reactivatedAt ?? existing?.reactivatedAt ?? null, + reactivationReason: options?.reactivationReason ?? existing?.reactivationReason ?? null, + sentAt: existing?.sentAt || null, + }); + + return db.getMaterialReturnFollowupByRequestId(input.request.id); +} + +export async function runMaterialReturnScheduler(baseUrl: string) { + const now = new Date(); + const logisticsSettings = await getLogisticsSettings(); + + const toEscalate = await db.listMaterialReturnFollowupsToEscalate(); + for (const followup of toEscalate) { + const alertDate = getDelayAlertDate(followup.restitutionDate, logisticsSettings.materialReturnGraceDays); + if (now >= alertDate) { + const shouldSendOverdueAlert = !followup.lastReminderSentAt || new Date(followup.lastReminderSentAt) < alertDate; + if (shouldSendOverdueAlert) { + const request = await db.getRequestById(followup.requestId); + if (request?.status === "validee" && request.type === "demande_materiel_evenementiel") { + const association = await db.getAssociationById(request.associationId); + try { + const result = await sendMaterialReturnFollowupEmail({ + followup, + request, + association, + baseUrl, + tone: "overdue", + }); + if (result.sent) { + await db.updateMaterialReturnFollowup(followup.id, { + lastReminderSentAt: new Date(), + }); + } + } catch (error) { + console.error("[MaterialReturnScheduler] Overdue reminder failed:", error); + } + } + } + + await db.updateMaterialReturnFollowup(followup.id, { + status: "en_cours", + }); + } + } + + const dueFollowups = await db.listPendingMaterialReturnFollowups(now); + for (const followup of dueFollowups) { + const request = await db.getRequestById(followup.requestId); + if (!request || request.status !== "validee" || request.type !== "demande_materiel_evenementiel") { + continue; + } + + const association = await db.getAssociationById(request.associationId); + + try { + const result = await sendMaterialReturnFollowupEmail({ + followup, + request, + association, + baseUrl, + tone: "scheduled", + }); + + if (result.sent) { + await db.updateMaterialReturnFollowup(followup.id, { + status: "en_attente", + sentAt: new Date(), + lastReminderSentAt: new Date(), + }); + } + } catch (error) { + console.error("[MaterialReturnScheduler] Email send failed:", error); + } + } +} + +export function serializeMaterialReturnFollowup(followup: Awaited>) { + if (!followup) return null; + let discrepancyCategories: string[] = []; + try { + const parsed = JSON.parse(followup.discrepancyCategories || "[]"); + if (Array.isArray(parsed)) { + discrepancyCategories = parsed.map((entry) => String(entry || "")).filter(Boolean); + } + } catch { + discrepancyCategories = []; + } + + return { + ...followup, + recipientEmails: parseRecipientEmails(followup.recipientEmails), + supervisionRecipientEmails: parseRecipientEmails(followup.supervisionRecipientEmails), + serviceLabel: typeof followup.serviceLabel === "string" ? followup.serviceLabel : "", + supervisionServiceLabel: typeof followup.supervisionServiceLabel === "string" ? followup.supervisionServiceLabel : "", + status: typeof followup.status === "string" ? followup.status : "planifie", + uploadLinkState: getMaterialReturnUploadLinkState({ + status: followup.status, + uploadTokenExpiresAt: followup.uploadTokenExpiresAt, + }), + compliance: followup.compliance === "conforme" || followup.compliance === "non_conforme" + ? followup.compliance + : null, + issueFlag: Boolean(followup.issueFlag), + litigationStatus: followup.litigationStatus === "pending" ? "pending" : "none", + geoFailureReason: typeof followup.geoFailureReason === "string" ? followup.geoFailureReason : null, + discrepancyCategories, + blockedItems: sanitizeMaterialEventQuantityMap( + (() => { + try { + return followup.blockedItems ? JSON.parse(followup.blockedItems) : null; + } catch { + return null; + } + })() + ), + }; +} diff --git a/server/operationalTour.ts b/server/operationalTour.ts new file mode 100644 index 0000000..87d1787 --- /dev/null +++ b/server/operationalTour.ts @@ -0,0 +1,952 @@ +import { z } from "zod"; +import type { Request } from "express"; +import { COOKIE_NAME } from "@shared/const"; +import type { User } from "../drizzle/schema"; +import { createSessionToken, loginLocalUser } from "./_core/auth"; +import * as db from "./db"; + +export const OPERATIONAL_TOUR_SETTINGS_KEY = "system.operationalTour.settings"; +export const OPERATIONAL_TOUR_HISTORY_KEY = "system.operationalTour.history"; + +export const operationalTourEnvironmentSchema = z.enum(["production", "secondary"]); +export type OperationalTourEnvironment = z.infer; +export const operationalTourAuthModeSchema = z.enum(["local", "oauth"]); +export type OperationalTourAuthMode = z.infer; +export const operationalTourStepKindSchema = z.enum(["technical", "user"]); +export type OperationalTourStepKind = z.infer; + +const operationalTourTargetSchema = z.object({ + baseUrl: z.string().trim().optional().default(""), + associationEmail: z.string().trim().optional().default(""), + associationPassword: z.string().trim().optional().default(""), + associationAuthMode: operationalTourAuthModeSchema.optional().default("local"), + accueilEmail: z.string().trim().optional().default(""), + accueilPassword: z.string().trim().optional().default(""), + accueilAuthMode: operationalTourAuthModeSchema.optional().default("local"), + adminEmail: z.string().trim().optional().default(""), + adminPassword: z.string().trim().optional().default(""), + adminAuthMode: operationalTourAuthModeSchema.optional().default("local"), +}); + +const EMPTY_TARGET = { + baseUrl: "", + associationEmail: "", + associationPassword: "", + associationAuthMode: "local", + accueilEmail: "", + accueilPassword: "", + accueilAuthMode: "local", + adminEmail: "", + adminPassword: "", + adminAuthMode: "local", +} as const; + +const operationalTourSettingsSchema = z.object({ + defaultEnvironment: operationalTourEnvironmentSchema.default("production"), + targets: z.object({ + production: operationalTourTargetSchema.default(EMPTY_TARGET), + secondary: operationalTourTargetSchema.default(EMPTY_TARGET), + }), +}); + +export type OperationalTourSettings = z.infer; + +const operationalTourStepSchema = z.object({ + id: z.string(), + label: z.string(), + kind: operationalTourStepKindSchema.default("technical"), + status: z.enum(["ok", "warning", "error"]), + durationMs: z.number().int().nonnegative(), + message: z.string(), + details: z.string().optional(), + url: z.string().optional(), + critical: z.boolean().default(true), +}); + +export type OperationalTourStep = z.infer; + +const operationalTourReportSchema = z.object({ + id: z.string(), + environment: operationalTourEnvironmentSchema, + environmentLabel: z.string(), + baseUrl: z.string(), + status: z.enum(["ok", "warning", "error"]), + startedAt: z.string(), + finishedAt: z.string(), + durationMs: z.number().int().nonnegative(), + initiatedBy: z.object({ + userId: z.number().int().positive(), + name: z.string(), + email: z.string().optional(), + }), + summary: z.object({ + total: z.number().int().nonnegative(), + ok: z.number().int().nonnegative(), + warning: z.number().int().nonnegative(), + error: z.number().int().nonnegative(), + }), + steps: z.array(operationalTourStepSchema), +}); + +export type OperationalTourReport = z.infer; + +const OPERATIONAL_TOUR_HISTORY_MAX_BYTES = 60_000; +const OPERATIONAL_TOUR_HISTORY_MAX_REPORTS = 12; +const OPERATIONAL_TOUR_DUPLICATE_WINDOW_MS = 15_000; + +const EMPTY_SETTINGS: OperationalTourSettings = { + defaultEnvironment: "production", + targets: { + production: { + baseUrl: "https://www.portail-association973.com", + associationEmail: "", + associationPassword: "", + associationAuthMode: "local", + accueilEmail: "", + accueilPassword: "", + accueilAuthMode: "local", + adminEmail: "", + adminPassword: "", + adminAuthMode: "local", + }, + secondary: { + baseUrl: "", + associationEmail: "", + associationPassword: "", + associationAuthMode: "local", + accueilEmail: "", + accueilPassword: "", + accueilAuthMode: "local", + adminEmail: "", + adminPassword: "", + adminAuthMode: "local", + }, + }, +}; + +type PageCheckOptions = { + id: string; + label: string; + kind: OperationalTourStepKind; + url: string; + cookieHeader?: string | null; + markers?: string[]; + expectedPath?: string; + critical?: boolean; +}; + +type PageCheckResult = OperationalTourStep; + +function normalizeBaseUrl(value: string | undefined) { + return String(value || "").trim().replace(/\/+$/, ""); +} + +function normalizeEmail(value: string | undefined) { + return String(value || "").trim().toLowerCase(); +} + +function buildRequestOrigin(req: Request) { + const forwardedProto = typeof req.headers["x-forwarded-proto"] === "string" ? req.headers["x-forwarded-proto"] : undefined; + const forwardedHost = typeof req.headers["x-forwarded-host"] === "string" ? req.headers["x-forwarded-host"] : undefined; + const protocol = forwardedProto || req.protocol || "http"; + const host = forwardedHost || req.get?.("host") || ""; + return host ? `${protocol}://${host}` : ""; +} + +function isLoginPage(body: string, finalUrl: string) { + return finalUrl.includes("/login") || body.includes("Connexion requise") || body.includes("Se connecter"); +} + +function normalizePathname(url: string) { + try { + const pathname = new URL(url).pathname || "/"; + return pathname === "/" ? "/" : pathname.replace(/\/+$/, ""); + } catch { + return url; + } +} + +function hasReactShell(body: string, contentType: string | null) { + const lowerBody = body.toLowerCase(); + const lowerType = String(contentType || "").toLowerCase(); + return ( + lowerType.includes("text/html") && + (lowerBody.includes("") || lowerBody.includes("script type=\"module\"") || lowerBody.includes("id=\"manus-runtime\"")) + ); +} + +function buildStaticStep(input: Omit & { durationMs?: number }): OperationalTourStep { + return { + ...input, + durationMs: input.durationMs ?? 0, + }; +} + +async function fetchPageCheck(options: PageCheckOptions): Promise { + const started = Date.now(); + try { + const response = await fetch(options.url, { + headers: options.cookieHeader ? { cookie: options.cookieHeader } : undefined, + redirect: "follow", + }); + const body = await response.text(); + const finalUrl = response.url || options.url; + const contentType = response.headers.get("content-type"); + const expectedPath = normalizePathname(options.expectedPath || options.url); + const finalPath = normalizePathname(finalUrl); + const markers = options.markers || []; + const matchedMarker = markers.find((marker) => body.includes(marker)); + const routeMatches = expectedPath === "/" ? finalPath === "/" : finalPath === expectedPath || finalPath.startsWith(`${expectedPath}/`); + const reactShell = hasReactShell(body, contentType); + + if (!response.ok) { + return { + id: options.id, + label: options.label, + kind: options.kind, + status: "error", + durationMs: Date.now() - started, + message: `HTTP ${response.status}`, + details: finalUrl, + url: options.url, + critical: options.critical ?? true, + }; + } + + if (options.cookieHeader && isLoginPage(body, finalUrl)) { + return { + id: options.id, + label: options.label, + kind: options.kind, + status: "error", + durationMs: Date.now() - started, + message: "Authentification non reconnue", + details: finalUrl, + url: options.url, + critical: options.critical ?? true, + }; + } + + if (!routeMatches) { + return { + id: options.id, + label: options.label, + kind: options.kind, + status: "warning", + durationMs: Date.now() - started, + message: "Page servie sur un chemin inattendu", + details: `${finalUrl} (attendu: ${expectedPath})`, + url: options.url, + critical: options.critical ?? true, + }; + } + + if (matchedMarker) { + return { + id: options.id, + label: options.label, + kind: options.kind, + status: "ok", + durationMs: Date.now() - started, + message: `Contrôle réussi (${matchedMarker})`, + details: finalUrl, + url: options.url, + critical: options.critical ?? true, + }; + } + + if (reactShell) { + return { + id: options.id, + label: options.label, + kind: options.kind, + status: "ok", + durationMs: Date.now() - started, + message: "Page accessible, rendu client servi", + details: finalUrl, + url: options.url, + critical: options.critical ?? true, + }; + } + + return { + id: options.id, + label: options.label, + kind: options.kind, + status: markers.length > 0 ? "warning" : "ok", + durationMs: Date.now() - started, + message: markers.length > 0 ? "Page accessible, marqueur visuel non confirmé" : "Contrôle réussi", + details: finalUrl, + url: options.url, + critical: options.critical ?? true, + }; + } catch (error) { + return { + id: options.id, + label: options.label, + kind: options.kind, + status: "error", + durationMs: Date.now() - started, + message: error instanceof Error ? error.message : "Erreur réseau", + url: options.url, + critical: options.critical ?? true, + }; + } +} + +async function fetchJsonCheck(options: { + id: string; + label: string; + kind: OperationalTourStepKind; + url: string; + cookieHeader?: string | null; + markers?: string[]; + critical?: boolean; +}) { + const started = Date.now(); + try { + const response = await fetch(options.url, { + headers: { + accept: "application/json", + ...(options.cookieHeader ? { cookie: options.cookieHeader } : {}), + }, + redirect: "follow", + }); + const body = await response.text(); + const contentType = String(response.headers.get("content-type") || "").toLowerCase(); + const matchesMarker = (options.markers || []).some((marker) => body.includes(marker)); + if (!response.ok) { + return buildStaticStep({ + id: options.id, + label: options.label, + kind: options.kind, + status: "error", + message: `HTTP ${response.status}`, + details: response.url || options.url, + url: options.url, + critical: options.critical ?? true, + durationMs: Date.now() - started, + }); + } + if (!contentType.includes("application/json")) { + return buildStaticStep({ + id: options.id, + label: options.label, + kind: options.kind, + status: "warning", + message: "Réponse non JSON", + details: response.url || options.url, + url: options.url, + critical: options.critical ?? true, + durationMs: Date.now() - started, + }); + } + return buildStaticStep({ + id: options.id, + label: options.label, + kind: options.kind, + status: matchesMarker || !(options.markers || []).length ? "ok" : "warning", + message: matchesMarker || !(options.markers || []).length ? "API accessible et réponse JSON valide" : "API accessible, signature JSON à confirmer", + details: response.url || options.url, + url: options.url, + critical: options.critical ?? true, + durationMs: Date.now() - started, + }); + } catch (error) { + return buildStaticStep({ + id: options.id, + label: options.label, + kind: options.kind, + status: "error", + message: error instanceof Error ? error.message : "Erreur réseau", + url: options.url, + critical: options.critical ?? true, + durationMs: Date.now() - started, + }); + } +} + +async function fetchHomeDiagnostics(baseUrl: string) { + const started = Date.now(); + try { + const response = await fetch(`${baseUrl}/`, { redirect: "follow" }); + const body = await response.text(); + return { + durationMs: Date.now() - started, + body, + headers: response.headers, + ok: response.ok, + finalUrl: response.url || `${baseUrl}/`, + }; + } catch (error) { + return { + durationMs: Date.now() - started, + body: "", + headers: new Headers(), + ok: false, + finalUrl: `${baseUrl}/`, + error: error instanceof Error ? error.message : "Erreur réseau", + }; + } +} + +async function createConfiguredSessionCookie(email: string, password?: string) { + const normalizedEmail = normalizeEmail(email); + const normalizedPassword = String(password || ""); + let user; + + if (normalizedPassword) { + user = await loginLocalUser({ email: normalizedEmail, password: normalizedPassword }); + } else { + const existingUser = await db.getUserByEmail(normalizedEmail); + if (!existingUser) { + throw new Error("Compte introuvable"); + } + if (!existingUser.isActive) { + throw new Error("Compte inactif"); + } + if (!existingUser.loginMethod || !existingUser.loginMethod.endsWith("_oauth")) { + throw new Error("Mot de passe requis pour ce compte"); + } + user = existingUser; + } + + const token = await createSessionToken(user); + return `${COOKIE_NAME}=${token}`; +} + +async function tryFindPublicProfileUrl(baseUrl: string) { + try { + const response = await fetch(`${baseUrl}/associations`, { redirect: "follow" }); + if (!response.ok) return null; + const body = await response.text(); + const match = body.match(/href="(\/associations\/[^"]+)"/); + if (match) return `${baseUrl}${match[1]}`; + } catch { + // fallback below + } + + try { + const directory = await db.listAssociationDirectoryMapEntries({ limit: 1 }); + const firstEntry = directory.data?.[0]; + return firstEntry?.id ? `${baseUrl}/associations/${firstEntry.id}` : null; + } catch { + return null; + } +} + +function computeReportStatus(steps: OperationalTourStep[]): OperationalTourReport["status"] { + const hasCriticalError = steps.some((step) => step.critical !== false && step.status === "error"); + if (hasCriticalError) return "error"; + const hasWarning = steps.some((step) => step.status !== "ok"); + return hasWarning ? "warning" : "ok"; +} + +function summarizeSteps(steps: OperationalTourStep[]) { + return { + total: steps.length, + ok: steps.filter((step) => step.status === "ok").length, + warning: steps.filter((step) => step.status === "warning").length, + error: steps.filter((step) => step.status === "error").length, + }; +} + +function summarizeStepsByKind(steps: OperationalTourStep[], kind: OperationalTourStepKind) { + return summarizeSteps(steps.filter((step) => step.kind === kind)); +} + +export function sanitizeOperationalTourSettings(input?: unknown): OperationalTourSettings { + const parsed = operationalTourSettingsSchema.safeParse(input); + if (!parsed.success) return EMPTY_SETTINGS; + return { + defaultEnvironment: parsed.data.defaultEnvironment, + targets: { + production: { + ...EMPTY_SETTINGS.targets.production, + ...parsed.data.targets.production, + baseUrl: normalizeBaseUrl(parsed.data.targets.production.baseUrl) || EMPTY_SETTINGS.targets.production.baseUrl, + associationAuthMode: parsed.data.targets.production.associationAuthMode || "local", + accueilAuthMode: parsed.data.targets.production.accueilAuthMode || "local", + adminAuthMode: parsed.data.targets.production.adminAuthMode || "local", + }, + secondary: { + ...EMPTY_SETTINGS.targets.secondary, + ...parsed.data.targets.secondary, + baseUrl: normalizeBaseUrl(parsed.data.targets.secondary.baseUrl), + associationAuthMode: parsed.data.targets.secondary.associationAuthMode || "local", + accueilAuthMode: parsed.data.targets.secondary.accueilAuthMode || "local", + adminAuthMode: parsed.data.targets.secondary.adminAuthMode || "local", + }, + }, + }; +} + +export async function getOperationalTourSettings() { + const raw = await db.getPortalSetting(OPERATIONAL_TOUR_SETTINGS_KEY); + if (!raw) return EMPTY_SETTINGS; + try { + return sanitizeOperationalTourSettings(JSON.parse(raw)); + } catch { + return EMPTY_SETTINGS; + } +} + +export async function saveOperationalTourSettings(settings: OperationalTourSettings) { + const sanitized = sanitizeOperationalTourSettings(settings); + await db.setPortalSetting( + OPERATIONAL_TOUR_SETTINGS_KEY, + JSON.stringify(sanitized), + "Configuration du tour opérationnel du site" + ); + return sanitized; +} + +export async function getOperationalTourHistory() { + const raw = await db.getPortalSetting(OPERATIONAL_TOUR_HISTORY_KEY); + if (!raw) return [] as OperationalTourReport[]; + try { + const parsed = z.array(operationalTourReportSchema).parse(JSON.parse(raw)); + return parsed; + } catch { + return [] as OperationalTourReport[]; + } +} + +function compactOperationalTourHistory(history: OperationalTourReport[]) { + const compactStep = (step: OperationalTourStep): OperationalTourStep => ({ + id: step.id, + label: step.label, + kind: step.kind, + status: step.status, + durationMs: step.durationMs, + message: step.message, + critical: step.critical ?? true, + ...(step.status !== "ok" && step.details ? { details: step.details } : {}), + ...(step.status !== "ok" && step.url ? { url: step.url } : {}), + }); + + const buildSignature = (report: OperationalTourReport) => JSON.stringify({ + environment: report.environment, + baseUrl: report.baseUrl, + status: report.status, + summary: report.summary, + steps: report.steps.map((step) => ({ + id: step.id, + kind: step.kind, + status: step.status, + message: step.message, + details: step.details || "", + })), + }); + + const dedupedHistory: OperationalTourReport[] = []; + for (const report of history) { + const previous = dedupedHistory[dedupedHistory.length - 1]; + if (previous && previous.environment === report.environment) { + const previousTime = new Date(previous.finishedAt).getTime(); + const currentTime = new Date(report.finishedAt).getTime(); + if ( + Number.isFinite(previousTime) && + Number.isFinite(currentTime) && + Math.abs(currentTime - previousTime) <= OPERATIONAL_TOUR_DUPLICATE_WINDOW_MS && + buildSignature(previous) === buildSignature(report) + ) { + dedupedHistory[dedupedHistory.length - 1] = report; + continue; + } + } + dedupedHistory.push(report); + } + + let reports = dedupedHistory.slice(0, OPERATIONAL_TOUR_HISTORY_MAX_REPORTS).map((report) => ({ + ...report, + steps: report.steps.map(compactStep), + })); + + while (reports.length > 0) { + const serialized = JSON.stringify(reports); + if (Buffer.byteLength(serialized, "utf8") <= OPERATIONAL_TOUR_HISTORY_MAX_BYTES) { + return reports; + } + reports = reports.slice(0, -1); + } + + return []; +} + +export async function saveOperationalTourHistory(history: OperationalTourReport[]) { + await db.setPortalSetting( + OPERATIONAL_TOUR_HISTORY_KEY, + JSON.stringify(compactOperationalTourHistory(history)), + "Historique du tour opérationnel du site" + ); +} + +export async function clearOperationalTourHistory() { + const history = await getOperationalTourHistory(); + const seen = new Set(); + const retained = history.filter((report) => { + if (seen.has(report.environment)) { + return false; + } + seen.add(report.environment); + return true; + }); + await saveOperationalTourHistory(retained); + return retained; +} + +export async function runOperationalTour(params: { + environment: OperationalTourEnvironment; + req: Request; + currentUser: User; +}) { + const startedAt = new Date(); + const settings = await getOperationalTourSettings(); + const target = settings.targets[params.environment]; + const baseUrl = normalizeBaseUrl(target.baseUrl); + const environmentLabel = params.environment === "production" ? "Production" : "Préproduction / Local"; + const steps: OperationalTourStep[] = []; + + if (!baseUrl) { + const report: OperationalTourReport = { + id: `tour-${Date.now()}`, + environment: params.environment, + environmentLabel, + baseUrl: "", + status: "error", + startedAt: startedAt.toISOString(), + finishedAt: new Date().toISOString(), + durationMs: 0, + initiatedBy: { + userId: params.currentUser.id, + name: params.currentUser.name || params.currentUser.email || `Utilisateur #${params.currentUser.id}`, + email: params.currentUser.email || undefined, + }, + summary: { total: 1, ok: 0, warning: 0, error: 1 }, + steps: [ + { + id: "configuration", + label: "Configuration de l’environnement", + kind: "technical", + status: "error", + durationMs: 0, + message: "URL cible manquante", + details: `Aucune URL n’est configurée pour ${environmentLabel.toLowerCase()}.`, + critical: true, + }, + ], + }; + const nextHistory = [report, ...(await getOperationalTourHistory())]; + await saveOperationalTourHistory(nextHistory); + return report; + } + + const currentOrigin = buildRequestOrigin(params.req); + const isSameOrigin = currentOrigin && normalizeBaseUrl(currentOrigin) === baseUrl; + const incomingCookieHeader = typeof params.req.headers.cookie === "string" ? params.req.headers.cookie : ""; + + const homeDiagnostics = await fetchHomeDiagnostics(baseUrl); + + if (homeDiagnostics.ok) { + steps.push( + buildStaticStep({ + id: "technical-performance-home", + label: "Performance de l’accueil", + kind: "technical", + status: homeDiagnostics.durationMs <= 1500 ? "ok" : homeDiagnostics.durationMs <= 3000 ? "warning" : "error", + message: + homeDiagnostics.durationMs <= 1500 + ? "Temps de réponse confortable" + : homeDiagnostics.durationMs <= 3000 + ? "Temps de réponse correct mais à surveiller" + : "Temps de réponse trop élevé", + details: `${homeDiagnostics.durationMs} ms`, + critical: false, + durationMs: homeDiagnostics.durationMs, + }), + buildStaticStep({ + id: "technical-security-baseline", + label: "Sécurité de base", + kind: "technical", + status: baseUrl.startsWith("https://") ? "ok" : "warning", + message: baseUrl.startsWith("https://") ? "Le site est servi en HTTPS" : "Le site n’utilise pas HTTPS", + details: homeDiagnostics.finalUrl, + critical: false, + }), + buildStaticStep({ + id: "technical-accessibility-baseline", + label: "Accessibilité de base", + kind: "technical", + status: + homeDiagnostics.body.includes("") + && /<html[^>]+lang=/i.test(homeDiagnostics.body) + ? "ok" + : "warning", + message: + homeDiagnostics.body.includes("<title>") + && /<html[^>]+lang=/i.test(homeDiagnostics.body) + ? "Titre de page et langue du document détectés" + : "Titre ou langue du document à confirmer", + details: homeDiagnostics.finalUrl, + critical: false, + }), + buildStaticStep({ + id: "technical-mobile-baseline", + label: "Compatibilité mobile", + kind: "technical", + status: /<meta[^>]+name=["']viewport["']/i.test(homeDiagnostics.body) ? "ok" : "warning", + message: /<meta[^>]+name=["']viewport["']/i.test(homeDiagnostics.body) + ? "Viewport mobile détecté" + : "Meta viewport absente ou non détectée", + details: homeDiagnostics.finalUrl, + critical: false, + }) + ); + } else { + steps.push( + buildStaticStep({ + id: "technical-performance-home", + label: "Performance de l’accueil", + kind: "technical", + status: "error", + message: homeDiagnostics.error || "Impossible de mesurer l’accueil", + details: homeDiagnostics.finalUrl, + critical: false, + durationMs: homeDiagnostics.durationMs, + }) + ); + } + + steps.push( + await fetchJsonCheck({ + id: "technical-api-public", + label: "API publique", + kind: "technical", + url: `${baseUrl}/api/trpc/auth.providers?input=%7B%7D`, + markers: ['"google"', '"facebook"'], + }) + ); + + let adminCookieHeader: string | null = isSameOrigin && incomingCookieHeader ? incomingCookieHeader : null; + const hasConfiguredAdminAccount = Boolean( + target.adminEmail && + (target.adminAuthMode === "oauth" || target.adminPassword) + ); + if (!adminCookieHeader && hasConfiguredAdminAccount) { + try { + adminCookieHeader = await createConfiguredSessionCookie(target.adminEmail, target.adminAuthMode === "oauth" ? "" : target.adminPassword); + } catch (error) { + const usesOAuthOnly = target.adminAuthMode === "oauth"; + steps.push({ + id: "admin-config", + label: "Configuration compte admin de contrôle", + kind: "user", + status: "warning", + durationMs: 0, + message: usesOAuthOnly ? "Impossible d’ouvrir une session admin OAuth" : "Impossible d’ouvrir une session admin dédiée", + details: error instanceof Error ? error.message : usesOAuthOnly ? "Compte OAuth admin introuvable" : "Identifiants admin invalides", + critical: false, + }); + } + } + + let associationCookieHeader: string | null = null; + const hasConfiguredAssociationAccount = Boolean( + target.associationEmail && + (target.associationAuthMode === "oauth" || target.associationPassword) + ); + if (hasConfiguredAssociationAccount) { + try { + associationCookieHeader = await createConfiguredSessionCookie( + target.associationEmail, + target.associationAuthMode === "oauth" ? "" : target.associationPassword + ); + } catch (error) { + const usesOAuthOnly = target.associationAuthMode === "oauth"; + steps.push({ + id: "association-config", + label: "Configuration compte association de test", + kind: "user", + status: "warning", + durationMs: 0, + message: usesOAuthOnly ? "Impossible d’ouvrir une session association OAuth" : "Impossible d’ouvrir une session association", + details: error instanceof Error ? error.message : usesOAuthOnly ? "Compte OAuth association introuvable" : "Identifiants association invalides", + critical: false, + }); + } + } else { + steps.push({ + id: "association-config", + label: "Configuration compte association de test", + kind: "user", + status: "warning", + durationMs: 0, + message: "Compte association non configuré", + details: "Les contrôles authentifiés association sont ignorés tant que l’email et le mot de passe ne sont pas renseignés.", + critical: false, + }); + } + + let accueilCookieHeader: string | null = null; + const hasConfiguredAccueilAccount = Boolean( + target.accueilEmail && + (target.accueilAuthMode === "oauth" || target.accueilPassword) + ); + if (hasConfiguredAccueilAccount) { + try { + accueilCookieHeader = await createConfiguredSessionCookie( + target.accueilEmail, + target.accueilAuthMode === "oauth" ? "" : target.accueilPassword + ); + } catch (error) { + const usesOAuthOnly = target.accueilAuthMode === "oauth"; + steps.push(buildStaticStep({ + id: "accueil-config", + label: "Configuration compte accueil / hôtesse", + kind: "user", + status: "warning", + message: usesOAuthOnly ? "Impossible d’ouvrir une session accueil OAuth" : "Impossible d’ouvrir une session accueil", + details: error instanceof Error ? error.message : usesOAuthOnly ? "Compte OAuth accueil introuvable" : "Identifiants accueil invalides", + critical: false, + })); + } + } else { + steps.push(buildStaticStep({ + id: "accueil-config", + label: "Configuration compte accueil / hôtesse", + kind: "user", + status: "warning", + message: "Compte accueil non configuré", + details: "Le parcours hôtesse reste ignoré tant qu’un compte accueil n’est pas renseigné.", + critical: false, + })); + } + + steps.push( + await fetchPageCheck({ id: "technical-public-home", label: "Navigation publique • Accueil", kind: "technical", url: `${baseUrl}/`, expectedPath: "/", markers: ["Portail Associations", "<title>Portail Associations"] }), + await fetchPageCheck({ id: "technical-public-login", label: "Navigation publique • Connexion", kind: "technical", url: `${baseUrl}/login`, expectedPath: "/login", markers: ["Connexion", "Se connecter"] }), + await fetchPageCheck({ id: "technical-directory", label: "Annuaire public des associations", kind: "technical", url: `${baseUrl}/associations`, expectedPath: "/associations", markers: ["Annuaire des associations", "associationDirectory.listPortal"] }), + await fetchPageCheck({ id: "technical-map", label: "Cartographie des associations", kind: "technical", url: `${baseUrl}/associations/carte`, expectedPath: "/associations/carte", markers: ["Cartographie interactive", "associationDirectory.listMap"] }), + ); + + const publicProfileUrl = await tryFindPublicProfileUrl(baseUrl); + if (publicProfileUrl) { + steps.push( + await fetchPageCheck({ + id: "technical-public-profile", + label: "Fiche publique association", + kind: "technical", + url: publicProfileUrl, + expectedPath: new URL(publicProfileUrl).pathname, + markers: ["Coordonnées", "actualités et publications"], + critical: false, + }) + ); + } else { + steps.push(buildStaticStep({ + id: "technical-public-profile", + label: "Fiche publique association", + kind: "technical", + status: "warning", + durationMs: 0, + message: "Aucune fiche publique détectée", + details: "Le tour n’a pas trouvé de lien de fiche association dans l’annuaire public.", + critical: false, + })); + } + + if (associationCookieHeader) { + steps.push( + await fetchJsonCheck({ + id: "technical-api-association", + label: "API association", + kind: "technical", + url: `${baseUrl}/api/trpc/auth.me?input=%7B%7D`, + cookieHeader: associationCookieHeader, + markers: ['"email"', '"role"'], + }), + await fetchPageCheck({ id: "user-association-dashboard", label: "Parcours association • Tableau de bord", kind: "user", url: `${baseUrl}/dashboard`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard", markers: ["Tableau de bord", "Portail Associations"] }), + await fetchPageCheck({ id: "user-association-requests", label: "Parcours association • Mes demandes", kind: "user", url: `${baseUrl}/dashboard/requests`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/requests", markers: ["Mes demandes"] }), + await fetchPageCheck({ id: "user-association-documents", label: "Parcours association • Mes documents", kind: "user", url: `${baseUrl}/dashboard/documents`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/documents", markers: ["Mes documents"] }), + await fetchPageCheck({ id: "user-association-directory", label: "Parcours association • Annuaire", kind: "user", url: `${baseUrl}/dashboard/associations`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/associations", markers: ["Annuaire des associations"], critical: false }), + await fetchPageCheck({ id: "user-association-form-salle", label: "Formulaire • Réservation de salle", kind: "user", url: `${baseUrl}/dashboard/reservation-salle`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/reservation-salle", markers: ["Réservation de salle", "Soumettre"] }), + await fetchPageCheck({ id: "user-association-form-request", label: "Formulaire • Nouvelle demande", kind: "user", url: `${baseUrl}/dashboard/requests/new`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/requests/new", markers: ["Nouvelle demande", "Soumettre"] }), + await fetchPageCheck({ id: "user-association-form-material", label: "Formulaire • Matériel événementiel", kind: "user", url: `${baseUrl}/dashboard/materiel-evenementiel`, cookieHeader: associationCookieHeader, expectedPath: "/dashboard/materiel-evenementiel", markers: ["matériel événementiel", "Soumettre"], critical: false }), + ); + } + + if (accueilCookieHeader) { + steps.push( + await fetchJsonCheck({ + id: "technical-api-accueil", + label: "API accueil / hôtesse", + kind: "technical", + url: `${baseUrl}/api/trpc/auth.me?input=%7B%7D`, + cookieHeader: accueilCookieHeader, + markers: ['"accueil"'], + critical: false, + }), + await fetchPageCheck({ id: "user-accueil-dashboard", label: "Parcours hôtesse • Administration", kind: "user", url: `${baseUrl}/admin`, cookieHeader: accueilCookieHeader, expectedPath: "/admin", markers: ["Administration accueil", "Portail Associations"] }), + await fetchPageCheck({ id: "user-accueil-requests", label: "Parcours hôtesse • Gestion des demandes", kind: "user", url: `${baseUrl}/admin?tab=requests`, cookieHeader: accueilCookieHeader, expectedPath: "/admin", markers: ["Gestion des demandes"] }), + await fetchPageCheck({ id: "user-accueil-calendar", label: "Parcours hôtesse • Calendrier", kind: "user", url: `${baseUrl}/admin?tab=calendrier`, cookieHeader: accueilCookieHeader, expectedPath: "/admin", markers: ["Calendrier"] }), + await fetchPageCheck({ id: "user-accueil-notifications", label: "Parcours hôtesse • Notifications", kind: "user", url: `${baseUrl}/admin?tab=notifications`, cookieHeader: accueilCookieHeader, expectedPath: "/admin", markers: ["Notifications"], critical: false }), + ); + } + + if (adminCookieHeader) { + steps.push( + await fetchJsonCheck({ + id: "technical-api-admin", + label: "API administration", + kind: "technical", + url: `${baseUrl}/api/internal/stats/reservations?period=month`, + cookieHeader: adminCookieHeader, + markers: ['"summary"', '"history"'], + }), + await fetchPageCheck({ id: "user-admin-dashboard", label: "Parcours administrateur • Administration", kind: "user", url: `${baseUrl}/admin`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Tableau de bord", "Portail Associations"] }), + await fetchPageCheck({ id: "user-admin-requests", label: "Parcours administrateur • Gestion des demandes", kind: "user", url: `${baseUrl}/admin?tab=requests`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Gestion des demandes"] }), + await fetchPageCheck({ id: "user-admin-calendar", label: "Parcours administrateur • Calendrier", kind: "user", url: `${baseUrl}/admin?tab=calendrier`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Calendrier"] }), + await fetchPageCheck({ id: "user-admin-analytics", label: "Parcours administrateur • Statistiques", kind: "user", url: `${baseUrl}/admin?tab=analytics`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Statistiques d’activité", "Tour opérationnel du site"] }), + await fetchPageCheck({ id: "user-admin-ccds-directory", label: "Parcours administrateur • Annuaire interne CCDS", kind: "user", url: `${baseUrl}/admin?tab=ccds-directory`, cookieHeader: adminCookieHeader, expectedPath: "/admin", markers: ["Annuaire interne CCDS"] }), + ); + } else { + steps.push(buildStaticStep({ + id: "admin-config", + label: "Configuration session admin", + kind: "user", + status: "warning", + durationMs: 0, + message: "Aucune session admin disponible pour cet environnement", + details: isSameOrigin + ? "Le navigateur n’a pas transmis de cookie de session exploitable." + : "Renseignez un compte admin dédié si vous voulez tester un autre environnement que celui où vous êtes connecté.", + critical: false, + })); + } + + const finishedAt = new Date(); + const report: OperationalTourReport = { + id: `tour-${finishedAt.getTime()}`, + environment: params.environment, + environmentLabel, + baseUrl, + status: computeReportStatus(steps), + startedAt: startedAt.toISOString(), + finishedAt: finishedAt.toISOString(), + durationMs: finishedAt.getTime() - startedAt.getTime(), + initiatedBy: { + userId: params.currentUser.id, + name: params.currentUser.name || params.currentUser.email || `Utilisateur #${params.currentUser.id}`, + email: params.currentUser.email || undefined, + }, + summary: summarizeSteps(steps), + steps, + }; + + const nextHistory = [report, ...(await getOperationalTourHistory())]; + await saveOperationalTourHistory(nextHistory); + + return report; +} diff --git a/server/pdfGenerator.test.ts b/server/pdfGenerator.test.ts new file mode 100644 index 0000000..6b0f1aa --- /dev/null +++ b/server/pdfGenerator.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect } from 'vitest'; +import { generateMaterialEventPDF, generateReservationPDF } from './pdfGenerator'; + +describe('generateReservationPDF', () => { + it('should generate a PDF buffer for a complete reservation', async () => { + const data = { + titre: 'Réservation Salle TOUCAN - 15/04/2026', + status: 'soumise', + createdAt: new Date('2026-03-01'), + dateSubmission: new Date('2026-03-01'), + dateTraitement: null, + commentaireAdmin: null, + formData: { + nomAssociation: 'Association Test', + adresseAssociation: '12 rue de la Paix, 97300 Cayenne', + communeSiege: 'Cayenne', + representantLegal: 'Jean Dupont', + telephoneAssociation: '0694123456', + emailAssociation: 'test@asso.fr', + sallesSelectionnees: ['Salle TOUCAN', 'Salle IBIS'], + motifReservation: 'Assemblée générale annuelle', + dateReservation: '2026-04-15', + dateFinReservation: '2026-04-15', + heureDebut: '09:00', + heureFin: '17:00', + nombreParticipants: '50', + materielNecessaire: 'Tables, chaises, vidéoprojecteur', + observations: 'Prévoir un accès PMR', + }, + }; + + const buffer = await generateReservationPDF(data); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.length).toBeGreaterThan(0); + // Verify PDF header + expect(buffer.slice(0, 5).toString()).toBe('%PDF-'); + }); + + it('should generate a PDF with DSU cadre filled', async () => { + const data = { + titre: 'Réservation Salle PELICAN', + status: 'validee', + createdAt: new Date('2026-03-01'), + dateSubmission: new Date('2026-03-01'), + dateTraitement: new Date('2026-03-10'), + commentaireAdmin: 'Demande acceptée sous conditions', + formData: { + nomAssociation: 'Association Sportive', + adresseAssociation: '5 avenue des Sports, 97360 Mana', + communeSiege: 'Mana', + representantLegal: 'Marie Martin', + telephoneAssociation: '0694567890', + emailAssociation: 'sport@asso.fr', + sallesSelectionnees: ['Salle PELICAN'], + motifReservation: 'Tournoi de basketball', + dateReservation: '2026-05-20', + dateFinReservation: '2026-05-21', + heureDebut: '08:00', + heureFin: '20:00', + nombreParticipants: '100', + materielNecessaire: 'Sono, tables de score', + observations: '', + cadreDSU: { + dateReception: '2026-03-02', + avisDSU: 'favorable_avec_reserves', + conditionsParticulieres: 'Nettoyage obligatoire après utilisation', + cautionRequise: true, + montantCaution: '500', + assuranceRequise: true, + horairesImposes: '08:00 - 20:00 strictement', + responsableDSU: 'Pierre Durand', + dateDecision: '2026-03-10', + observationsDSU: 'Veiller au respect des horaires de fermeture', + }, + }, + }; + + const buffer = await generateReservationPDF(data); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.length).toBeGreaterThan(0); + expect(buffer.slice(0, 5).toString()).toBe('%PDF-'); + // PDF with DSU should be larger than without + expect(buffer.length).toBeGreaterThan(1000); + }); + + it('should generate a PDF with empty DSU cadre (pending)', async () => { + const data = { + titre: 'Réservation DOJO', + status: 'soumise', + createdAt: new Date('2026-03-05'), + formData: { + nomAssociation: 'Club Judo', + sallesSelectionnees: ['DOJO'], + motifReservation: 'Entraînement hebdomadaire', + dateReservation: '2026-04-01', + heureDebut: '18:00', + heureFin: '20:00', + }, + }; + + const buffer = await generateReservationPDF(data); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.length).toBeGreaterThan(0); + expect(buffer.slice(0, 5).toString()).toBe('%PDF-'); + }); + + it('should handle missing optional fields gracefully', async () => { + const data = { + titre: 'Réservation minimale', + status: 'brouillon', + createdAt: new Date(), + formData: {}, + }; + + const buffer = await generateReservationPDF(data); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.length).toBeGreaterThan(0); + expect(buffer.slice(0, 5).toString()).toBe('%PDF-'); + }); +}); + +describe('generateMaterialEventPDF', () => { + it('should generate a PDF buffer for an event material request', async () => { + const data = { + titre: 'Demande matériel événementiel', + status: 'validee', + createdAt: new Date('2026-03-01'), + dateSubmission: new Date('2026-03-01'), + dateTraitement: new Date('2026-03-10'), + commentaireAdmin: 'Accord partiel', + formData: { + nomAssociation: 'Association Test', + commune: 'Kourou', + direction: 'Sports', + service: 'Animation', + demandeurNomPrenom: 'Jean Dupont', + dateDemande: '2026-03-01', + dateManifestation: '2026-04-15', + dateDebutManifestation: '2026-04-15', + dateFinManifestation: '2026-04-16', + motifDemande: 'Fête communale', + datePriseEnCharge: '2026-04-15', + dateRestitution: '2026-04-19', + materielsDemandes: { tente3x3: true, podium: true, chapiteau5x5: false, autres: true }, + quantitesDemandees: { tente3x3: '5', podium: '1', chapiteau5x5: '', autres: '2' }, + autreMaterielPrecisions: 'Barrières', + cadreDSU: { + dateReception: '2026-03-02', + responsableDSU: 'Pierre Durand', + dateDecision: '2026-03-10', + observationsDSU: 'Prévoir le retrait la veille', + materielEvent: { + itemsAccordes: { tente3x3: true, podium: true, chapiteau5x5: false, autres: false }, + quantitesAccordees: { tente3x3: '3', podium: '1', chapiteau5x5: '', autres: '' }, + autresPrecisions: '', + }, + }, + }, + }; + + const buffer = await generateMaterialEventPDF(data); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.length).toBeGreaterThan(0); + expect(buffer.slice(0, 5).toString()).toBe('%PDF-'); + }); +}); diff --git a/server/pdfGenerator.ts b/server/pdfGenerator.ts new file mode 100644 index 0000000..2b62d46 --- /dev/null +++ b/server/pdfGenerator.ts @@ -0,0 +1,958 @@ +import PDFDocument from 'pdfkit'; +import fs from 'node:fs'; +import path from 'node:path'; +import { SALLE_FREQUENCY_LABELS, SALLE_USAGE_TYPE_LABELS } from '@shared/sallePricing'; + +interface ReservationPDFData { + titre: string; + status: string; + createdAt: string | Date; + dateSubmission?: string | Date | null; + dateTraitement?: string | Date | null; + commentaireAdmin?: string | null; + formData: { + nomAssociation?: string; + adresseAssociation?: string; + communeSiege?: string; + representantLegal?: string; + telephoneAssociation?: string; + emailAssociation?: string; + sallesSelectionnees?: string[]; + motifReservation?: string; + dateReservation?: string; + dateFinReservation?: string; + heureDebut?: string; + heureFin?: string; + useDetailedSchedule?: boolean; + horairesParJour?: Array<{ + date?: string; + heureDebut?: string; + heureFin?: string; + }>; + typeUsage?: 'conventionne' | 'occasionnel'; + frequence?: 'demi_journee' | 'journee' | 'mensuel'; + nombreParticipants?: string; + besoinsComplementaires?: string; + materielNecessaire?: string; + observations?: string; + salleWorkflow?: { + pricing?: { + totalAmountCents?: number; + }; + directorSignedAt?: string; + directorSignedByName?: string; + }; + cadreDSU?: { + dateReception?: string; + avisDSU?: string; + conditionsParticulieres?: string; + cautionRequise?: boolean; + montantCaution?: string; + assuranceRequise?: boolean; + horairesImposes?: string; + responsableDSU?: string; + dateDecision?: string; + observationsDSU?: string; + materielEvent?: { + financialDecision?: { + financialMode?: 'gratuite' | 'gratuite_avec_caution' | 'location_payante'; + depositRequired?: boolean; + depositAmountCents?: number; + rentalAmountCents?: number; + pricingNotes?: string; + contractStatus?: 'a_generer' | 'generee' | 'signee' | 'refusee' | 'annulee'; + }; + }; + }; + }; +} + +interface MaterialEventPDFData { + titre: string; + status: string; + createdAt: string | Date; + dateSubmission?: string | Date | null; + dateTraitement?: string | Date | null; + commentaireAdmin?: string | null; + formData: { + nomAssociation?: string; + adresseAssociation?: string; + communeSiege?: string; + representantLegal?: string; + telephoneAssociation?: string; + emailAssociation?: string; + commune?: string; + direction?: string; + service?: string; + demandeurNomPrenom?: string; + dateDemande?: string; + dateManifestation?: string; + dateDebutManifestation?: string; + dateFinManifestation?: string; + motifDemande?: string; + datePriseEnCharge?: string; + dateRestitution?: string; + autreMaterielPrecisions?: string; + materielsDemandes?: Record; + quantitesDemandees?: Record; + cadreDSU?: { + dateReception?: string; + cautionRequise?: boolean; + montantCaution?: string; + responsableDSU?: string; + dateDecision?: string; + observationsDSU?: string; + materielEvent?: { + itemsAccordes?: Record; + quantitesAccordees?: Record; + autresPrecisions?: string; + financialDecision?: { + financialMode?: 'gratuite' | 'gratuite_avec_caution' | 'location_payante'; + depositRequired?: boolean; + depositAmountCents?: number; + rentalAmountCents?: number; + pricingNotes?: string; + contractStatus?: 'a_generer' | 'generee' | 'signee' | 'refusee' | 'annulee'; + contractGeneratedAt?: string | Date | null; + }; + }; + }; + }; +} + +const statusLabels: Record = { + brouillon: 'Brouillon', + soumise: 'Soumise', + en_cours_traitement: 'En cours de traitement', + information_complementaire: 'Information complémentaire requise', + validee: 'Validée', + refusee: 'Refusée', +}; + +const avisDSULabels: Record = { + favorable: 'Favorable', + defavorable: 'Défavorable', + favorable_avec_reserves: 'Favorable avec réserves', +}; + +const materialEventLabels: Record = { + tente3x3: 'Tente 3x3', + chapiteau5x5: 'Chapiteau 5x5', + podium: 'Podium', + autres: 'Autres', +}; + +const CCDS_LOGO_PATH = path.resolve(process.cwd(), 'client/src/assets/ccds.png'); + +function formatDateFr(dateStr: string | Date | null | undefined): string { + if (!dateStr) return '-'; + try { + const d = new Date(dateStr); + return d.toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'long', + year: 'numeric', + }); + } catch { + return String(dateStr); + } +} + +function formatCurrency(cents: number | null | undefined): string { + if (cents === null || cents === undefined || !Number.isFinite(cents)) return '-'; + return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(cents / 100); +} + +function getFinancialModeLabel(mode: string | null | undefined): string { + switch (mode) { + case 'gratuite': + return 'Mise à disposition gratuite'; + case 'gratuite_avec_caution': + return 'Mise à disposition gratuite avec caution'; + case 'location_payante': + return 'Location payante'; + default: + return '-'; + } +} + +function getContractStatusLabel(status: string | null | undefined): string { + switch (status) { + case 'a_generer': + return 'À générer'; + case 'generee': + return 'Générée'; + case 'signee': + return 'Signée'; + case 'refusee': + return 'Refusée'; + case 'annulee': + return 'Annulée'; + default: + return '-'; + } +} + +function formatMaterialEventPeriod(formData: { + dateManifestation?: string; + dateDebutManifestation?: string; + dateFinManifestation?: string; +}): string { + const start = formData.dateDebutManifestation || formData.dateManifestation; + const end = formData.dateFinManifestation || formData.dateManifestation; + + if (!start && !end) return '-'; + if (start && end && start !== end) { + return `${formatDateFr(start)} au ${formatDateFr(end)}`; + } + return formatDateFr(start || end); +} + +function drawCcdsLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) { + if (!fs.existsSync(CCDS_LOGO_PATH)) return; + try { + doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: 'center', valign: 'center' }); + } catch (error) { + console.warn('[PDF] Impossible de charger le logo CCDS:', error); + } +} + +// Color definitions (RGB) +const COLORS = { + primary: [0, 0, 102] as [number, number, number], // oklch(0.25 0.05 265) approx + primaryLight: [230, 230, 245] as [number, number, number], + text: [30, 30, 30] as [number, number, number], + muted: [120, 120, 120] as [number, number, number], + border: [200, 200, 200] as [number, number, number], + white: [255, 255, 255] as [number, number, number], + green: [34, 139, 34] as [number, number, number], + red: [220, 20, 60] as [number, number, number], + amber: [200, 150, 0] as [number, number, number], + blueBg: [235, 245, 255] as [number, number, number], + blueBorder: [180, 210, 240] as [number, number, number], + blueText: [50, 80, 140] as [number, number, number], +}; + +export function generateReservationPDF(data: ReservationPDFData): Promise { + return new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: 'A4', + margins: { top: 40, bottom: 40, left: 50, right: 50 }, + info: { + Title: `Formulaire de Réservation - ${data.formData.nomAssociation || 'Association'}`, + Author: 'Communauté de Communes Des Savanes', + Subject: 'Réservation de local - Maison de la Jeunesse des Savanes', + }, + }); + + const chunks: Buffer[] = []; + doc.on('data', (chunk: Buffer) => chunks.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + + const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + const dateDebut = formatDateFr(data.formData.dateReservation); + const dateFin = data.formData.dateFinReservation && data.formData.dateFinReservation !== data.formData.dateReservation + ? formatDateFr(data.formData.dateFinReservation) + : dateDebut; + const dailySlots = Array.isArray(data.formData.horairesParJour) + ? data.formData.horairesParJour.filter((slot) => slot?.date) + : []; + const creneau = data.formData.heureDebut || data.formData.heureFin + ? `${data.formData.heureDebut || '?'} — ${data.formData.heureFin || '?'}` + : '-'; + + doc.save(); + doc.lineWidth(2) + .strokeColor('#f0c94b') + .moveTo(doc.page.margins.left, y) + .lineTo(doc.page.margins.left + pageWidth, y) + .stroke(); + doc.restore(); + + y += 16; + drawCcdsLogo(doc, doc.page.margins.left + (pageWidth - 90) / 2, y, 90, 90); + y += 96; + doc.fillColor(COLORS.muted) + .fontSize(9) + .font('Helvetica') + .text('COMMUNAUTÉ DE COMMUNES DES SAVANES', doc.page.margins.left, y, { + width: pageWidth, + align: 'center', + }); + y += 18; + doc.fillColor(COLORS.text) + .fontSize(18) + .font('Helvetica-Bold') + .text('FORMULAIRE ADMINISTRATIF DE RÉSERVATION', doc.page.margins.left, y, { + width: pageWidth, + align: 'center', + }); + y += 20; + doc.text('D\'UN LOCAL - MAISON DE LA JEUNESSE DES SAVANES', doc.page.margins.left, y, { + width: pageWidth, + align: 'center', + }); + y += 26; + + doc.save(); + doc.roundedRect(doc.page.margins.left + 18, y, pageWidth - 36, 34, 4) + .fill('#d9eefc') + .stroke('#f0c94b'); + doc.fillColor(COLORS.primary).fontSize(10).font('Helvetica-Bold') + .text('Réservation à transmettre à la Direction des Services aux Usagers', doc.page.margins.left + 28, y + 7, { + width: pageWidth - 56, + align: 'center', + }); + doc.fontSize(9).font('Helvetica') + .text('Merci de compléter les informations demandées avant instruction', doc.page.margins.left + 28, y + 19, { + width: pageWidth - 56, + align: 'center', + }); + doc.restore(); + y += 50; + + // Statut de la demande + const statusLabel = statusLabels[data.status] || data.status; + doc.fillColor(COLORS.muted) + .fontSize(8) + .font('Helvetica') + .text(`Statut : ${statusLabel} | Créée le : ${formatDateFr(data.createdAt)}`, doc.page.margins.left, y, { + width: pageWidth, + align: 'right', + }); + y += 18; + + // ========================================== + // SECTION 1 : INFORMATIONS DE L'ASSOCIATION + // ========================================== + y = drawSectionTitle(doc, 'INFORMATIONS DE L\'ASSOCIATION', y, pageWidth); + + const assoRows = [ + ['Nom de l\'association', data.formData.nomAssociation || '-'], + ['Adresse', data.formData.adresseAssociation || '-'], + ['Commune du siège', data.formData.communeSiege || '-'], + ['Représentant légal', data.formData.representantLegal || '-'], + ['Téléphone', data.formData.telephoneAssociation || '-'], + ['Email', data.formData.emailAssociation || '-'], + ]; + y = drawTable(doc, assoRows, y, pageWidth); + y += 15; + + // ========================================== + // SECTION 2 : LOCAL(AUX) SOLLICITÉ(S) + // ========================================== + y = drawSectionTitle(doc, 'LOCAL(AUX) SOLLICITÉ(S)', y, pageWidth); + + const salles = data.formData.sallesSelectionnees?.join(', ') || '-'; + doc.fillColor(COLORS.text) + .fontSize(10) + .font('Helvetica') + .text(salles, doc.page.margins.left + 5, y, { width: pageWidth - 10 }); + y += doc.heightOfString(salles, { width: pageWidth - 10 }) + 15; + + y = checkPageBreak(doc, y, 200); + y = drawSectionTitle(doc, 'DÉTAILS DE LA RÉSERVATION', y, pageWidth); + y = drawTable(doc, [['Motif de la réservation', data.formData.motifReservation || '-']], y, pageWidth); + y += 8; + + const hasDetailedSchedule = Boolean(data.formData.useDetailedSchedule && dailySlots.length > 0); + const scheduleBoxHeight = hasDetailedSchedule ? 70 + (dailySlots.length * 18) : 70; + y = checkPageBreak(doc, y, scheduleBoxHeight + 18); + doc.save(); + doc.roundedRect(doc.page.margins.left, y, pageWidth, scheduleBoxHeight, 4) + .fill('#f8fafc') + .stroke(COLORS.border); + doc.fillColor(COLORS.primary).fontSize(10).font('Helvetica-Bold') + .text('PÉRIODE ET CRÉNEAU SOLLICITÉS', doc.page.margins.left + 14, y + 10, { width: pageWidth - 28 }); + doc.fillColor(COLORS.muted).fontSize(9).font('Helvetica') + .text('Du', doc.page.margins.left + 14, y + 34); + doc.fillColor(COLORS.text).fontSize(10).font('Helvetica-Bold') + .text(dateDebut, doc.page.margins.left + 34, y + 33); + doc.fillColor(COLORS.muted).fontSize(9).font('Helvetica') + .text('Au', doc.page.margins.left + pageWidth / 2, y + 34); + doc.fillColor(COLORS.text).fontSize(10).font('Helvetica-Bold') + .text(dateFin, doc.page.margins.left + pageWidth / 2 + 20, y + 33, { + width: pageWidth / 2 - 34, + }); + if (hasDetailedSchedule) { + doc.fillColor(COLORS.muted).fontSize(9).font('Helvetica') + .text('Organisation', doc.page.margins.left + 14, y + 52); + doc.fillColor(COLORS.text).fontSize(10).font('Helvetica-Bold') + .text(`Horaires détaillés sur ${dailySlots.length} jour(s)`, doc.page.margins.left + 72, y + 51, { + width: pageWidth - 86, + }); + + let slotY = y + 68; + dailySlots.forEach((slot) => { + doc.fillColor(COLORS.muted).fontSize(9).font('Helvetica') + .text(formatDateFr(slot.date || ''), doc.page.margins.left + 28, slotY); + doc.fillColor(COLORS.text).fontSize(10).font('Helvetica-Bold') + .text(`${slot.heureDebut || '?'} — ${slot.heureFin || '?'}`, doc.page.margins.left + 160, slotY, { + width: pageWidth - 190, + }); + slotY += 16; + }); + } else { + doc.fillColor(COLORS.muted).fontSize(9).font('Helvetica') + .text('Créneau', doc.page.margins.left + 14, y + 52); + doc.fillColor(COLORS.text).fontSize(10).font('Helvetica-Bold') + .text(creneau, doc.page.margins.left + 72, y + 51, { + width: pageWidth - 86, + }); + } + doc.restore(); + y += scheduleBoxHeight + 12; + + const detailRows = [ + ['Nombre prévisionnel de participants', data.formData.nombreParticipants ? `${data.formData.nombreParticipants} personnes` : '-'], + ['Type d’usage', data.formData.typeUsage ? SALLE_USAGE_TYPE_LABELS[data.formData.typeUsage] : '-'], + ['Fréquence', data.formData.frequence ? SALLE_FREQUENCY_LABELS[data.formData.frequence] : '-'], + ['Besoins complémentaires', data.formData.besoinsComplementaires || '-'], + ['Matériel nécessaire', data.formData.materielNecessaire || '-'], + ['Observations complémentaires', data.formData.observations || '-'], + ['Montant estimatif / validé', typeof data.formData.salleWorkflow?.pricing?.totalAmountCents === 'number' ? formatCurrency(data.formData.salleWorkflow.pricing.totalAmountCents) : '-'], + ]; + y = drawTable(doc, detailRows, y, pageWidth); + y += 18; + + // ========================================== + // SECTION 4 : CADRE RÉSERVÉ À LA DSU + // ========================================== + y = checkPageBreak(doc, y, 250); + y = drawSectionTitle(doc, 'CADRE RÉSERVÉ À LA DIRECTION DES SERVICES AUX USAGERS (DSU)', y, pageWidth); + + const dsu = data.formData.cadreDSU; + if (dsu) { + const financialDecision = dsu.materielEvent?.financialDecision; + // Avis DSU avec couleur + if (dsu.avisDSU) { + const avisLabel = avisDSULabels[dsu.avisDSU] || dsu.avisDSU; + const avisColor = dsu.avisDSU === 'favorable' ? COLORS.green + : dsu.avisDSU === 'defavorable' ? COLORS.red + : COLORS.amber; + + doc.fillColor(COLORS.muted) + .fontSize(9) + .font('Helvetica') + .text('Avis de la DSU : ', doc.page.margins.left + 5, y, { continued: true }); + doc.fillColor(avisColor) + .font('Helvetica-Bold') + .text(avisLabel); + y += 18; + } + + const dsuRows = [ + ['Date de réception', dsu.dateReception ? formatDateFr(dsu.dateReception) : '-'], + ['Date de la décision', dsu.dateDecision ? formatDateFr(dsu.dateDecision) : '-'], + ['Responsable DSU', dsu.responsableDSU || '-'], + ]; + y = drawTable(doc, dsuRows, y, pageWidth); + y += 10; + + // Conditions particulières + const conditions: string[] = []; + if (dsu.cautionRequise) { + conditions.push(`Caution requise${dsu.montantCaution ? ` : ${dsu.montantCaution} €` : ''}`); + } + if (dsu.assuranceRequise) { + conditions.push('Attestation d\'assurance requise'); + } + if (dsu.horairesImposes) { + conditions.push(`Horaires imposés : ${dsu.horairesImposes}`); + } + if (dsu.conditionsParticulieres) { + conditions.push(dsu.conditionsParticulieres); + } + + if (conditions.length > 0) { + y = checkPageBreak(doc, y, 80); + doc.fillColor(COLORS.primary) + .fontSize(9) + .font('Helvetica-Bold') + .text('Conditions particulières :', doc.page.margins.left + 5, y); + y += 14; + + for (const condition of conditions) { + y = checkPageBreak(doc, y, 20); + doc.fillColor(COLORS.text) + .fontSize(9) + .font('Helvetica') + .text(`• ${condition}`, doc.page.margins.left + 15, y, { width: pageWidth - 25 }); + y += doc.heightOfString(`• ${condition}`, { width: pageWidth - 25 }) + 4; + } + y += 5; + } + + if (financialDecision?.financialMode) { + y = checkPageBreak(doc, y, 96); + doc.fillColor(COLORS.primary) + .fontSize(9) + .font('Helvetica-Bold') + .text('Conditions financières / location :', doc.page.margins.left + 5, y); + y += 14; + + const financialRows = [ + ['Mode financier', getFinancialModeLabel(financialDecision.financialMode)], + ['Caution / dépôt', financialDecision.depositRequired ? formatCurrency(financialDecision.depositAmountCents ?? 0) : 'Aucune'], + ['Montant de location', financialDecision.financialMode === 'location_payante' ? formatCurrency(financialDecision.rentalAmountCents ?? 0) : 'Non applicable'], + ['Statut de la convention', getContractStatusLabel(financialDecision.contractStatus)], + ]; + y = drawTable(doc, financialRows, y, pageWidth); + y += 8; + + if (financialDecision.pricingNotes) { + y = checkPageBreak(doc, y, 50); + doc.fillColor(COLORS.primary) + .fontSize(9) + .font('Helvetica-Bold') + .text('Clauses spécifiques :', doc.page.margins.left + 5, y); + y += 14; + doc.fillColor(COLORS.text) + .fontSize(9) + .font('Helvetica') + .text(financialDecision.pricingNotes, doc.page.margins.left + 15, y, { width: pageWidth - 25 }); + y += doc.heightOfString(financialDecision.pricingNotes, { width: pageWidth - 25 }) + 10; + } + } + + // Observations DSU + if (dsu.observationsDSU) { + y = checkPageBreak(doc, y, 50); + doc.fillColor(COLORS.primary) + .fontSize(9) + .font('Helvetica-Bold') + .text('Observations de la DSU :', doc.page.margins.left + 5, y); + y += 14; + doc.fillColor(COLORS.text) + .fontSize(9) + .font('Helvetica') + .text(dsu.observationsDSU, doc.page.margins.left + 15, y, { width: pageWidth - 25 }); + y += doc.heightOfString(dsu.observationsDSU, { width: pageWidth - 25 }) + 10; + } + } else { + // Cadre vide avec lignes pointillées + const emptyRows = [ + ['Date de réception', ''], + ['Avis', ''], + ['Conditions particulières', ''], + ['Responsable DSU', ''], + ['Date de la décision', ''], + ['Observations', ''], + ]; + y = drawTable(doc, emptyRows, y, pageWidth, true); + } + + y += 18; + y = checkPageBreak(doc, y, 80); + doc.strokeColor(COLORS.border) + .lineWidth(0.5) + .moveTo(doc.page.margins.left + 10, y + 28) + .lineTo(doc.page.margins.left + pageWidth / 2 - 20, y + 28) + .stroke(); + doc.strokeColor(COLORS.border) + .lineWidth(0.5) + .moveTo(doc.page.margins.left + pageWidth / 2 + 20, y + 28) + .lineTo(doc.page.margins.left + pageWidth - 10, y + 28) + .stroke(); + doc.fillColor(COLORS.text).fontSize(9).font('Helvetica') + .text('Signature du représentant de l\'association', doc.page.margins.left + 10, y + 34, { + width: pageWidth / 2 - 30, + align: 'center', + }) + .text('Visa de la DSU', doc.page.margins.left + pageWidth / 2 + 20, y + 34, { + width: pageWidth / 2 - 30, + align: 'center', + }); + + const directorSignedAt = data.formData.salleWorkflow?.directorSignedAt; + const directorSignedByName = data.formData.salleWorkflow?.directorSignedByName; + if (directorSignedAt) { + doc.fillColor(COLORS.blueText).fontSize(8).font('Helvetica-Bold') + .text('Validé électroniquement', doc.page.margins.left + pageWidth / 2 + 20, y + 49, { + width: pageWidth / 2 - 30, + align: 'center', + }); + doc.fillColor(COLORS.text).fontSize(8).font('Helvetica') + .text( + `${formatDateFr(directorSignedAt)}${directorSignedByName ? ` · ${directorSignedByName}` : ''}`, + doc.page.margins.left + pageWidth / 2 + 20, + y + 61, + { + width: pageWidth / 2 - 30, + align: 'center', + } + ); + } + + // ========================================== + // COMMENTAIRE ADMIN (si présent) + // ========================================== + if (data.commentaireAdmin) { + y = checkPageBreak(doc, y, 60); + y += 10; + doc.fillColor(COLORS.primary) + .fontSize(9) + .font('Helvetica-Bold') + .text('Commentaire de l\'administration :', doc.page.margins.left + 5, y); + y += 14; + doc.fillColor(COLORS.text) + .fontSize(9) + .font('Helvetica') + .text(data.commentaireAdmin, doc.page.margins.left + 15, y, { width: pageWidth - 25 }); + y += doc.heightOfString(data.commentaireAdmin, { width: pageWidth - 25 }) + 10; + } + + // ========================================== + // PIED DE PAGE + // ========================================== + const footerY = doc.page.height - doc.page.margins.bottom - 30; + doc.strokeColor(COLORS.border) + .lineWidth(0.5) + .moveTo(doc.page.margins.left, footerY) + .lineTo(doc.page.margins.left + pageWidth, footerY) + .stroke(); + + doc.fillColor(COLORS.muted) + .fontSize(7) + .font('Helvetica') + .text( + `Document généré le ${formatDateFr(new Date())} — Communauté de Communes Des Savanes — Direction des Services aux Usagers`, + doc.page.margins.left, + footerY + 8, + { width: pageWidth, align: 'center' } + ); + + if (directorSignedAt) { + doc.text( + `Visa Direction enregistré le ${formatDateFr(directorSignedAt)}${directorSignedByName ? ` par ${directorSignedByName}` : ''}`, + doc.page.margins.left, + footerY + 18, + { width: pageWidth, align: 'center' } + ); + } + + doc.end(); + } catch (err) { + reject(err); + } + }); +} + +export function generateMaterialEventPDF(data: MaterialEventPDFData): Promise { + return new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: 'A4', + margins: { top: 40, bottom: 40, left: 50, right: 50 }, + info: { + Title: `Demande matériel - ${data.formData.nomAssociation || data.formData.commune || 'Association'}`, + Author: 'Communauté de Communes Des Savanes', + Subject: 'Demande de mise à disposition du matériel événementiel', + }, + }); + + const chunks: Buffer[] = []; + doc.on('data', (chunk: Buffer) => chunks.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + + const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + const manifestationStart = data.formData.dateDebutManifestation || data.formData.dateManifestation; + const manifestationEnd = data.formData.dateFinManifestation || data.formData.dateManifestation; + + doc.save(); + doc.lineWidth(2) + .strokeColor('#f0c94b') + .moveTo(doc.page.margins.left, y) + .lineTo(doc.page.margins.left + pageWidth, y) + .stroke(); + doc.restore(); + + y += 16; + drawCcdsLogo(doc, doc.page.margins.left + (pageWidth - 90) / 2, y, 90, 90); + y += 96; + doc.fillColor(COLORS.muted).fontSize(9).font('Helvetica') + .text('COMMUNAUTÉ DE COMMUNES DES SAVANES', doc.page.margins.left, y, { width: pageWidth, align: 'center' }); + y += 18; + doc.fillColor(COLORS.text).fontSize(18).font('Helvetica-Bold') + .text('FICHE DE DEMANDE DE MISE À DISPOSITION', doc.page.margins.left, y, { width: pageWidth, align: 'center' }); + y += 20; + doc.text('DU MATÉRIEL ÉVÉNEMENTIEL', doc.page.margins.left, y, { width: pageWidth, align: 'center' }); + y += 26; + + doc.save(); + doc.roundedRect(doc.page.margins.left + 18, y, pageWidth - 36, 34, 4) + .fill('#d9eefc') + .stroke('#f0c94b'); + doc.fillColor(COLORS.primary).fontSize(10).font('Helvetica-Bold') + .text('Fiche à remplir obligatoirement (1 mois avant la manifestation)', doc.page.margins.left + 28, y + 7, { + width: pageWidth - 56, + align: 'center', + }); + doc.fontSize(9).font('Helvetica') + .text('À transmettre à la Direction des Services aux Usagers', doc.page.margins.left + 28, y + 19, { + width: pageWidth - 56, + align: 'center', + }); + doc.restore(); + y += 50; + + const statusLabel = statusLabels[data.status] || data.status; + doc.fillColor(COLORS.muted).fontSize(8).font('Helvetica') + .text(`Statut : ${statusLabel} | Créée le : ${formatDateFr(data.createdAt)}`, doc.page.margins.left, y, { + width: pageWidth, + align: 'right', + }); + y += 18; + + y = drawSectionTitle(doc, 'INFORMATIONS DU DEMANDEUR', y, pageWidth); + y = drawTable(doc, [ + ['Association', data.formData.nomAssociation || '-'], + ['Commune', data.formData.commune || data.formData.communeSiege || '-'], + ['Direction', data.formData.direction || '-'], + ['Service', data.formData.service || '-'], + ['Nom et prénom du demandeur', data.formData.demandeurNomPrenom || '-'], + ['Téléphone', data.formData.telephoneAssociation || '-'], + ['Email', data.formData.emailAssociation || '-'], + ], y, pageWidth); + y += 15; + + y = drawSectionTitle(doc, 'MATÉRIEL DEMANDÉ', y, pageWidth); + const requestedRows = Object.entries(materialEventLabels) + .filter(([key]) => data.formData.materielsDemandes?.[key]) + .map(([key, label]) => [ + label, + `${data.formData.quantitesDemandees?.[key] || '-'}${key === 'autres' && data.formData.autreMaterielPrecisions ? ` — ${data.formData.autreMaterielPrecisions}` : ''}`, + ]); + y = drawTable(doc, requestedRows.length > 0 ? requestedRows : [['Aucun matériel demandé', '-']], y, pageWidth); + y += 15; + + y = drawSectionTitle(doc, 'DÉTAILS DE LA DEMANDE', y, pageWidth); + y = drawTable(doc, [['Date de la demande', formatDateFr(data.formData.dateDemande)]], y, pageWidth); + y += 8; + + y = checkPageBreak(doc, y, 88); + doc.save(); + doc.roundedRect(doc.page.margins.left, y, pageWidth, 70, 4) + .fill('#f8fafc') + .stroke(COLORS.border); + doc.fillColor(COLORS.primary).fontSize(10).font('Helvetica-Bold') + .text('PÉRIODE DE LA MANIFESTATION', doc.page.margins.left + 14, y + 10, { width: pageWidth - 28 }); + doc.fillColor(COLORS.muted).fontSize(9).font('Helvetica') + .text('Du', doc.page.margins.left + 14, y + 34); + doc.fillColor(COLORS.text).fontSize(10).font('Helvetica-Bold') + .text(formatDateFr(manifestationStart), doc.page.margins.left + 34, y + 33); + doc.fillColor(COLORS.muted).fontSize(9).font('Helvetica') + .text('Au', doc.page.margins.left + pageWidth / 2, y + 34); + doc.fillColor(COLORS.text).fontSize(10).font('Helvetica-Bold') + .text(formatDateFr(manifestationEnd), doc.page.margins.left + pageWidth / 2 + 20, y + 33, { + width: pageWidth / 2 - 34, + }); + doc.restore(); + y += 82; + + y = drawTable(doc, [ + ['Date prévisionnelle de prise en charge du matériel', formatDateFr(data.formData.datePriseEnCharge)], + ['Date prévisionnelle de restitution', formatDateFr(data.formData.dateRestitution)], + ['Motif de la demande', data.formData.motifDemande || '-'], + ], y, pageWidth); + y += 18; + + y = checkPageBreak(doc, y, 220); + y = drawSectionTitle(doc, 'CADRE RÉSERVÉ À LA DSU', y, pageWidth); + const dsu = data.formData.cadreDSU; + if (dsu) { + y = drawTable(doc, [ + ['Date de réception', formatDateFr(dsu.dateReception)], + ['Date de la décision', formatDateFr(dsu.dateDecision)], + ['Responsable DSU', dsu.responsableDSU || '-'], + ], y, pageWidth); + y += 10; + + const grantedRows = Object.entries(materialEventLabels).map(([key, label]) => { + const granted = dsu.materielEvent?.itemsAccordes?.[key]; + const qty = dsu.materielEvent?.quantitesAccordees?.[key]; + const extra = key === 'autres' ? dsu.materielEvent?.autresPrecisions : ''; + return [label, granted ? `${qty || '-'} accordé(s)${extra ? ` — ${extra}` : ''}` : (data.status === 'refusee' ? 'Refusé' : 'Non renseigné')]; + }); + y = drawTable(doc, grantedRows, y, pageWidth); + + const financialDecision = dsu.materielEvent?.financialDecision; + if (financialDecision?.financialMode) { + y += 12; + y = checkPageBreak(doc, y, 120); + doc.fillColor(COLORS.primary).fontSize(9).font('Helvetica-Bold') + .text('Conditions financières / convention :', doc.page.margins.left + 5, y); + y += 14; + + const financialRows = [ + ['Mode financier', getFinancialModeLabel(financialDecision.financialMode)], + ['Caution / dépôt de garantie', financialDecision.depositRequired ? formatCurrency(financialDecision.depositAmountCents ?? 0) : 'Aucune'], + ['Montant de location', financialDecision.financialMode === 'location_payante' ? formatCurrency(financialDecision.rentalAmountCents ?? 0) : 'Non applicable'], + ['Statut de la convention', getContractStatusLabel(financialDecision.contractStatus)], + ['Convention générée le', formatDateFr(financialDecision.contractGeneratedAt)], + ]; + y = drawTable(doc, financialRows, y, pageWidth); + + if (financialDecision.pricingNotes) { + y += 10; + doc.fillColor(COLORS.primary).fontSize(9).font('Helvetica-Bold') + .text('Clauses spécifiques :', doc.page.margins.left + 5, y); + y += 14; + doc.fillColor(COLORS.text).fontSize(9).font('Helvetica') + .text(financialDecision.pricingNotes, doc.page.margins.left + 5, y, { width: pageWidth - 10 }); + y += doc.heightOfString(financialDecision.pricingNotes, { width: pageWidth - 10 }) + 6; + } + } + + if (dsu.observationsDSU) { + y += 12; + doc.fillColor(COLORS.primary).fontSize(9).font('Helvetica-Bold') + .text('Observations de la DSU :', doc.page.margins.left + 5, y); + y += 14; + doc.fillColor(COLORS.text).fontSize(9).font('Helvetica') + .text(dsu.observationsDSU, doc.page.margins.left + 5, y, { width: pageWidth - 10 }); + } + } else { + doc.fillColor(COLORS.muted).fontSize(10).font('Helvetica-Oblique') + .text('Cadre DSU non encore renseigné.', doc.page.margins.left + 5, y); + } + + y += 18; + y = checkPageBreak(doc, y, 80); + doc.strokeColor(COLORS.border) + .lineWidth(0.5) + .moveTo(doc.page.margins.left + 10, y + 28) + .lineTo(doc.page.margins.left + pageWidth / 2 - 20, y + 28) + .stroke(); + doc.strokeColor(COLORS.border) + .lineWidth(0.5) + .moveTo(doc.page.margins.left + pageWidth / 2 + 20, y + 28) + .lineTo(doc.page.margins.left + pageWidth - 10, y + 28) + .stroke(); + doc.fillColor(COLORS.text).fontSize(9).font('Helvetica') + .text('Signature du DGS de la commune', doc.page.margins.left + 10, y + 34, { + width: pageWidth / 2 - 30, + align: 'center', + }) + .text('Signature de la DSU', doc.page.margins.left + pageWidth / 2 + 20, y + 34, { + width: pageWidth / 2 - 30, + align: 'center', + }); + + const footerY = doc.page.height - doc.page.margins.bottom - 30; + doc.strokeColor(COLORS.border) + .lineWidth(0.5) + .moveTo(doc.page.margins.left, footerY) + .lineTo(doc.page.margins.left + pageWidth, footerY) + .stroke(); + + doc.fillColor(COLORS.muted) + .fontSize(7) + .font('Helvetica') + .text( + `Document généré le ${formatDateFr(new Date())} — Communauté de Communes Des Savanes — Direction des Services aux Usagers`, + doc.page.margins.left, + footerY + 8, + { width: pageWidth, align: 'center' } + ); + + doc.end(); + } catch (error) { + reject(error); + } + }); +} + +// ========================================== +// HELPER FUNCTIONS +// ========================================== + +function drawSectionTitle(doc: PDFKit.PDFDocument, title: string, y: number, pageWidth: number): number { + const leftMargin = (doc as any).page.margins.left; + + // Background bar + doc.save(); + doc.rect(leftMargin - 5, y, pageWidth + 10, 20) + .fill(COLORS.primaryLight); + + doc.fillColor(COLORS.primary) + .fontSize(9) + .font('Helvetica-Bold') + .text(title, leftMargin + 5, y + 5, { width: pageWidth }); + + doc.restore(); + return y + 28; +} + +function drawTable(doc: PDFKit.PDFDocument, rows: string[][], y: number, pageWidth: number, emptyStyle: boolean = false): number { + const leftMargin = (doc as any).page.margins.left; + const labelWidth = pageWidth * 0.4; + const valueWidth = pageWidth * 0.6; + const rowPadding = 5; + + for (const [label, value] of rows) { + y = checkPageBreak(doc, y, 20); + + // Label + doc.fillColor(COLORS.muted) + .fontSize(9) + .font('Helvetica') + .text(label + ' :', leftMargin + rowPadding, y, { + width: labelWidth - rowPadding * 2, + }); + + // Value + if (emptyStyle && !value) { + // Draw dotted line for empty fields + doc.strokeColor(COLORS.border) + .lineWidth(0.5) + .dash(3, { space: 2 }) + .moveTo(leftMargin + labelWidth + 5, y + 10) + .lineTo(leftMargin + pageWidth - 5, y + 10) + .stroke() + .undash(); + } else { + doc.fillColor(COLORS.text) + .fontSize(9) + .font('Helvetica-Bold') + .text(value || '-', leftMargin + labelWidth, y, { + width: valueWidth - rowPadding, + align: 'right', + }); + } + + // Calculate the height used + const labelH = doc.heightOfString(label + ' :', { width: labelWidth - rowPadding * 2 }); + const valueH = value ? doc.heightOfString(value, { width: valueWidth - rowPadding }) : 12; + const rowHeight = Math.max(labelH, valueH) + 4; + + y += rowHeight; + + // Separator line + doc.strokeColor(COLORS.border) + .lineWidth(0.3) + .dash(1, { space: 2 }) + .moveTo(leftMargin + 5, y) + .lineTo(leftMargin + pageWidth - 5, y) + .stroke() + .undash(); + + y += 4; + } + + return y; +} + +function checkPageBreak(doc: PDFKit.PDFDocument, y: number, requiredSpace: number): number { + const pageBottom = doc.page.height - doc.page.margins.bottom - 40; + if (y + requiredSpace > pageBottom) { + doc.addPage(); + return doc.page.margins.top; + } + return y; +} diff --git a/server/requestDetail.test.ts b/server/requestDetail.test.ts new file mode 100644 index 0000000..b8ca646 --- /dev/null +++ b/server/requestDetail.test.ts @@ -0,0 +1,224 @@ +import { describe, it, expect, vi } from 'vitest'; + +// Mock db module +vi.mock('./db', () => ({ + getRequestById: vi.fn(), + getAssociationByUserId: vi.fn(), + getAssociationById: vi.fn(), + getDocumentById: vi.fn(), + getRequestHistoryByRequestId: vi.fn(), + deleteRequest: vi.fn(), + createAuditLog: vi.fn(), +})); + +import * as db from './db'; + +describe('Request Detail & Delete Logic', () => { + describe('getById enrichment', () => { + it('should return associationInfo when association exists', async () => { + const mockAssociation = { + id: 1, + nomAssociation: 'Association Test', + siret: '12345678901234', + adresse: '10 rue de la Paix', + codePostal: '97300', + ville: 'Cayenne', + telephone: '0594123456', + emailContact: 'test@asso.fr', + nomRepresentant: 'Jean Dupont', + }; + + (db.getAssociationById as any).mockResolvedValue(mockAssociation); + + const result = await db.getAssociationById(1); + expect(result).toBeDefined(); + expect(result?.nomAssociation).toBe('Association Test'); + expect(result?.siret).toBe('12345678901234'); + expect(result?.telephone).toBe('0594123456'); + }); + + it('should return null when association does not exist', async () => { + (db.getAssociationById as any).mockResolvedValue(null); + + const result = await db.getAssociationById(999); + expect(result).toBeNull(); + }); + + it('should resolve attached documents from documentsJoints JSON', async () => { + const mockDoc1 = { id: 1, nom: 'Statuts', type: 'statuts', fileSize: 1024 }; + const mockDoc2 = { id: 2, nom: 'RIB', type: 'rib', fileSize: 2048 }; + + (db.getDocumentById as any) + .mockResolvedValueOnce(mockDoc1) + .mockResolvedValueOnce(mockDoc2); + + const documentsJoints = JSON.stringify([1, 2]); + const docIds = JSON.parse(documentsJoints) as number[]; + + const docs = await Promise.all( + docIds.map(async (docId: number) => { + const doc = await db.getDocumentById(docId); + return doc || null; + }) + ); + const attachedDocuments = docs.filter(Boolean); + + expect(attachedDocuments).toHaveLength(2); + expect(attachedDocuments[0]?.nom).toBe('Statuts'); + expect(attachedDocuments[1]?.nom).toBe('RIB'); + }); + + it('should handle empty documentsJoints gracefully', () => { + const documentsJoints = null; + let attachedDocuments: any[] = []; + + if (documentsJoints) { + try { + const docIds = JSON.parse(documentsJoints) as number[]; + // would process here + } catch { + // ignore + } + } + + expect(attachedDocuments).toHaveLength(0); + }); + + it('should handle invalid JSON in documentsJoints', () => { + const documentsJoints = 'not-json'; + let attachedDocuments: any[] = []; + + if (documentsJoints) { + try { + const docIds = JSON.parse(documentsJoints) as number[]; + attachedDocuments = docIds as any[]; + } catch { + // ignore parse errors + } + } + + expect(attachedDocuments).toHaveLength(0); + }); + }); + + describe('delete request', () => { + it('should allow deletion of a brouillon request by the owning association', async () => { + const mockRequest = { + id: 1, + associationId: 10, + status: 'brouillon', + titre: 'Ma demande', + type: 'subvention_fonctionnement', + }; + const mockAssociation = { id: 10, userId: 5 }; + + (db.getRequestById as any).mockResolvedValue(mockRequest); + (db.getAssociationByUserId as any).mockResolvedValue(mockAssociation); + (db.deleteRequest as any).mockResolvedValue(undefined); + + // Simulate the logic + const request = await db.getRequestById(1); + const association = await db.getAssociationByUserId(5); + + expect(request).toBeDefined(); + expect(association).toBeDefined(); + expect(request!.associationId).toBe(association!.id); + expect(request!.status).not.toBe('validee'); + expect(request!.status).not.toBe('refusee'); + + await db.deleteRequest(1); + expect(db.deleteRequest).toHaveBeenCalledWith(1); + }); + + it('should reject deletion of a validated request', async () => { + const mockRequest = { + id: 2, + associationId: 10, + status: 'validee', + titre: 'Demande validée', + type: 'subvention_projet', + }; + + (db.getRequestById as any).mockResolvedValue(mockRequest); + + const request = await db.getRequestById(2); + expect(request!.status).toBe('validee'); + + // The router would throw a TRPCError here + const canDelete = request!.status !== 'validee' && request!.status !== 'refusee'; + expect(canDelete).toBe(false); + }); + + it('should reject deletion of a refused request', async () => { + const mockRequest = { + id: 3, + associationId: 10, + status: 'refusee', + titre: 'Demande refusée', + type: 'demande_salle', + }; + + (db.getRequestById as any).mockResolvedValue(mockRequest); + + const request = await db.getRequestById(3); + const canDelete = request!.status !== 'validee' && request!.status !== 'refusee'; + expect(canDelete).toBe(false); + }); + + it('should reject deletion by a non-owning association', async () => { + const mockRequest = { + id: 4, + associationId: 10, + status: 'soumise', + titre: 'Demande autre asso', + type: 'autre', + }; + const mockAssociation = { id: 20, userId: 8 }; // Different association + + (db.getRequestById as any).mockResolvedValue(mockRequest); + (db.getAssociationByUserId as any).mockResolvedValue(mockAssociation); + + const request = await db.getRequestById(4); + const association = await db.getAssociationByUserId(8); + + // Association ID mismatch + expect(request!.associationId).not.toBe(association!.id); + }); + + it('should allow deletion of a submitted request', async () => { + const mockRequest = { + id: 5, + associationId: 10, + status: 'soumise', + titre: 'Demande soumise', + type: 'subvention_fonctionnement', + }; + + (db.getRequestById as any).mockResolvedValue(mockRequest); + (db.deleteRequest as any).mockResolvedValue(undefined); + + const request = await db.getRequestById(5); + const canDelete = request!.status !== 'validee' && request!.status !== 'refusee'; + expect(canDelete).toBe(true); + + await db.deleteRequest(5); + expect(db.deleteRequest).toHaveBeenCalledWith(5); + }); + + it('should allow deletion of a request in information_complementaire status', async () => { + const mockRequest = { + id: 6, + associationId: 10, + status: 'information_complementaire', + titre: 'Demande info comp', + type: 'agrement_sport', + }; + + (db.getRequestById as any).mockResolvedValue(mockRequest); + + const request = await db.getRequestById(6); + const canDelete = request!.status !== 'validee' && request!.status !== 'refusee'; + expect(canDelete).toBe(true); + }); + }); +}); diff --git a/server/requestPdf.ts b/server/requestPdf.ts new file mode 100644 index 0000000..bfeda11 --- /dev/null +++ b/server/requestPdf.ts @@ -0,0 +1,66 @@ +import { TRPCError } from "@trpc/server"; +import { generateMaterialEventPDF, generateReservationPDF } from "./pdfGenerator"; + +type RequestLike = { + id: number; + type: string; + titre: string; + status: string; + createdAt: Date | string; + dateSubmission?: Date | string | null; + dateTraitement?: Date | string | null; + commentaireAdmin?: string | null; + formData?: string | null; +}; + +export async function generateRequestPdfDocument(request: RequestLike) { + let formData: any = {}; + try { + formData = request.formData ? JSON.parse(request.formData) : {}; + } catch { + formData = {}; + } + + if (request.type === "demande_salle") { + const pdfBuffer = await generateReservationPDF({ + titre: request.titre, + status: request.status, + createdAt: request.createdAt, + dateSubmission: request.dateSubmission, + dateTraitement: request.dateTraitement, + commentaireAdmin: request.commentaireAdmin, + formData, + }); + + return { + buffer: pdfBuffer, + storageName: `reservations/pdf-${request.id}-${Date.now()}.pdf`, + fileName: `Reservation_${request.id}_${formData.nomAssociation || "association"}.pdf`, + contentType: "application/pdf", + }; + } + + if (request.type === "demande_materiel_evenementiel") { + const pdfBuffer = await generateMaterialEventPDF({ + titre: request.titre, + status: request.status, + createdAt: request.createdAt, + dateSubmission: request.dateSubmission, + dateTraitement: request.dateTraitement, + commentaireAdmin: request.commentaireAdmin, + formData, + }); + + return { + buffer: pdfBuffer, + storageName: `materiel-evenementiel/pdf-${request.id}-${Date.now()}.pdf`, + fileName: `Materiel_Evenementiel_${request.id}_${formData.nomAssociation || formData.commune || "association"}.pdf`, + contentType: "application/pdf", + }; + } + + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le PDF n'est disponible que pour les demandes de salle et de matériel événementiel", + }); +} diff --git a/server/routers.ts b/server/routers.ts new file mode 100644 index 0000000..520e595 --- /dev/null +++ b/server/routers.ts @@ -0,0 +1,10846 @@ +import { + cancelAuthenticatorSetup, + cancelScheduledAccountDeletion, + clearSessionCookie, + confirmAuthenticatorSetup, + createSessionToken, + getInternalMfaPolicy, + hashLocalPassword, + isInternalRole, + isAccountLockedError, + isMfaCodeRequiredError, + loginLocalUser, + registerLocalUser, + resendLocalUserMfaChallenge, + scheduleAccountDeletion, + setSessionCookie, + startAuthenticatorSetup, + verifyLocalUserMfa, +} from "./_core/auth"; +import { systemRouter } from "./_core/systemRouter"; +import { publicProcedure, protectedProcedure, router } from "./_core/trpc"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { nanoid } from "nanoid"; +import { randomBytes } from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { storagePut, storageGet } from "./storage"; +import * as db from "./db"; +import { notifyOwner } from "./_core/notification"; +import { generateRequestPdfDocument } from "./requestPdf"; +import { generateRequestRecapEmail } from "./emailRecap"; +import { canSendOperationalEmails, sendOperationalEmail } from "./mailer"; +import { getOAuthProviderStatus } from "./socialAuth"; +import { createAssociationProfileFromDirectoryEntry, parseAssociationDirectoryWorkbook, resolveAssociationDirectoryImportRows } from "./associationDirectory"; +import { generateAssociationInvitationEmail } from "./associationInvitationEmail"; +import { computeAssociationDirectoryGeoUpdate, getPublicMapCoordinates } from "./associationGeo"; +import { computeAssociationDirectoryReferenceProposal } from "./associationReferenceSync"; +import { + HELLOASSO_SETTINGS_KEY, + HelloAssoSyncClient, + computeAssociationDirectoryHelloAssoUpdate, + sanitizeHelloAssoSettings, + serializeHelloAssoSettingsPublic, +} from "./associationHelloAssoSync"; +import type { AssociationDirectoryMatchResult } from "./associationDirectoryMatcher"; +import { getLogisticsSettings, saveLogisticsSettings } from "./logisticsSettings"; +import { getLogisticsGroupSettings, saveLogisticsGroupSettings } from "./logisticsGroup"; +import { + getSalleSignatureDelegateCandidates, + getSalleSignatureDelegates, + withEffectiveInternalAccess, +} from "./internalAccess"; +import { + addDelegatedSignerForDirectrice, + getDelegatedSignerIdsForDirectrice, + removeDelegatedSignerForDirectrice, + setDelegatedSignerIdsForDirectrice, +} from "./salleSignatureDelegation"; +import { getAdminMailSettings, saveAdminMailSettings } from "./mailSettings"; +import { + agentRoleOptions, + buildMaterialReturnUploadLink, + borrowerRoleOptions, + computeMaterialReturnBoardState, + getMaterialReturnUploadLinkState, + materialReturnDiscrepancyOptions, + materialReturnBoardStateLabels, + scheduleMaterialReturnFollowup, + sendMaterialReturnFollowupEmail, + serializeMaterialReturnFollowup, +} from "./materialReturnWorkflow"; +import { + generateCompletedMaterialReturnStatementPdf, + generateMaterialReturnStatementPdf, +} from "./materialReturnPdf"; +import { generateMaterialReturnLitigationLetterPdf } from "./materialReturnLitigationPdf"; +import { generateMaterialReturnCompletionEmail } from "./materialReturnEmail"; +import { generateMaterialConventionPdf, type MaterialContractFinancialMode, type MaterialContractStatus } from "./materialConventionPdf"; +import { generateSalleConventionPdf } from "./salleConventionPdf"; +import { generateSalleQuotePdf } from "./salleQuotePdf"; +import { generateSalleAdministrativeDecisionPdf } from "./salleAdministrativeDecisionPdf"; +import { generateSalleInvoicePdf } from "./salleInvoicePdf"; +import { generateSalleFinalEmail, generateSalleQuoteEmail } from "./salleReservationEmails"; +import { generateReservationAnalyticsExcel, generateReservationAnalyticsPdf } from "./statsReportArtifacts"; +import { + buildDefaultSalleWorkflowTexts, + computeSalleWorkflowPricing, + getSalleWorkflowData, + mergeSalleWorkflowData, + updateSalleWorkflowDirectorStatus, + updateSalleWorkflowQuoteStatus, +} from "./salleReservationWorkflow"; +import { + APPEARANCE_SETTING_KEY, + appearanceAssetFields, + appearanceCardDecorationAnchors, + appearanceDecorationScopes, + appearanceDecorationLayers, + appearanceBackgroundTargets, + appearanceBackgroundPositions, + appearanceBackgroundPresentationModes, + appearanceButtonStyles, + appearanceBackgroundModes, + appearanceCardStyles, + appearanceFontFamilies, + appearanceSidebarStyles, + appearanceTableStyles, + appearanceThemeModes, + appearanceInputStyles, + defaultPortalAppearance, + sanitizePortalAppearance, +} from "@shared/appearance"; +import { associationCommuneOptions } from "@shared/associationCommunes"; +import { + DATA_PRIVACY_NOTICE_VERSION, + dataPrivacyConsentSchema, + type DataPrivacyConsent, +} from "@shared/privacyCompliance"; +import { + buildDisplayName, + legalRepresentativeRoleValues, + governanceMemberRoleValues, + normalizeLegalRepresentativeRole, + parseAssociationGovernance, + sanitizeGovernance, + serializeAssociationGovernance, + type AssociationGovernance, +} from "@shared/associationGovernance"; +import { + associationThematicValues, + getAssociationThematicLabel, + parseAssociationThematics, + serializeAssociationThematics, +} from "@shared/associationThematics"; +import { + emptyMaterialEventQuantityMap, + getMaterialEventLabel, + materialEventItems, + parseMaterialEventQuantity, + sanitizeMaterialEventQuantityMap, + type MaterialEventItemKey, +} from "@shared/materialEvent"; +import { isValidFacebookUrl, isValidInstagramUrl, normalizeSocialUrl } from "@shared/socialLinks"; +import { supportedMailProviders } from "@shared/mailProviders"; +import { + CONDITIONS_FINANCIERES_SALLE_TEXT, + computeSallePricing, + SALLE_WORKFLOW_DIRECTOR_STATUS_LABELS, + SALLE_WORKFLOW_PAYMENT_STATUS_LABELS, + SALLE_WORKFLOW_QUOTE_STATUS_LABELS, +} from "@shared/sallePricing"; +import { + operationalTourAuthModeSchema, + clearOperationalTourHistory, + getOperationalTourHistory, + getOperationalTourSettings, + operationalTourEnvironmentSchema, + OPERATIONAL_TOUR_SETTINGS_KEY, + runOperationalTour, + saveOperationalTourSettings, +} from "./operationalTour"; + +const SALLE_BILLING_SETTING_KEY = "system.salle.billing"; +const INTERNAL_DIRECTORY_SETTING_KEY = "system.ccds.internalDirectory"; +const STATS_AUTOMATION_SETTING_KEY = "system.stats.automation"; +const STATS_REPORT_HISTORY_SETTING_KEY = "system.stats.reportHistory"; +const ASSOCIATION_MAP_SETTING_KEY = "system.associationDirectory.map"; +const USER_OPERATING_TOUR_SETTING_KEY = "system.userOperatingTour"; +const COMPLIANCE_SUPPLIERS_SETTING_KEY = "system.compliance.suppliers"; +const COMPLIANCE_DPA_SETTING_KEY = "system.compliance.dpa"; +const COMPLIANCE_BACKUP_SETTING_KEY = "system.compliance.backups"; +const COMPLIANCE_RETENTION_HISTORY_SETTING_KEY = "system.compliance.retentionHistory"; +const COMPLIANCE_EVIDENCE_SETTING_KEY = "system.compliance.evidenceCenter"; +const COMPLIANCE_ISO_DELIVERABLES_SETTING_KEY = "system.compliance.isoDeliverables"; +const ANALYTICS_PERIOD_VALUES = ["7d", "month", "quarter", "semester", "year"] as const; +type AnalyticsPeriod = (typeof ANALYTICS_PERIOD_VALUES)[number]; +const analyticsPeriodSchema = z.enum(ANALYTICS_PERIOD_VALUES); +const userOperatingTourSettingsSchema = z.object({ + enabled: z.boolean().default(true), +}); +type UserOperatingTourSettings = z.infer; +type ComplianceStatus = "implemented" | "partial" | "missing"; + +const defaultUserOperatingTourSettings: UserOperatingTourSettings = { + enabled: true, +}; + +const appearanceSettingsSchema = z.object({ + themeMode: z.enum(appearanceThemeModes), + activeSkinId: z.string().trim().min(1).max(80), + portalTitle: z.string().trim().min(1).max(120), + portalTagline: z.string().trim().min(1).max(220), + primaryColor: z.string(), + secondaryColor: z.string(), + accentColor: z.string(), + buttonColor: z.string(), + alertColor: z.string(), + fontFamily: z.enum(appearanceFontFamilies), + headingFontFamily: z.enum(appearanceFontFamilies), + headingScalePercent: z.number().min(85).max(130), + bodyScalePercent: z.number().min(90).max(115), + logoUrl: z.string().optional(), + faviconUrl: z.string().optional(), + heroImageUrl: z.string().optional(), + backgroundImageUrl: z.string().optional(), + backgroundEnabled: z.boolean().optional(), + backgroundMode: z.enum(appearanceBackgroundModes).optional(), + backgroundPresentationMode: z.enum(appearanceBackgroundPresentationModes).optional(), + backgroundOverlayOpacity: z.number().min(0).max(100).optional(), + backgroundBlurRadius: z.number().min(0).max(24).optional(), + backgroundTargets: z.array(z.enum(appearanceBackgroundTargets)).optional(), + backgroundTargetImageUrls: z.record(z.string(), z.string()).optional(), + backgroundTargetOverlayOpacities: z.record(z.string(), z.number().min(0).max(100)).optional(), + backgroundTargetBlurRadii: z.record(z.string(), z.number().min(0).max(24)).optional(), + backgroundTargetPositions: z.record(z.string(), z.enum(appearanceBackgroundPositions)).optional(), + backgroundTargetPresentationModes: z.record(z.string(), z.enum(appearanceBackgroundPresentationModes)).optional(), + heroImagePositionX: z.number().min(0).max(100).optional(), + heroImagePositionY: z.number().min(0).max(100).optional(), + heroOverlayOpacity: z.number().min(0).max(100).optional(), + heroTitlePositionX: z.number().min(-50).max(50).optional(), + heroTitlePositionY: z.number().min(-50).max(50).optional(), + heroTitleWidthPercent: z.number().min(30).max(100).optional(), + heroTaglinePositionX: z.number().min(-50).max(50).optional(), + heroTaglinePositionY: z.number().min(-50).max(50).optional(), + heroTaglineWidthPercent: z.number().min(30).max(100).optional(), + decorativeElements: z.array(z.object({ + id: z.string().trim().min(1).max(80), + imageUrl: z.string().trim().min(1), + scope: z.enum(appearanceDecorationScopes), + layer: z.enum(appearanceDecorationLayers), + widthPercent: z.number().min(6).max(38), + topPercent: z.number().min(0).max(100), + leftPercent: z.number().min(0).max(100), + opacity: z.number().min(4).max(28), + blurRadius: z.number().min(0).max(12), + cardAnchor: z.enum(appearanceCardDecorationAnchors), + cardInsetPercent: z.number().min(0).max(20), + })).optional(), + cardStyle: z.enum(appearanceCardStyles), + tableStyle: z.enum(appearanceTableStyles), + sidebarStyle: z.enum(appearanceSidebarStyles), + buttonStyle: z.enum(appearanceButtonStyles), + inputStyle: z.enum(appearanceInputStyles), +}); + +async function getUserOperatingTourSettings(): Promise { + const raw = await db.getPortalSetting(USER_OPERATING_TOUR_SETTING_KEY); + if (!raw) { + return defaultUserOperatingTourSettings; + } + + try { + return userOperatingTourSettingsSchema.parse(JSON.parse(raw)); + } catch { + return defaultUserOperatingTourSettings; + } +} + +function buildComplianceItem( + id: string, + label: string, + status: ComplianceStatus, + summary: string, + detail?: string, +) { + return { id, label, status, summary, detail }; +} + +const complianceSupplierSchema = z.object({ + id: z.string().trim().min(1).max(80), + supplierName: z.string().trim().min(1).max(200), + service: z.string().trim().min(1).max(200), + location: z.string().trim().min(1).max(120), + criticality: z.enum(["critique", "elevee", "moyenne"]), + dpaStatus: z.enum(["signed", "pending", "renew", "expired", "missing"]), + reviewDate: z.string().trim().optional().or(z.literal("")), + owner: z.string().trim().min(1).max(160), + notes: z.string().trim().max(2000).optional().or(z.literal("")), +}); +type ComplianceSupplier = z.infer; + +const complianceDpaSchema = z.object({ + id: z.string().trim().min(1).max(80), + supplierId: z.string().trim().optional().or(z.literal("")), + supplierName: z.string().trim().min(1).max(200), + status: z.enum(["signed", "pending", "renew", "expired"]), + signedAt: z.string().trim().optional().or(z.literal("")), + expiresAt: z.string().trim().optional().or(z.literal("")), + reviewDueAt: z.string().trim().optional().or(z.literal("")), + owner: z.string().trim().min(1).max(160), + notes: z.string().trim().max(2000).optional().or(z.literal("")), +}); +type ComplianceDpa = z.infer; + +const complianceBackupRecordSchema = z.object({ + id: z.string().trim().min(1).max(80), + scope: z.enum(["database", "documents", "system", "global"]), + environment: z.enum(["production", "preproduction", "staging", "local", "external"]).optional().catch("production"), + backupType: z.enum(["scheduled", "manual", "snapshot", "replication", "external"]).optional().catch("scheduled"), + lastSuccessAt: z.string().trim().optional().or(z.literal("")), + lastFailureAt: z.string().trim().optional().or(z.literal("")), + monthlySuccessRate: z.number().min(0).max(100), + volumeLabel: z.string().trim().max(120).optional().or(z.literal("")), + retentionLabel: z.string().trim().max(120).optional().or(z.literal("")), + proofLabel: z.string().trim().max(160).optional().or(z.literal("")), + proofUrl: z.string().trim().max(1000).optional().or(z.literal("")), + notes: z.string().trim().max(2000).optional().or(z.literal("")), +}); +type ComplianceBackupRecord = z.infer; + +const complianceRestoreTestSchema = z.object({ + id: z.string().trim().min(1).max(80), + testedAt: z.string().trim().optional().or(z.literal("")), + environment: z.enum(["production", "preproduction", "staging", "local", "external"]).optional().catch("preproduction"), + scopeLabel: z.string().trim().max(160).optional().or(z.literal("")), + result: z.enum(["success", "warning", "failed"]), + responsible: z.string().trim().max(160).optional().or(z.literal("")), + observations: z.string().trim().max(2000).optional().or(z.literal("")), + proofLabel: z.string().trim().max(160).optional().or(z.literal("")), + nextPlannedAt: z.string().trim().optional().or(z.literal("")), +}); +type ComplianceRestoreTest = z.infer; + +const complianceRetentionReportSchema = z.object({ + id: z.string().trim().min(1).max(80), + generatedAt: z.string().trim().optional().or(z.literal("")), + periodLabel: z.string().trim().max(120).optional().or(z.literal("")), + deletedAccounts: z.number().int().min(0), + pendingPurge: z.number().int().min(0), + anonymizedRecords: z.number().int().min(0), + executedPurges: z.number().int().min(0), + legalHoldCount: z.number().int().min(0), + incidentsCount: z.number().int().min(0), + notes: z.string().trim().max(2000).optional().or(z.literal("")), +}); +type ComplianceRetentionReport = z.infer; + +const complianceEvidenceSchema = z.object({ + id: z.string().trim().min(1).max(80), + category: z.enum([ + "security_policy", + "processing_register", + "dpa", + "audit_report", + "backup_report", + "retention_report", + "restore_test", + "incident_report", + "other", + ]), + title: z.string().trim().min(1).max(200), + reference: z.string().trim().max(120).optional().or(z.literal("")), + url: z.string().trim().optional().or(z.literal("")), + description: z.string().trim().max(2000).optional().or(z.literal("")), + updatedAt: z.string().trim().min(1), +}); +type ComplianceEvidence = z.infer; + +const complianceIsoDeliverableSectionSchema = z.object({ + id: z.string().trim().min(1).max(80), + title: z.string().trim().min(1).max(200), + content: z.string().trim().max(12000).optional().or(z.literal("")), + guidance: z.string().trim().max(1200).optional().or(z.literal("")), +}); +type ComplianceIsoDeliverableSection = z.infer; + +const complianceIsoDeliverableSchema = z.object({ + id: z.string().trim().min(1).max(80), + reference: z.string().trim().min(1).max(80), + title: z.string().trim().min(1).max(240), + domain: z.enum(["gouvernance", "acces", "incidents", "sauvegardes", "fournisseurs", "risques", "continuite", "rgpd", "audit"]), + status: z.enum(["a_rediger", "en_cours", "a_relire", "a_valider", "valide", "a_reviser"]), + owner: z.string().trim().max(160).optional().or(z.literal("")), + approverName: z.string().trim().max(160).optional().or(z.literal("")), + approverEmail: z.string().trim().max(320).optional().or(z.literal("")), + summary: z.string().trim().max(2000).optional().or(z.literal("")), + notes: z.string().trim().max(6000).optional().or(z.literal("")), + lastReviewedAt: z.string().trim().optional().or(z.literal("")), + validatedAt: z.string().trim().optional().or(z.literal("")), + revisionDueAt: z.string().trim().optional().or(z.literal("")), + sections: z.array(complianceIsoDeliverableSectionSchema).max(20), +}); +type ComplianceIsoDeliverable = z.infer; + +type ComplianceOperationsPayload = { + suppliers: ComplianceSupplier[]; + dpas: ComplianceDpa[]; + backupRecords: ComplianceBackupRecord[]; + restoreTests: ComplianceRestoreTest[]; + retentionReports: ComplianceRetentionReport[]; + evidenceCenter: ComplianceEvidence[]; + isoDeliverables: ComplianceIsoDeliverable[]; +}; + +type ComplianceBootstrapSummary = { + suppliersAdded: number; + dpasAdded: number; + evidenceAdded: number; + notes: string[]; +}; +type StatsAutomationSettings = { + enabled: boolean; + frequencies: Record | "weekly", boolean>; + recipientEmails: string[]; + lastGeneratedCycleKeys: Partial, string>>; +}; +type StatsReportHistoryEntry = { + id: string; + period: "weekly" | Exclude; + periodLabel: string; + startDate: string; + endDate: string; + generatedAt: string; + pdfUrl: string; + pdfName: string; + excelUrl: string; + excelName: string; + cycleKey: string; + generatedBy: "scheduler" | "manual"; +}; + +type AssociationMapSettings = { + publicStyleUrl: string; + adminStyleUrl: string; + styleUrl: string; +}; + +const salleAnalyticsCatalog = [ + { id: "bureau_11_17", name: "Bureau 11–17 m²" }, + { id: "salle_toucan", name: "Salle TOUCAN" }, + { id: "salle_ibis", name: "Salle IBIS" }, + { id: "salle_pelican", name: "Salle PELICAN" }, + { id: "dojo", name: "DOJO" }, + { id: "hall_amphitheatre", name: "Hall-Amphithéâtre" }, + { id: "vestiaires", name: "Vestiaires" }, +] as const; +type SalleAnalyticsRoomId = (typeof salleAnalyticsCatalog)[number]["id"]; + +function sanitizeStatsAutomationSettings(rawValue?: unknown): StatsAutomationSettings { + const source = rawValue && typeof rawValue === "object" ? rawValue as Record : {}; + const parseEmails = (value: unknown) => Array.isArray(value) + ? value.map((entry) => normalizeEmail(String(entry || ""))).filter(Boolean) + : []; + + return { + enabled: source.enabled !== false, + frequencies: { + weekly: source.frequencies && typeof source.frequencies === "object" ? Boolean((source.frequencies as Record).weekly) : true, + month: source.frequencies && typeof source.frequencies === "object" ? Boolean((source.frequencies as Record).month) : true, + quarter: source.frequencies && typeof source.frequencies === "object" ? Boolean((source.frequencies as Record).quarter) : true, + semester: source.frequencies && typeof source.frequencies === "object" ? Boolean((source.frequencies as Record).semester) : true, + year: source.frequencies && typeof source.frequencies === "object" ? Boolean((source.frequencies as Record).year) : true, + }, + recipientEmails: parseEmails(source.recipientEmails), + lastGeneratedCycleKeys: + source.lastGeneratedCycleKeys && typeof source.lastGeneratedCycleKeys === "object" + ? Object.fromEntries( + Object.entries(source.lastGeneratedCycleKeys as Record) + .filter(([, value]) => typeof value === "string" && value.trim().length > 0) + .map(([key, value]) => [key, String(value)]) + ) + : {}, + }; +} + +function sanitizeStatsReportHistory(rawValue?: unknown): StatsReportHistoryEntry[] { + if (!Array.isArray(rawValue)) return []; + return rawValue + .filter((entry) => entry && typeof entry === "object") + .map((entry) => entry as Record) + .filter((entry) => + typeof entry.id === "string" + && typeof entry.period === "string" + && typeof entry.periodLabel === "string" + && typeof entry.generatedAt === "string" + && typeof entry.pdfUrl === "string" + && typeof entry.excelUrl === "string" + ) + .map((entry) => ({ + id: String(entry.id), + period: entry.period as StatsReportHistoryEntry["period"], + periodLabel: String(entry.periodLabel), + startDate: String(entry.startDate || ""), + endDate: String(entry.endDate || ""), + generatedAt: String(entry.generatedAt), + pdfUrl: String(entry.pdfUrl), + pdfName: String(entry.pdfName || "rapport.pdf"), + excelUrl: String(entry.excelUrl), + excelName: String(entry.excelName || "rapport.xlsx"), + cycleKey: String(entry.cycleKey || ""), + generatedBy: entry.generatedBy === "manual" ? "manual" : "scheduler", + })); +} + +function sanitizeAssociationMapSettings(rawValue?: unknown): AssociationMapSettings { + const source = rawValue && typeof rawValue === "object" ? rawValue as Record : {}; + const fallbackStyleUrl = typeof source.styleUrl === "string" && source.styleUrl.trim().length > 0 + ? source.styleUrl.trim() + : "https://www.portail-association973.com/map-styles/openmaptiles-positron/style.json"; + const publicStyleUrl = typeof source.publicStyleUrl === "string" && source.publicStyleUrl.trim().length > 0 + ? source.publicStyleUrl.trim() + : fallbackStyleUrl; + const adminStyleUrl = typeof source.adminStyleUrl === "string" && source.adminStyleUrl.trim().length > 0 + ? source.adminStyleUrl.trim() + : "https://www.portail-association973.com/map-styles/openmaptiles-positron-admin/style.json"; + + return { + publicStyleUrl, + adminStyleUrl, + styleUrl: publicStyleUrl, + }; +} + +function parseStoredList(rawValue: string | null, schema: z.ZodType) { + if (!rawValue) return [] as T[]; + try { + const parsed = JSON.parse(rawValue); + if (!Array.isArray(parsed)) return [] as T[]; + return parsed + .map((entry) => { + try { + return schema.parse(entry); + } catch { + return null; + } + }) + .filter(Boolean) as T[]; + } catch { + return [] as T[]; + } +} + +function parseComplianceDate(input?: string | null) { + if (!input) return null; + const parsed = new Date(input); + return Number.isNaN(parsed.getTime()) ? null : parsed; +} + +function formatRecencyStatus(date: Date | null, warningAfterDays: number) { + if (!date) { + return { status: "missing" as const, ageDays: null }; + } + const ageMs = Date.now() - date.getTime(); + const ageDays = Math.max(0, Math.floor(ageMs / (1000 * 60 * 60 * 24))); + if (ageDays > warningAfterDays) { + return { status: "warning" as const, ageDays }; + } + return { status: "ok" as const, ageDays }; +} + +async function getComplianceOperations(): Promise { + const [ + suppliersRaw, + dpasRaw, + backupsRaw, + retentionHistoryRaw, + evidenceRaw, + retentionReportRaw, + isoDeliverablesRaw, + ] = await Promise.all([ + db.getPortalSetting(COMPLIANCE_SUPPLIERS_SETTING_KEY), + db.getPortalSetting(COMPLIANCE_DPA_SETTING_KEY), + db.getPortalSetting(COMPLIANCE_BACKUP_SETTING_KEY), + db.getPortalSetting(COMPLIANCE_RETENTION_HISTORY_SETTING_KEY), + db.getPortalSetting(COMPLIANCE_EVIDENCE_SETTING_KEY), + db.getPortalSetting("retention.lastReport"), + db.getPortalSetting(COMPLIANCE_ISO_DELIVERABLES_SETTING_KEY), + ]); + + const backupsPayload = backupsRaw ? (() => { + try { + const parsed = JSON.parse(backupsRaw) as Record; + return { + backupRecords: Array.isArray(parsed.backupRecords) + ? parsed.backupRecords.map((entry) => { + try { return complianceBackupRecordSchema.parse(entry); } catch { return null; } + }).filter(Boolean) as ComplianceBackupRecord[] + : [], + restoreTests: Array.isArray(parsed.restoreTests) + ? parsed.restoreTests.map((entry) => { + try { return complianceRestoreTestSchema.parse(entry); } catch { return null; } + }).filter(Boolean) as ComplianceRestoreTest[] + : [], + }; + } catch { + return { backupRecords: [], restoreTests: [] }; + } + })() : { backupRecords: [], restoreTests: [] }; + + const retentionReports = parseStoredList(retentionHistoryRaw, complianceRetentionReportSchema); + + if (retentionReportRaw) { + try { + const latest = JSON.parse(retentionReportRaw) as Record; + if (typeof latest.generatedAt === "string" && !retentionReports.some((entry) => entry.generatedAt === latest.generatedAt)) { + retentionReports.unshift(complianceRetentionReportSchema.parse({ + id: `auto-${latest.generatedAt}`, + generatedAt: String(latest.generatedAt), + periodLabel: "Rapport automatique quotidien", + deletedAccounts: Number(latest.purgedUsers || 0), + pendingPurge: Number(latest.pendingDeletionCount || 0), + anonymizedRecords: Number(latest.purgedUsers || 0), + executedPurges: Number(latest.purgedUsers || 0), + legalHoldCount: 0, + incidentsCount: 0, + notes: "", + })); + } + } catch { + // ignore malformed latest report + } + } + + return { + suppliers: parseStoredList(suppliersRaw, complianceSupplierSchema), + dpas: parseStoredList(dpasRaw, complianceDpaSchema), + backupRecords: backupsPayload.backupRecords, + restoreTests: backupsPayload.restoreTests, + retentionReports, + evidenceCenter: parseStoredList(evidenceRaw, complianceEvidenceSchema), + isoDeliverables: mergeComplianceEntriesById( + parseStoredList(isoDeliverablesRaw, complianceIsoDeliverableSchema), + buildDefaultIsoDeliverables(), + ).entries.sort((a, b) => a.reference.localeCompare(b.reference, "fr")), + }; +} + +async function saveComplianceBackups(payload: { + backupRecords: ComplianceBackupRecord[]; + restoreTests: ComplianceRestoreTest[]; +}) { + await db.setPortalSetting( + COMPLIANCE_BACKUP_SETTING_KEY, + JSON.stringify(payload), + "Supervision des sauvegardes, restaurations et preuves associées" + ); +} + +function buildDefaultIsoDeliverables(): ComplianceIsoDeliverable[] { + return [ + { + id: "iso-pol-001", + reference: "POL-001", + title: "Politique de securite de l'information", + domain: "gouvernance", + status: "a_rediger", + owner: "", + approverName: "", + approverEmail: "", + summary: "Document cadre du SMSI, perimetre, principes de protection et gouvernance.", + notes: "", + lastReviewedAt: "", + validatedAt: "", + revisionDueAt: "", + sections: [ + { id: "scope", title: "Perimetre", content: "", guidance: "Definir les activites, services et environnements couverts par le SMSI." }, + { id: "principles", title: "Principes directeurs", content: "", guidance: "Preciser confidentialite, integrite, disponibilite et tracabilite." }, + { id: "governance", title: "Gouvernance et responsabilites", content: "", guidance: "Nommer les roles de direction, RSSI, DPO et exploitation." }, + ], + }, + { + id: "iso-sec-001", + reference: "SEC-001", + title: "Gestion des acces et habilitations", + domain: "acces", + status: "en_cours", + owner: "", + approverName: "", + approverEmail: "", + summary: "Controle des acces, MFA, moindre privilege et revues d'habilitations.", + notes: "", + lastReviewedAt: "", + validatedAt: "", + revisionDueAt: "", + sections: [ + { id: "roles", title: "Roles et droits", content: "", guidance: "Documenter les profils internes et les niveaux d'acces." }, + { id: "mfa", title: "Authentification forte", content: "", guidance: "Definir les roles soumis a MFA obligatoire et les exceptions." }, + { id: "review", title: "Revue periodique", content: "", guidance: "Prevoir la frequence de revue et le circuit de validation." }, + ], + }, + { + id: "iso-sec-002", + reference: "SEC-002", + title: "Sauvegardes et restauration", + domain: "sauvegardes", + status: "en_cours", + owner: "", + approverName: "", + approverEmail: "", + summary: "Strategie de sauvegarde, retention, tests de restauration et preuves d'execution.", + notes: "", + lastReviewedAt: "", + validatedAt: "", + revisionDueAt: "", + sections: [ + { id: "strategy", title: "Strategie de sauvegarde", content: "", guidance: "Preciser la regle 3-2-1, les frequences et les supports." }, + { id: "restore", title: "Procedure de restauration", content: "", guidance: "Decrire la sequence de restauration et les responsables." }, + { id: "evidence", title: "Preuves et revues", content: "", guidance: "Lister les rapports, journaux et controles mensuels attendus." }, + ], + }, + { + id: "iso-sec-003", + reference: "SEC-003", + title: "Gestion des incidents de securite", + domain: "incidents", + status: "en_cours", + owner: "", + approverName: "", + approverEmail: "", + summary: "Detection, qualification, confinement, remediations et retours d'experience.", + notes: "", + lastReviewedAt: "", + validatedAt: "", + revisionDueAt: "", + sections: [ + { id: "classification", title: "Classification des incidents", content: "", guidance: "Definir les niveaux de gravite et les SLA internes." }, + { id: "workflow", title: "Workflow de traitement", content: "", guidance: "Decrire detection, confinement, investigation, correction et cloture." }, + { id: "rgpd", title: "Lien avec les violations RGPD", content: "", guidance: "Documenter l'escalade CNIL et l'information des personnes si necessaire." }, + ], + }, + { + id: "iso-sec-005", + reference: "SEC-005", + title: "Gestion des fournisseurs et sous-traitants", + domain: "fournisseurs", + status: "en_cours", + owner: "", + approverName: "", + approverEmail: "", + summary: "Registre fournisseurs, DPA, criticite, revues annuelles et preuves contractuelles.", + notes: "", + lastReviewedAt: "", + validatedAt: "", + revisionDueAt: "", + sections: [ + { id: "inventory", title: "Inventaire des fournisseurs", content: "", guidance: "Identifier hebergement, SMTP, analytics, sauvegardes et services tiers." }, + { id: "contracts", title: "Contrats et DPA", content: "", guidance: "Tracer les statuts signes, a renouveler et a recuperer." }, + { id: "annual-review", title: "Revue annuelle", content: "", guidance: "Definir le responsable, la date et les criteres de revue." }, + ], + }, + { + id: "iso-risk-001", + reference: "ISO27001-RISK-001", + title: "Analyse des risques", + domain: "risques", + status: "a_rediger", + owner: "", + approverName: "", + approverEmail: "", + summary: "Identification des actifs, menaces, vulnerabilites, impacts et plans de traitement.", + notes: "", + lastReviewedAt: "", + validatedAt: "", + revisionDueAt: "", + sections: [ + { id: "assets", title: "Actifs et perimetre", content: "", guidance: "Lister les actifs informationnels et les proprietaires." }, + { id: "risks", title: "Evaluation des risques", content: "", guidance: "Qualifier probabilite, impact et niveau de risque." }, + { id: "treatment", title: "Plan de traitement", content: "", guidance: "Associer les mesures, responsables et dates cibles." }, + ], + }, + { + id: "iso-bcp-001", + reference: "SEC-004", + title: "Continuite et reprise d'activite", + domain: "continuite", + status: "a_rediger", + owner: "", + approverName: "", + approverEmail: "", + summary: "PCA/PRA, RTO/RPO, scenarios majeurs et organisation de crise.", + notes: "", + lastReviewedAt: "", + validatedAt: "", + revisionDueAt: "", + sections: [ + { id: "scenarios", title: "Scenarios de rupture", content: "", guidance: "Panne serveur, corruption, cyberattaque, indisponibilite hebergeur." }, + { id: "targets", title: "Objectifs de reprise", content: "", guidance: "Fixer RTO, RPO et priorites de remise en service." }, + { id: "exercise", title: "Tests et exercices", content: "", guidance: "Prevoir la frequence des exercices et les comptes-rendus." }, + ], + }, + { + id: "iso-audit-001", + reference: "ISO27001-AUDIT-001", + title: "Preparation audit et declaration d'applicabilite", + domain: "audit", + status: "a_rediger", + owner: "", + approverName: "", + approverEmail: "", + summary: "Centralisation des preuves, ecarts, controles applicables et readiness audit.", + notes: "", + lastReviewedAt: "", + validatedAt: "", + revisionDueAt: "", + sections: [ + { id: "soa", title: "Declaration d'applicabilite", content: "", guidance: "Documenter les controles applicables, exclus et justifications." }, + { id: "evidence", title: "Corpus de preuves", content: "", guidance: "Lister les preuves documentaires et techniques a fournir." }, + { id: "gaps", title: "Ecarts residuels", content: "", guidance: "Consolider les points restants avant audit." }, + ], + }, + ]; +} + +function mergeComplianceEntriesById(existing: T[], additions: T[]) { + const knownIds = new Set(existing.map((entry) => entry.id)); + const merged = [...existing]; + let addedCount = 0; + + for (const entry of additions) { + if (knownIds.has(entry.id)) continue; + merged.push(entry); + knownIds.add(entry.id); + addedCount += 1; + } + + return { entries: merged, addedCount }; +} + +function todayIsoDate() { + return new Date().toISOString().slice(0, 10); +} + +function plusDaysIsoDate(days: number) { + const date = new Date(); + date.setDate(date.getDate() + days); + return date.toISOString().slice(0, 10); +} + +function toProviderLabel(input: string) { + const normalized = String(input || "").trim().toLowerCase(); + if (normalized === "ovh") return "OVHcloud"; + if (normalized === "gmail") return "Google Workspace / Gmail"; + if (normalized === "outlook") return "Microsoft 365 / Outlook"; + return normalized ? normalized : "Fournisseur à confirmer"; +} + +function toProviderLocation(input: string) { + const normalized = String(input || "").trim().toLowerCase(); + if (normalized === "ovh") return "UE"; + if (normalized === "gmail" || normalized === "outlook") { + return "À confirmer contractuellement"; + } + return "À confirmer"; +} + +async function getDocUpdatedAt(relativePath: string) { + try { + const absolutePath = path.resolve(process.cwd(), relativePath); + const stat = await fs.stat(absolutePath); + return stat.mtime.toISOString(); + } catch { + return new Date().toISOString(); + } +} + +async function buildComplianceBootstrapPayload(existing: ComplianceOperationsPayload): Promise<{ + suppliers: ComplianceSupplier[]; + dpas: ComplianceDpa[]; + evidenceCenter: ComplianceEvidence[]; + notes: string[]; +}> { + const [ + mailSettings, + oauthStatus, + helloAssoSettings, + operationalTourHistory, + retentionReportRaw, + sec001UpdatedAt, + sec002UpdatedAt, + sec002aUpdatedAt, + sec002bUpdatedAt, + sec003UpdatedAt, + sec005UpdatedAt, + rgpd002UpdatedAt, + rgpd005UpdatedAt, + rgpd006UpdatedAt, + rgpd007UpdatedAt, + rgpd008UpdatedAt, + ] = await Promise.all([ + getAdminMailSettings(), + Promise.resolve(getOAuthProviderStatus()), + getHelloAssoSettings(), + getOperationalTourHistory(), + db.getPortalSetting("retention.lastReport"), + getDocUpdatedAt("docs/compliance/SEC-001-gestion-des-acces.md"), + getDocUpdatedAt("docs/compliance/SEC-002-gestion-des-sauvegardes-et-restauration.md"), + getDocUpdatedAt("docs/compliance/SEC-002A-preuve-execution-sauvegarde.md"), + getDocUpdatedAt("docs/compliance/SEC-002B-test-restauration-historise.md"), + getDocUpdatedAt("docs/compliance/SEC-003-gestion-des-incidents.md"), + getDocUpdatedAt("docs/compliance/SEC-005-registre-fournisseurs-et-sous-traitants.md"), + getDocUpdatedAt("docs/compliance/RGPD-002-registre-des-traitements.md"), + getDocUpdatedAt("docs/compliance/RGPD-005-accord-de-traitement-des-donnees-dpa.md"), + getDocUpdatedAt("docs/compliance/RGPD-006-politique-de-retention-des-donnees.md"), + getDocUpdatedAt("docs/compliance/RGPD-007-procedure-suppression-et-purge-planifiee.md"), + getDocUpdatedAt("docs/compliance/RGPD-008-rapport-mensuel-dpo-exploitation.md"), + ]); + + const notes: string[] = []; + const suppliers: ComplianceSupplier[] = []; + const dpas: ComplianceDpa[] = []; + const evidenceCenter: ComplianceEvidence[] = []; + const reviewDate = todayIsoDate(); + + suppliers.push({ + id: "ovh-hosting", + supplierName: "OVHcloud", + service: "Hébergement principal du portail, DNS et exposition web", + location: "UE", + criticality: "critique", + dpaStatus: "pending", + reviewDate, + owner: "RSSI / exploitation CCDS", + notes: "Prestataire d’hébergement principal déclaré pour la production et la préproduction du portail.", + }); + + dpas.push({ + id: "dpa-ovh-hosting", + supplierId: "ovh-hosting", + supplierName: "OVHcloud", + status: "pending", + signedAt: "", + expiresAt: "", + reviewDueAt: plusDaysIsoDate(365), + owner: "DPO / RSSI", + notes: "DPA et clauses de sécurité OVH à centraliser dans le portail.", + }); + + if (mailSettings.hasSavedConfig || mailSettings.ready) { + const supplierId = `smtp-${mailSettings.smtpProvider}`; + const providerLabel = toProviderLabel(mailSettings.smtpProvider); + const providerLocation = toProviderLocation(mailSettings.smtpProvider); + const mailStatus = mailSettings.ready ? "pending" : "missing"; + suppliers.push({ + id: supplierId, + supplierName: providerLabel, + service: `Messagerie transactionnelle / SMTP (${mailSettings.smtpProvider === "custom" ? (mailSettings.smtpHost || "hôte personnalisé") : mailSettings.smtpProvider})`, + location: providerLocation, + criticality: "elevee", + dpaStatus: mailStatus, + reviewDate, + owner: "Administration du portail", + notes: mailSettings.ready + ? `Configuration SMTP détectée (${mailSettings.source}). Contrat DPA et revue annuelle à verser.` + : "Configuration SMTP partielle détectée. Finaliser les paramètres et centraliser le contrat avant validation.", + }); + dpas.push({ + id: `dpa-${supplierId}`, + supplierId, + supplierName: providerLabel, + status: mailSettings.ready ? "pending" : "expired", + signedAt: "", + expiresAt: "", + reviewDueAt: plusDaysIsoDate(180), + owner: "DPO / administration du portail", + notes: mailSettings.ready + ? "Ajouter le DPA réel du fournisseur de messagerie et sa date de révision." + : "Le fournisseur SMTP est détecté mais reste incomplet ou non validé contractuellement.", + }); + } else { + notes.push("Aucun fournisseur SMTP confirmé n’a été auto-détecté dans la configuration active."); + } + + if (oauthStatus.google) { + suppliers.push({ + id: "google-oauth", + supplierName: "Google", + service: "Authentification sociale Google", + location: "À confirmer contractuellement", + criticality: "elevee", + dpaStatus: "pending", + reviewDate, + owner: "RSSI / administration du portail", + notes: "Client OAuth Google configuré dans l’application.", + }); + dpas.push({ + id: "dpa-google-oauth", + supplierId: "google-oauth", + supplierName: "Google", + status: "pending", + signedAt: "", + expiresAt: "", + reviewDueAt: plusDaysIsoDate(180), + owner: "DPO / RSSI", + notes: "Vérifier le cadre contractuel et les clauses RGPD liées à la connexion Google.", + }); + } + + if (oauthStatus.facebook) { + suppliers.push({ + id: "meta-oauth", + supplierName: "Meta / Facebook", + service: "Authentification sociale Facebook", + location: "À confirmer contractuellement", + criticality: "moyenne", + dpaStatus: "pending", + reviewDate, + owner: "RSSI / administration du portail", + notes: "Client OAuth Facebook configuré dans l’application.", + }); + dpas.push({ + id: "dpa-meta-oauth", + supplierId: "meta-oauth", + supplierName: "Meta / Facebook", + status: "pending", + signedAt: "", + expiresAt: "", + reviewDueAt: plusDaysIsoDate(180), + owner: "DPO / RSSI", + notes: "Vérifier le cadre contractuel et la localisation effective des traitements.", + }); + } + + if (helloAssoSettings.enabled && helloAssoSettings.clientId) { + suppliers.push({ + id: "helloasso-api", + supplierName: "HelloAsso", + service: "API d’inscription et d’enrichissement des associations", + location: "À confirmer contractuellement", + criticality: "elevee", + dpaStatus: "pending", + reviewDate, + owner: "Administration associations / DPO", + notes: "Synchronisation HelloAsso activable depuis le portail. Validation manuelle des créations maintenue.", + }); + dpas.push({ + id: "dpa-helloasso-api", + supplierId: "helloasso-api", + supplierName: "HelloAsso", + status: "pending", + signedAt: "", + expiresAt: "", + reviewDueAt: plusDaysIsoDate(180), + owner: "DPO / administration associations", + notes: "Ajouter les engagements RGPD et le support contractuel de l’intégration HelloAsso.", + }); + } + + evidenceCenter.push( + { + id: "evidence-sec-001", + category: "security_policy", + title: "Politique MFA et gestion des accès", + reference: "SEC-001", + url: "", + description: "Document interne de gestion des accès et politique MFA obligatoire par rôle.", + updatedAt: sec001UpdatedAt, + }, + { + id: "evidence-sec-002", + category: "backup_report", + title: "Politique de sauvegardes et restauration", + reference: "SEC-002", + url: "", + description: "Procédure documentaire 3-2-1, RPO/RTO et cadre de restauration.", + updatedAt: sec002UpdatedAt, + }, + { + id: "evidence-sec-002a", + category: "backup_report", + title: "Fiche de preuve d'exécution des sauvegardes", + reference: "SEC-002A", + url: "", + description: "Modèle opérationnel pour rattacher date, statut, volume, rétention et source de preuve à chaque sauvegarde utile.", + updatedAt: sec002aUpdatedAt, + }, + { + id: "evidence-sec-002b", + category: "restore_test", + title: "Fiche de test de restauration historisé", + reference: "SEC-002B", + url: "", + description: "Modèle de trace pour démontrer qu'une restauration a réellement été testée, validée et commentée.", + updatedAt: sec002bUpdatedAt, + }, + { + id: "evidence-sec-003", + category: "incident_report", + title: "Procédure de gestion des incidents de sécurité", + reference: "SEC-003", + url: "", + description: "Classification, confinement, investigation, notification RGPD et revue post-incident.", + updatedAt: sec003UpdatedAt, + }, + { + id: "evidence-sec-005", + category: "dpa", + title: "Registre fournisseurs et sous-traitants", + reference: "SEC-005", + url: "", + description: "Référentiel documentaire de suivi des prestataires, criticités et révisions annuelles.", + updatedAt: sec005UpdatedAt, + }, + { + id: "evidence-rgpd-002", + category: "processing_register", + title: "Registre des traitements", + reference: "RGPD-002", + url: "", + description: "Base documentaire du registre des traitements du portail.", + updatedAt: rgpd002UpdatedAt, + }, + { + id: "evidence-rgpd-005", + category: "dpa", + title: "Modèle d’accord de traitement des données", + reference: "RGPD-005", + url: "", + description: "Gabarit DPA à rattacher aux fournisseurs réellement utilisés.", + updatedAt: rgpd005UpdatedAt, + }, + { + id: "evidence-rgpd-006", + category: "retention_report", + title: "Politique de rétention des données", + reference: "RGPD-006", + url: "", + description: "Politique de conservation, archivage et suppression des données personnelles.", + updatedAt: rgpd006UpdatedAt, + }, + { + id: "evidence-rgpd-007", + category: "retention_report", + title: "Procédure de suppression et purge planifiée", + reference: "RGPD-007", + url: "", + description: "Cycle de suppression différée, retention 30 jours, legal hold et audit de purge.", + updatedAt: rgpd007UpdatedAt, + }, + { + id: "evidence-rgpd-008", + category: "audit_report", + title: "Rapport mensuel DPO et exploitation", + reference: "RGPD-008", + url: "", + description: "Cadre mensuel pour suivre sauvegardes, restaurations, incidents, rétention, écarts et actions de conformité.", + updatedAt: rgpd008UpdatedAt, + }, + { + id: "evidence-public-privacy", + category: "security_policy", + title: "Politique de confidentialité publique", + reference: "Portail public", + url: "/confidentialite?returnTo=%2Fadmin%3Ftab%3Dconformite", + description: "Version publique exposée dans le portail.", + updatedAt: new Date().toISOString(), + }, + { + id: "evidence-public-mentions", + category: "security_policy", + title: "Mentions légales publiques", + reference: "Portail public", + url: "/mentions-legales?returnTo=%2Fadmin%3Ftab%3Dconformite", + description: "Version publique exposée dans le portail.", + updatedAt: new Date().toISOString(), + }, + { + id: "evidence-public-cookies", + category: "security_policy", + title: "Politique cookies publique", + reference: "Portail public", + url: "/cookies?returnTo=%2Fadmin%3Ftab%3Dconformite", + description: "Version publique exposée dans le portail.", + updatedAt: new Date().toISOString(), + }, + { + id: "evidence-public-cgu", + category: "security_policy", + title: "CGU publiques", + reference: "Portail public", + url: "/cgu?returnTo=%2Fadmin%3Ftab%3Dconformite", + description: "Version publique exposée dans le portail.", + updatedAt: new Date().toISOString(), + }, + ); + + const latestProductionTour = operationalTourHistory.find((entry) => entry.environment === "production"); + const latestSecondaryTour = operationalTourHistory.find((entry) => entry.environment === "secondary"); + + if (latestProductionTour) { + evidenceCenter.push({ + id: "evidence-tour-production", + category: "audit_report", + title: "Dernier tour opérationnel production", + reference: "Tour opérationnel", + url: "/admin?tab=dashboard", + description: `${latestProductionTour.summary.ok} contrôles OK, ${latestProductionTour.summary.warning} avertissement(s), ${latestProductionTour.summary.error} erreur(s).`, + updatedAt: latestProductionTour.finishedAt, + }); + } else { + notes.push("Aucun tour opérationnel de production n’a encore été trouvé pour l’archivage automatique."); + } + + if (latestSecondaryTour) { + evidenceCenter.push({ + id: "evidence-tour-secondary", + category: "audit_report", + title: "Dernier tour opérationnel préproduction", + reference: "Tour opérationnel", + url: "/admin?tab=dashboard", + description: `${latestSecondaryTour.summary.ok} contrôles OK, ${latestSecondaryTour.summary.warning} avertissement(s), ${latestSecondaryTour.summary.error} erreur(s).`, + updatedAt: latestSecondaryTour.finishedAt, + }); + } else { + notes.push("Aucun tour opérationnel de préproduction n’a encore été trouvé pour l’archivage automatique."); + } + + if (retentionReportRaw) { + try { + const latestRetention = JSON.parse(retentionReportRaw) as Record; + const generatedAt = typeof latestRetention.generatedAt === "string" ? latestRetention.generatedAt : new Date().toISOString(); + evidenceCenter.push({ + id: "evidence-retention-latest", + category: "retention_report", + title: "Dernier rapport automatique de rétention", + reference: "Retention auto", + url: "", + description: `Purges du run: ${Number(latestRetention.purgedUsers || 0)} • comptes en attente: ${Number(latestRetention.pendingDeletionCount || 0)}.`, + updatedAt: generatedAt, + }); + } catch { + notes.push("Le dernier rapport automatique de rétention existe mais n’a pas pu être interprété automatiquement."); + } + } else { + notes.push("Aucun rapport automatique de rétention n’a encore été détecté."); + } + + if (existing.backupRecords.length === 0) { + notes.push("Les preuves d’exécution des sauvegardes restent à saisir manuellement ou à brancher depuis l’exploitation."); + } + if (existing.restoreTests.length === 0) { + notes.push("Aucun test de restauration historisé n’est encore présent dans le tableau de pilotage."); + } + + return { suppliers, dpas, evidenceCenter, notes }; +} + +const optionalEmailFieldSchema = z + .string() + .trim() + .email("Adresse email invalide") + .optional() + .or(z.literal("")); + +type SalleBillingSettings = { + ribDocumentUrl: string; + ribDocumentName: string; + ribMimeType: string; + ribUploadedAt: string; +}; + +const internalDirectoryContactSchema = z.object({ + name: z.string().trim().min(1).max(160), + role: z + .string() + .trim() + .max(200) + .optional() + .or(z.literal("")) + .transform((value) => { + const normalized = String(value || "").trim(); + return normalized || "Fonction à compléter"; + }), + email: z.string().trim().email().optional().or(z.literal("")).transform((value) => value || undefined), + phone: z.string().trim().max(60).optional().or(z.literal("")).transform((value) => value || undefined), + mobile: z.string().trim().max(60).optional().or(z.literal("")).transform((value) => value || undefined), + extension: z.string().trim().max(20).optional().or(z.literal("")).transform((value) => value || undefined), +}); + +const internalDirectoryServiceSchema = z.object({ + id: z.string().trim().min(1).max(120), + title: z.string().trim().min(1).max(200), + description: z.string().trim().min(1).max(500), + contacts: z.array(internalDirectoryContactSchema).max(100), +}); + +const internalDirectoryDivisionSchema = z.object({ + id: z.string().trim().min(1).max(120), + title: z.string().trim().min(1).max(200), + description: z.string().trim().min(1).max(500), + accentClassName: z.string().trim().min(1).max(200), + services: z.array(internalDirectoryServiceSchema).max(100), +}); + +// Admin procedure - only for admin users +const adminProcedure = protectedProcedure.use(({ ctx, next }) => { + if (ctx.user.role !== 'admin' && ctx.user.role !== 'super_admin') { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux administrateurs' }); + } + return next({ ctx }); +}); + +// Super admin procedure +const superAdminProcedure = protectedProcedure.use(({ ctx, next }) => { + if (ctx.user.role !== 'super_admin') { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux super administrateurs' }); + } + return next({ ctx }); +}); + +function canAccessAccueilBackoffice(user: { role: string }) { + return user.role === "accueil" || user.role === "logistique_controle" || user.role === "admin" || user.role === "super_admin"; +} + +function canAccessTerrainWorkspace(user: { role: string }) { + return user.role === "service_terrain" || user.role === "super_admin"; +} + +function canReadInternalRequest(user: { role: string; canSignSalle?: boolean | null }) { + return canAccessAccueilBackoffice(user) || canSignSalle(user); +} + +const accueilAdminProcedure = protectedProcedure.use(({ ctx, next }) => { + if (!canAccessAccueilBackoffice(ctx.user)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Accès réservé à l’accueil, aux administrateurs et aux super administrateurs", + }); + } + return next({ ctx }); +}); + +const terrainProcedure = protectedProcedure.use(({ ctx, next }) => { + if (!canAccessTerrainWorkspace(ctx.user)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Accès réservé au service terrain désigné", + }); + } + return next({ ctx }); +}); + +const logisticsProcedure = protectedProcedure.use(({ ctx, next }) => { + assertLogisticsAccess(ctx.user); + return next({ ctx }); +}); + +function canManageLogistics(user: { role: string; canManageLogistics?: boolean | null }) { + return user.role === "super_admin" || user.role === "logistique_controle" || Boolean(user.canManageLogistics); +} + +function assertLogisticsAccess(user: { role: string; canManageLogistics?: boolean | null }) { + if (!canManageLogistics(user)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Accès réservé au service logistique et contrôle désigné par le super administrateur", + }); + } +} + +function canSignSalle(user: { role: string; canSignSalle?: boolean | null }) { + return user.role === "super_admin" || user.role === "directrice" || Boolean(user.canSignSalle); +} + +const salleSignerProcedure = protectedProcedure.use(({ ctx, next }) => { + if (!canSignSalle(ctx.user)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Accès réservé à la Directrice ou à son délégataire de signature", + }); + } + return next({ ctx }); +}); + +const directriceProcedure = protectedProcedure.use(({ ctx, next }) => { + if (ctx.user.role !== "directrice" && ctx.user.role !== "super_admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Accès réservé à la Directrice", + }); + } + return next({ ctx }); +}); + +const salleReadProcedure = protectedProcedure.use(({ ctx, next }) => { + if (!canAccessAccueilBackoffice(ctx.user) && !canSignSalle(ctx.user)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Accès réservé aux profils autorisés à consulter le suivi des salles", + }); + } + return next({ ctx }); +}); + +const ACTIVE_MATERIAL_AVAILABILITY_STATUSES = new Set([ + "soumise", + "en_cours_traitement", + "information_complementaire", + "validee", +]); + +// Helper to log admin actions +async function logAdminAction(userId: number, action: string, entityType: string, entityId?: number, details?: any) { + try { + await db.createAuditLog({ + userId, + action, + entityType, + entityId, + details: details ? JSON.stringify(details) : undefined, + ipAddress: typeof details?.ipAddress === "string" ? details.ipAddress : undefined, + }); + } catch (e) { + console.error('Failed to log admin action:', e); + } +} + +function extractClientIp(req: { headers?: Record; socket?: { remoteAddress?: string | null } }) { + const forwardedFor = req.headers?.["x-forwarded-for"]; + if (typeof forwardedFor === "string" && forwardedFor.trim()) { + return forwardedFor.split(",")[0]?.trim() || null; + } + const realIp = req.headers?.["x-real-ip"]; + if (typeof realIp === "string" && realIp.trim()) { + return realIp.trim(); + } + return req.socket?.remoteAddress || null; +} + +function assertDataPrivacyConsent(consent: DataPrivacyConsent | undefined) { + if (!consent?.accepted) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "La validation des données personnelles est requise pour poursuivre.", + }); + } + + if (consent.version !== DATA_PRIVACY_NOTICE_VERSION) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "La version d'information sur les données personnelles n'est plus à jour. Merci de relire puis de confirmer à nouveau.", + }); + } +} + +async function recordDataPrivacyConsent( + userId: number, + req: { headers?: Record; socket?: { remoteAddress?: string | null } }, + consent: DataPrivacyConsent +) { + await db.updateUser(userId, { + privacyConsentVersion: consent.version, + privacyConsentAcceptedAt: new Date(), + privacyConsentContext: consent.context, + }); + + try { + await db.createAuditLog({ + userId, + action: "privacy_consent", + entityType: "privacy", + details: JSON.stringify({ + version: consent.version, + context: consent.context, + }), + ipAddress: extractClientIp(req) || undefined, + }); + } catch (error) { + console.error("Failed to record privacy consent audit:", error); + } +} + +function validateReservationSalleFormData(rawFormData?: string) { + if (!rawFormData) return; + + let formData: any; + try { + formData = JSON.parse(rawFormData); + } catch { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Le formulaire de réservation de salle est invalide' }); + } + + if (formData?.formulaireType !== 'reservation_salle_mjs') { + return; + } + + const requiredFields = [ + { key: 'motifReservation', label: 'Motif de la réservation' }, + { key: 'dateReservation', label: 'Date de début' }, + { key: 'dateFinReservation', label: 'Date de fin' }, + { key: 'typeUsage', label: "Type d'usage" }, + { key: 'frequence', label: 'Fréquence' }, + { key: 'nombreParticipants', label: 'Nombre de participants' }, + ]; + + const missing = requiredFields.find(field => !String(formData?.[field.key] ?? '').trim()); + if (missing) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `${missing.label} est obligatoire pour une demande de salle`, + }); + } + + if (String(formData.dateFinReservation) < String(formData.dateReservation)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'La date de fin doit être postérieure ou égale à la date de début', + }); + } + + if (formData.useDetailedSchedule) { + const slots = Array.isArray(formData.horairesParJour) ? formData.horairesParJour : []; + if (slots.length === 0) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Veuillez renseigner au moins un créneau détaillé pour cette demande de salle', + }); + } + + for (const slot of slots) { + if (!String(slot?.date || '').trim() || !String(slot?.heureDebut || '').trim() || !String(slot?.heureFin || '').trim()) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Chaque créneau détaillé doit contenir une date, une heure de début et une heure de fin', + }); + } + if (String(slot.heureFin) <= String(slot.heureDebut)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Chaque créneau détaillé doit avoir une heure de fin postérieure à l’heure de début', + }); + } + if (String(slot.date) < String(formData.dateReservation) || String(slot.date) > String(formData.dateFinReservation)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Un créneau détaillé ne correspond pas à la période sélectionnée', + }); + } + } + return; + } + + if (!String(formData.heureDebut ?? '').trim() || !String(formData.heureFin ?? '').trim()) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Le créneau horaire est obligatoire pour une demande de salle', + }); + } + + if (String(formData.heureFin) <= String(formData.heureDebut)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'L’heure de fin doit être postérieure à l’heure de début', + }); + } + + const pricing = computeSalleWorkflowPricing(rawFormData); + if (pricing?.unsupportedSalles?.length) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Aucune tarification automatique n'est disponible pour : ${pricing.unsupportedSalles.map((item) => item.salleNom).join(', ')}`, + }); + } +} + +function sanitizeDuplicatedRequestFormData(rawFormData?: string | null) { + if (!rawFormData) return undefined; + + try { + const formData = JSON.parse(rawFormData); + if (formData && typeof formData === 'object') { + delete formData.cadreDSU; + delete formData.notificationTrace; + delete formData.dateReception; + delete formData.dateDecision; + delete formData.responsableDSU; + delete formData.observationsDSU; + delete formData.commentaireAdmin; + if (formData.formulaireType === 'demande_materiel_evenementiel') { + delete formData.datePriseEnCharge; + } + } + return JSON.stringify(formData); + } catch { + return rawFormData; + } +} + +async function syncAssociationBackToDirectoryEntry(association: Awaited>) { + if (!association?.sourceDirectoryEntryId) { + return; + } + + const existingEntry = await db.getAssociationDirectoryEntryById(association.sourceDirectoryEntryId); + const parsedGovernance = association.gouvernance + ? parseAssociationGovernance(association.gouvernance) + : buildGovernanceFromLegacyRepresentative(association.nomRepresentant, association.fonctionRepresentant); + const representantLegal = parsedGovernance.representantLegal; + const representantLegalDisplayName = buildDisplayName(representantLegal) || association.nomRepresentant || null; + + await db.updateAssociationDirectoryEntry(association.sourceDirectoryEntryId, { + nomAssociation: association.nomAssociation, + siret: association.siret || existingEntry?.siret || null, + rna: association.rna || existingEntry?.rna || null, + adresse: association.adresse || existingEntry?.adresse || null, + codePostal: association.codePostal || existingEntry?.codePostal || null, + ville: association.ville || existingEntry?.ville || null, + telephone: association.telephone || existingEntry?.telephone || null, + emailOfficiel: association.emailContact || existingEntry?.emailOfficiel || null, + emailOfficielNormalise: association.emailContact + ? normalizeEmail(association.emailContact) + : existingEntry?.emailOfficielNormalise || null, + siteWeb: association.siteWeb || existingEntry?.siteWeb || null, + facebookUrl: association.facebookUrl || existingEntry?.facebookUrl || null, + instagramUrl: association.instagramUrl || existingEntry?.instagramUrl || null, + thematique: association.thematique || existingEntry?.thematique || null, + dateCreation: association.dateCreation || existingEntry?.dateCreation || null, + objetAssociation: association.objetAssociation || existingEntry?.objetAssociation || null, + statutJuridique: association.statutJuridique || existingEntry?.statutJuridique || undefined, + nomRepresentant: representantLegalDisplayName || existingEntry?.nomRepresentant || null, + fonctionRepresentant: representantLegal?.fonction || association.fonctionRepresentant || existingEntry?.fonctionRepresentant || null, + gouvernance: association.gouvernance || existingEntry?.gouvernance || null, + updatedAt: new Date(), + }); +} + +function buildPublicAssociationDirectoryPayload(details: NonNullable>>) { + const coordinates = getPublicMapCoordinates(details.entry); + if (!coordinates) { + return null; + } + + return { + id: details.entry.id, + nomAssociation: details.entry.nomAssociation, + ville: details.entry.ville || details.association?.ville, + siret: details.entry.siret || details.association?.siret, + rna: details.entry.rna || details.association?.rna, + thematiques: parseAssociationThematics(details.entry.thematique || details.association?.thematique), + objetAssociation: details.entry.objetAssociation || details.association?.objetAssociation, + siteWeb: details.entry.siteWeb || details.association?.siteWeb, + facebookUrl: details.entry.facebookUrl || details.association?.facebookUrl, + instagramUrl: details.entry.instagramUrl || details.association?.instagramUrl, + registered: Boolean(details.association), + latitude: coordinates.latitude, + longitude: coordinates.longitude, + geoPrecision: coordinates.precision, + publicUrl: `/associations/${details.entry.id}`, + }; +} + +function parseRecipients(input?: string[]) { + return (input || []) + .map(value => value.trim().toLowerCase()) + .filter(Boolean); +} + +function mergeRecipients(...recipientSets: Array) { + return Array.from( + new Set( + recipientSets + .flatMap(values => values || []) + .map(value => value.trim().toLowerCase()) + .filter(Boolean) + ) + ); +} + +function parseStoredRecipientEmails(value?: string | null): string[] { + if (!value) return []; + + try { + const parsed = JSON.parse(value); + if (!Array.isArray(parsed)) return []; + return parsed + .map((entry) => String(entry || "").trim().toLowerCase()) + .filter(Boolean); + } catch { + return []; + } +} + +function parseMaterialFinancialDecision(rawFormData?: string | null) { + const formData = parseRequestFormData(rawFormData); + const decision = formData?.cadreDSU?.materielEvent?.financialDecision; + if (!decision || typeof decision !== "object") return null; + + return { + financialMode: (["gratuite", "gratuite_avec_caution", "location_payante"].includes(decision.financialMode) + ? decision.financialMode + : "gratuite") as MaterialContractFinancialMode, + depositRequired: Boolean(decision.depositRequired), + depositAmountCents: Number.isFinite(Number(decision.depositAmountCents)) ? Math.max(0, Number(decision.depositAmountCents)) : 0, + rentalAmountCents: Number.isFinite(Number(decision.rentalAmountCents)) ? Math.max(0, Number(decision.rentalAmountCents)) : 0, + pricingNotes: decision.pricingNotes ? String(decision.pricingNotes) : "", + contractStatus: (["a_generer", "generee", "signee", "refusee", "annulee"].includes(decision.contractStatus) + ? decision.contractStatus + : "a_generer") as MaterialContractStatus, + contractPdfUrl: decision.contractPdfUrl ? String(decision.contractPdfUrl) : "", + contractPdfName: decision.contractPdfName ? String(decision.contractPdfName) : "", + contractGeneratedAt: decision.contractGeneratedAt ? String(decision.contractGeneratedAt) : "", + contractValidatedByUserId: Number.isFinite(Number(decision.contractValidatedByUserId)) + ? Number(decision.contractValidatedByUserId) + : null, + }; +} + +function mergeMaterialFinancialDecision(input: { + rawFormData?: string | null; + decision: { + financialMode: MaterialContractFinancialMode; + depositRequired: boolean; + depositAmountCents: number; + rentalAmountCents: number; + pricingNotes?: string | null; + contractStatus: MaterialContractStatus; + contractPdfUrl?: string | null; + contractPdfName?: string | null; + contractGeneratedAt?: string | Date | null; + contractValidatedByUserId?: number | null; + }; +}) { + const formData = parseRequestFormData(input.rawFormData); + formData.cadreDSU = formData.cadreDSU && typeof formData.cadreDSU === "object" ? formData.cadreDSU : {}; + formData.cadreDSU.materielEvent = formData.cadreDSU.materielEvent && typeof formData.cadreDSU.materielEvent === "object" + ? formData.cadreDSU.materielEvent + : {}; + formData.cadreDSU.materielEvent.financialDecision = { + financialMode: input.decision.financialMode, + depositRequired: input.decision.depositRequired, + depositAmountCents: input.decision.depositAmountCents, + rentalAmountCents: input.decision.rentalAmountCents, + pricingNotes: input.decision.pricingNotes || "", + contractStatus: input.decision.contractStatus, + contractPdfUrl: input.decision.contractPdfUrl || "", + contractPdfName: input.decision.contractPdfName || "", + contractGeneratedAt: input.decision.contractGeneratedAt + ? new Date(input.decision.contractGeneratedAt).toISOString() + : "", + contractValidatedByUserId: input.decision.contractValidatedByUserId ?? null, + }; + return JSON.stringify(formData); +} + +function buildSalleDatesLabel(formData: any) { + if (formData?.dateReservation === formData?.dateFinReservation || !formData?.dateFinReservation) { + return formatDateFr(formData?.dateReservation); + } + return `${formatDateFr(formData?.dateReservation)} au ${formatDateFr(formData?.dateFinReservation)}`; +} + +function getSallePaymentTracking(rawFormData?: string | null) { + const workflow = getSalleWorkflowData(rawFormData); + const formData = parseRequestFormData(rawFormData); + const paymentStatus = ["en_attente_paiement", "paiement_partiel", "paiement_recu", "paiement_valide"].includes(String(workflow.paymentStatus)) + ? String(workflow.paymentStatus) + : ""; + + return { + paymentStatus: paymentStatus as "" | "en_attente_paiement" | "paiement_partiel" | "paiement_recu" | "paiement_valide", + paymentDueDate: normalizeDateString(workflow.paymentDueDate || formData?.dateReservation || ""), + amountReceivedCents: Number.isFinite(Number(workflow.amountReceivedCents)) ? Math.max(0, Number(workflow.amountReceivedCents)) : 0, + paymentReceivedAt: typeof workflow.paymentReceivedAt === "string" ? workflow.paymentReceivedAt : "", + paymentValidatedAt: typeof workflow.paymentValidatedAt === "string" ? workflow.paymentValidatedAt : "", + paymentReference: typeof workflow.paymentReference === "string" ? workflow.paymentReference : "", + paymentNotes: typeof workflow.paymentNotes === "string" ? workflow.paymentNotes : "", + }; +} + +function ensureSallePaymentTracking(input: { + rawFormData?: string | null; + billed: boolean; + totalAmountCents: number; +}) { + if (!input.billed) { + return input.rawFormData || ""; + } + + const current = getSallePaymentTracking(input.rawFormData); + return mergeSalleWorkflowData(input.rawFormData, { + paymentStatus: current.paymentStatus || "en_attente_paiement", + paymentDueDate: current.paymentDueDate || normalizeDateString(parseRequestFormData(input.rawFormData)?.dateReservation || ""), + amountReceivedCents: current.amountReceivedCents, + paymentReceivedAt: current.paymentReceivedAt, + paymentValidatedAt: current.paymentValidatedAt, + paymentReference: current.paymentReference, + paymentNotes: current.paymentNotes, + }); +} + +function sanitizeSalleBillingSettings(value: unknown): SalleBillingSettings { + const record = value && typeof value === "object" ? value as Record : {}; + return { + ribDocumentUrl: typeof record.ribDocumentUrl === "string" ? record.ribDocumentUrl : "", + ribDocumentName: typeof record.ribDocumentName === "string" ? record.ribDocumentName : "", + ribMimeType: typeof record.ribMimeType === "string" ? record.ribMimeType : "", + ribUploadedAt: typeof record.ribUploadedAt === "string" ? record.ribUploadedAt : "", + }; +} + +async function getSalleBillingSettings() { + const storedValue = await db.getPortalSetting(SALLE_BILLING_SETTING_KEY); + if (!storedValue) { + return sanitizeSalleBillingSettings(null); + } + try { + return sanitizeSalleBillingSettings(JSON.parse(storedValue)); + } catch { + return sanitizeSalleBillingSettings(null); + } +} + +async function getHelloAssoSettings() { + const storedValue = await db.getPortalSetting(HELLOASSO_SETTINGS_KEY); + if (!storedValue) return sanitizeHelloAssoSettings(); + try { + return sanitizeHelloAssoSettings(JSON.parse(storedValue)); + } catch { + return sanitizeHelloAssoSettings(); + } +} + +async function getAssociationMapSettings() { + const storedValue = await db.getPortalSetting(ASSOCIATION_MAP_SETTING_KEY); + if (!storedValue) return sanitizeAssociationMapSettings(); + try { + return sanitizeAssociationMapSettings(JSON.parse(storedValue)); + } catch { + return sanitizeAssociationMapSettings(); + } +} + +async function saveHelloAssoSettings(nextValue: { + enabled: boolean; + clientId: string; + clientSecret?: string; +}) { + const current = await getHelloAssoSettings(); + const normalized = sanitizeHelloAssoSettings({ + enabled: nextValue.enabled, + clientId: nextValue.clientId, + clientSecret: nextValue.clientSecret?.trim() ? nextValue.clientSecret : current.clientSecret, + }); + + await db.setPortalSetting( + HELLOASSO_SETTINGS_KEY, + JSON.stringify(normalized), + "Synchronisation contrôlée du bordereau des associations avec HelloAsso" + ); + + return normalized; +} + +async function readStoredUploadBufferFromUrl(url: string) { + const normalized = String(url || "").trim(); + if (!normalized.startsWith("/uploads/")) return null; + const relativePath = normalized.replace(/^\/uploads\//, ""); + const absolutePath = path.resolve(process.cwd(), "uploads", relativePath); + try { + return await fs.readFile(absolutePath); + } catch { + return null; + } +} + +function serializeOperationalRecapService( + service: + | Awaited> + | Awaited>[number] + | null + | undefined +) { + if (!service) return null; + + return { + ...service, + usage: service.usage === "controle" ? "controle" : "terrain", + recipientEmails: parseStoredRecipientEmails(service.recipientEmails), + }; +} + +function normalizeEmail(value: string) { + return value.trim().toLowerCase(); +} + +function normalizeSiretValue(value?: string | null) { + const digits = String(value || "").replace(/\D/g, ""); + return digits || null; +} + +function normalizeRnaValue(value?: string | null) { + const normalized = String(value || "").replace(/\s+/g, "").trim().toUpperCase(); + return normalized || null; +} + +async function ensureAssociationLinkedToDirectoryEntry(userId: number, directoryEntryId: number) { + const [existingProfile, directoryEntry, alreadyLinkedProfile] = await Promise.all([ + db.getAssociationByUserId(userId), + db.getAssociationDirectoryEntryById(directoryEntryId), + db.getAssociationBySourceDirectoryEntryId(directoryEntryId), + ]); + + if (!directoryEntry) { + throw new Error("Association du bordereau introuvable"); + } + + if (alreadyLinkedProfile && alreadyLinkedProfile.userId !== userId) { + throw new Error("Cette fiche du bordereau est déjà rattachée à une autre association portail"); + } + + if (existingProfile) { + if (existingProfile.sourceDirectoryEntryId !== directoryEntryId) { + await db.updateAssociation(existingProfile.id, { sourceDirectoryEntryId: directoryEntryId }); + } + return existingProfile.id; + } + + return db.createAssociation(createAssociationProfileFromDirectoryEntry(userId, directoryEntry)); +} + +const socialUrlSchema = z.string().optional().or(z.literal('')); + +async function queueAssociationDirectoryReview(input: { + userId?: number | null; + sourceType: "portal_signup" | "helloasso"; + sourceLabel?: string | null; + nomAssociation: string; + email?: string | null; + siret?: string | null; + rna?: string | null; + ville?: string | null; + telephone?: string | null; + nomRepresentant?: string | null; + matchResult: AssociationDirectoryMatchResult; +}) { + const payload = JSON.stringify({ + reason: input.matchResult.reason, + proposedTelephone: input.telephone || null, + proposedNomRepresentant: input.nomRepresentant || null, + candidates: input.matchResult.candidates.map((candidate) => ({ + id: candidate.id, + nomAssociation: candidate.nomAssociation, + ville: candidate.ville, + siret: candidate.siret, + rna: candidate.rna, + emailOfficiel: candidate.emailOfficiel, + telephone: candidate.telephone, + nomRepresentant: candidate.nomRepresentant, + })), + }); + + const reviewPayload = { + sourceType: input.sourceType, + sourceLabel: input.sourceLabel || null, + userId: input.userId || null, + proposedNomAssociation: input.nomAssociation, + proposedEmail: input.email || null, + proposedEmailNormalise: input.email ? normalizeEmail(input.email) : null, + proposedSiret: normalizeSiretValue(input.siret), + proposedRna: normalizeRnaValue(input.rna), + proposedVille: input.ville || null, + matchStatus: input.matchResult.status, + payload, + status: "pending" as const, + resolutionNote: null, + }; + + const existing = input.userId ? await db.getPendingAssociationDirectoryReviewByUserId(input.userId) : undefined; + if (existing) { + await db.updateAssociationDirectoryReview(existing.id, reviewPayload); + return existing.id; + } + + return db.createAssociationDirectoryReview(reviewPayload); +} + +async function resolvePendingAssociationDirectoryReviewAfterAutoLink(input: { + userId: number; + directoryEntryId: number; + sourceLabel?: string | null; +}) { + const pendingReview = await db.getPendingAssociationDirectoryReviewByUserId(input.userId); + if (!pendingReview) { + return; + } + + await db.updateAssociationDirectoryReview(pendingReview.id, { + status: "linked", + resolvedDirectoryEntryId: input.directoryEntryId, + resolutionNote: + input.sourceLabel + ? `Rapprochement validé automatiquement après ${input.sourceLabel}.` + : "Rapprochement validé automatiquement après mise à jour du profil portail.", + }); +} + +const associationDirectoryManualInputSchema = z.object({ + reviewId: z.number().optional(), + nomAssociation: z.string().min(1, "Le nom de l'association est obligatoire"), + emailOfficiel: z.string().email("Adresse email invalide").optional().or(z.literal("")), + siret: z.string().optional(), + rna: z.string().optional(), + thematiques: z.array(z.enum(associationThematicValues)).optional(), + adresse: z.string().optional(), + codePostal: z.string().optional(), + ville: z.string().optional(), + telephone: z.string().optional(), + siteWeb: z.string().url().optional().or(z.literal("")), + facebookUrl: socialUrlSchema, + instagramUrl: socialUrlSchema, + dateCreation: z.string().optional(), + objetAssociation: z.string().optional(), + statutJuridique: z.enum(['association_loi_1901', 'association_reconnue_utilite_publique', 'fondation', 'autre']).optional(), + nomRepresentant: z.string().optional(), + fonctionRepresentant: z.string().optional(), + isActive: z.boolean().optional(), +}); + +function splitRepresentativeName(fullName: string | null | undefined) { + const cleaned = String(fullName || "").trim().replace(/\s+/g, " "); + if (!cleaned) { + return { prenom: "", nom: "" }; + } + + const parts = cleaned.split(" "); + if (parts.length === 1) { + return { prenom: "", nom: parts[0] }; + } + + return { + prenom: parts.slice(0, -1).join(" "), + nom: parts.at(-1) || "", + }; +} + +function buildGovernanceFromLegacyRepresentative( + nomRepresentant: string | null | undefined, + fonctionRepresentant: string | null | undefined +): AssociationGovernance { + const parts = splitRepresentativeName(nomRepresentant); + const hasRepresentative = Boolean(parts.nom || parts.prenom || fonctionRepresentant); + + return { + representantLegal: hasRepresentative + ? { + nom: parts.nom, + prenom: parts.prenom, + email: "", + telephone: "", + fonction: normalizeLegalRepresentativeRole(fonctionRepresentant), + } + : null, + membres: [], + }; +} + +function validateSocialUrl(field: "facebookUrl" | "instagramUrl", value?: string) { + const normalized = normalizeSocialUrl(value); + if (!normalized) return; + + const isValid = field === "facebookUrl" ? isValidFacebookUrl(normalized) : isValidInstagramUrl(normalized); + if (!isValid) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + field === "facebookUrl" + ? "Le lien Facebook doit être une URL HTTPS valide vers facebook.com" + : "Le lien Instagram doit être une URL HTTPS valide vers instagram.com", + }); + } +} + +function buildAppBaseUrl(req: { protocol?: string; headers?: Record; get?: (name: string) => string | undefined }) { + const configuredBaseUrl = process.env.APP_BASE_URL?.trim(); + if (configuredBaseUrl) { + return configuredBaseUrl.replace(/\/$/, ""); + } + + const forwardedProto = typeof req.headers?.["x-forwarded-proto"] === "string" + ? req.headers["x-forwarded-proto"] + : undefined; + const forwardedHost = typeof req.headers?.["x-forwarded-host"] === "string" + ? req.headers["x-forwarded-host"] + : undefined; + const host = forwardedHost || req.get?.("host") || "localhost:3000"; + const protocol = forwardedProto || req.protocol || "http"; + return `${protocol}://${host}`.replace(/\/$/, ""); +} + +function buildAssociationInvitationLink(req: Parameters[0], token: string) { + return `${buildAppBaseUrl(req)}/invite/${token}`; +} + +function formatDateFr(date: Date | string | null | undefined) { + if (!date) return ""; + try { + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(date); + } +} + +function parseRequestFormData(rawFormData?: string | null) { + if (!rawFormData) return {}; + try { + return JSON.parse(rawFormData); + } catch { + return {}; + } +} + +function normalizeDateString(value?: string | null) { + if (!value) return ""; + const normalized = String(value).trim(); + return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : ""; +} + +function dateRangesOverlap(startA?: string | null, endA?: string | null, startB?: string | null, endB?: string | null) { + const aStart = normalizeDateString(startA); + const aEnd = normalizeDateString(endA || startA); + const bStart = normalizeDateString(startB); + const bEnd = normalizeDateString(endB || startB); + if (!aStart || !aEnd || !bStart || !bEnd) return false; + return aStart <= bEnd && bStart <= aEnd; +} + +function addDays(date: Date, days: number) { + const next = new Date(date); + next.setDate(next.getDate() + days); + return next; +} + +function startOfDay(date: Date) { + const next = new Date(date); + next.setHours(0, 0, 0, 0); + return next; +} + +function toIsoDay(date: Date) { + return date.toISOString().slice(0, 10); +} + +function daysBetweenInclusive(startIso?: string | null, endIso?: string | null) { + const start = normalizeDateString(startIso); + const end = normalizeDateString(endIso || startIso); + if (!start || !end) return 0; + const startDate = new Date(`${start}T00:00:00`); + const endDate = new Date(`${end}T00:00:00`); + const diff = endDate.getTime() - startDate.getTime(); + if (Number.isNaN(diff) || diff < 0) return 0; + return Math.floor(diff / 86_400_000) + 1; +} + +function getOverlapDays(startA?: string | null, endA?: string | null, startB?: string | null, endB?: string | null) { + if (!dateRangesOverlap(startA, endA, startB, endB)) return 0; + const left = new Date(`${normalizeDateString(startA)!}T00:00:00`); + const right = new Date(`${normalizeDateString(endA || startA)!}T00:00:00`); + const periodStart = new Date(`${normalizeDateString(startB)!}T00:00:00`); + const periodEnd = new Date(`${normalizeDateString(endB || startB)!}T00:00:00`); + const overlapStart = left > periodStart ? left : periodStart; + const overlapEnd = right < periodEnd ? right : periodEnd; + const diff = overlapEnd.getTime() - overlapStart.getTime(); + return diff < 0 ? 0 : Math.floor(diff / 86_400_000) + 1; +} + +function buildAnalyticsRange(period: AnalyticsPeriod) { + const today = startOfDay(new Date()); + const end = today; + let start = today; + let label = "7 derniers jours"; + let comparisonLabel = "période précédente"; + let bucketMode: "day" | "week" | "month" = "day"; + + if (period === "7d") { + start = addDays(today, -6); + label = "7 derniers jours"; + comparisonLabel = "7 jours précédents"; + bucketMode = "day"; + } else if (period === "month") { + start = new Date(today.getFullYear(), today.getMonth(), 1); + label = "Mois en cours"; + comparisonLabel = "Mois précédent"; + bucketMode = "week"; + } else if (period === "quarter") { + const quarterStartMonth = Math.floor(today.getMonth() / 3) * 3; + start = new Date(today.getFullYear(), quarterStartMonth, 1); + label = "Trimestre en cours"; + comparisonLabel = "Trimestre précédent"; + bucketMode = "month"; + } else if (period === "semester") { + const semesterStartMonth = today.getMonth() < 6 ? 0 : 6; + start = new Date(today.getFullYear(), semesterStartMonth, 1); + label = "Semestre en cours"; + comparisonLabel = "Semestre précédent"; + bucketMode = "month"; + } else { + start = new Date(today.getFullYear(), 0, 1); + label = "Année en cours"; + comparisonLabel = "Année précédente"; + bucketMode = "month"; + } + + const spanDays = daysBetweenInclusive(toIsoDay(start), toIsoDay(end)); + const previousEnd = addDays(start, -1); + const previousStart = addDays(previousEnd, -(spanDays - 1)); + + return { + start, + end, + startIso: toIsoDay(start), + endIso: toIsoDay(end), + previousStartIso: toIsoDay(previousStart), + previousEndIso: toIsoDay(previousEnd), + label, + comparisonLabel, + bucketMode, + spanDays, + }; +} + +function buildAnalyticsRangeFromReference(period: AnalyticsPeriod, referenceDate: Date, mode: "current" | "completed" = "current") { + if (mode === "current") { + return buildAnalyticsRange(period); + } + + const today = startOfDay(referenceDate); + if (period === "7d") { + const end = addDays(today, -1); + const start = addDays(end, -6); + return { + start, + end, + startIso: toIsoDay(start), + endIso: toIsoDay(end), + previousStartIso: toIsoDay(addDays(start, -7)), + previousEndIso: toIsoDay(addDays(end, -7)), + label: "7 jours précédents", + comparisonLabel: "7 jours antérieurs", + bucketMode: "day" as const, + spanDays: 7, + }; + } + + if (period === "month") { + const end = new Date(today.getFullYear(), today.getMonth(), 0); + const start = new Date(end.getFullYear(), end.getMonth(), 1); + const previousEnd = new Date(start.getFullYear(), start.getMonth(), 0); + const previousStart = new Date(previousEnd.getFullYear(), previousEnd.getMonth(), 1); + return { + start, + end, + startIso: toIsoDay(start), + endIso: toIsoDay(end), + previousStartIso: toIsoDay(previousStart), + previousEndIso: toIsoDay(previousEnd), + label: "Mois précédent", + comparisonLabel: "Mois antérieur", + bucketMode: "week" as const, + spanDays: daysBetweenInclusive(toIsoDay(start), toIsoDay(end)), + }; + } + + if (period === "quarter") { + const currentQuarterStartMonth = Math.floor(today.getMonth() / 3) * 3; + const currentQuarterStart = new Date(today.getFullYear(), currentQuarterStartMonth, 1); + const end = new Date(currentQuarterStart.getFullYear(), currentQuarterStart.getMonth(), 0); + const start = new Date(end.getFullYear(), end.getMonth() - 2, 1); + const previousEnd = new Date(start.getFullYear(), start.getMonth(), 0); + const previousStart = new Date(previousEnd.getFullYear(), previousEnd.getMonth() - 2, 1); + return { + start, + end, + startIso: toIsoDay(start), + endIso: toIsoDay(end), + previousStartIso: toIsoDay(previousStart), + previousEndIso: toIsoDay(previousEnd), + label: "Trimestre précédent", + comparisonLabel: "Trimestre antérieur", + bucketMode: "month" as const, + spanDays: daysBetweenInclusive(toIsoDay(start), toIsoDay(end)), + }; + } + + if (period === "semester") { + const currentSemesterStartMonth = today.getMonth() < 6 ? 0 : 6; + const currentSemesterStart = new Date(today.getFullYear(), currentSemesterStartMonth, 1); + const end = new Date(currentSemesterStart.getFullYear(), currentSemesterStart.getMonth(), 0); + const start = new Date(end.getFullYear(), end.getMonth() - 5, 1); + const previousEnd = new Date(start.getFullYear(), start.getMonth(), 0); + const previousStart = new Date(previousEnd.getFullYear(), previousEnd.getMonth() - 5, 1); + return { + start, + end, + startIso: toIsoDay(start), + endIso: toIsoDay(end), + previousStartIso: toIsoDay(previousStart), + previousEndIso: toIsoDay(previousEnd), + label: "Semestre précédent", + comparisonLabel: "Semestre antérieur", + bucketMode: "month" as const, + spanDays: daysBetweenInclusive(toIsoDay(start), toIsoDay(end)), + }; + } + + const end = new Date(today.getFullYear(), 0, 0); + const start = new Date(end.getFullYear(), 0, 1); + const previousStart = new Date(start.getFullYear() - 1, 0, 1); + const previousEnd = new Date(start.getFullYear() - 1, 11, 31); + return { + start, + end, + startIso: toIsoDay(start), + endIso: toIsoDay(end), + previousStartIso: toIsoDay(previousStart), + previousEndIso: toIsoDay(previousEnd), + label: "Année précédente", + comparisonLabel: "Année antérieure", + bucketMode: "month" as const, + spanDays: daysBetweenInclusive(toIsoDay(start), toIsoDay(end)), + }; +} + +async function getStatsAutomationSettings() { + const raw = await db.getPortalSetting(STATS_AUTOMATION_SETTING_KEY); + if (!raw) return sanitizeStatsAutomationSettings(); + try { + return sanitizeStatsAutomationSettings(JSON.parse(raw)); + } catch { + return sanitizeStatsAutomationSettings(); + } +} + +async function saveStatsAutomationSettings(settings: StatsAutomationSettings) { + await db.setPortalSetting( + STATS_AUTOMATION_SETTING_KEY, + JSON.stringify(settings), + "Automatisation des rapports statistiques" + ); + return settings; +} + +async function getStatsReportHistory() { + const raw = await db.getPortalSetting(STATS_REPORT_HISTORY_SETTING_KEY); + if (!raw) return [] as StatsReportHistoryEntry[]; + try { + return sanitizeStatsReportHistory(JSON.parse(raw)); + } catch { + return []; + } +} + +async function saveStatsReportHistory(history: StatsReportHistoryEntry[]) { + await db.setPortalSetting( + STATS_REPORT_HISTORY_SETTING_KEY, + JSON.stringify(history.slice(0, 30)), + "Historique des rapports statistiques générés automatiquement" + ); +} + +function getCayenneDateParts(date: Date) { + const formatter = new Intl.DateTimeFormat("en-CA", { + timeZone: "America/Cayenne", + year: "numeric", + month: "2-digit", + day: "2-digit", + weekday: "short", + }); + const parts = formatter.formatToParts(date); + const get = (type: string) => parts.find((part) => part.type === type)?.value || ""; + const year = Number(get("year")); + const month = Number(get("month")); + const day = Number(get("day")); + const weekdayLabel = get("weekday"); + const weekdayIndexMap: Record = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 0 }; + return { + year, + month, + day, + isoDate: `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`, + weekday: weekdayIndexMap[weekdayLabel] ?? 0, + }; +} + +function getStatsCycleForToday(period: StatsReportHistoryEntry["period"], now: Date) { + const parts = getCayenneDateParts(now); + if (period === "weekly") { + return { + due: parts.weekday === 1, + cycleKey: `weekly-${parts.isoDate}`, + analyticsPeriod: "7d" as const, + }; + } + if (period === "month") { + return { + due: parts.day === 1, + cycleKey: `month-${parts.year}-${String(parts.month - 1 || 12).padStart(2, "0")}`, + analyticsPeriod: "month" as const, + }; + } + if (period === "quarter") { + return { + due: parts.day === 1 && [1, 4, 7, 10].includes(parts.month), + cycleKey: `quarter-${parts.year}-${Math.max(1, Math.ceil(((parts.month - 1) || 12) / 3))}`, + analyticsPeriod: "quarter" as const, + }; + } + if (period === "semester") { + return { + due: parts.day === 1 && [1, 7].includes(parts.month), + cycleKey: `semester-${parts.year}-${parts.month === 1 ? "S2-prev" : "S1"}`, + analyticsPeriod: "semester" as const, + }; + } + return { + due: parts.day === 1 && parts.month === 1, + cycleKey: `year-${parts.year - 1}`, + analyticsPeriod: "year" as const, + }; +} + +function formatAnalyticsBucketLabel(start: Date, end: Date, mode: "day" | "week" | "month") { + if (mode === "day") { + return start.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit" }); + } + if (mode === "month") { + return start.toLocaleDateString("fr-FR", { month: "short", year: "2-digit" }); + } + return `${start.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit" })} - ${end.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit" })}`; +} + +function buildAnalyticsBuckets(range: ReturnType) { + const buckets: Array<{ key: string; label: string; startIso: string; endIso: string }> = []; + + if (range.bucketMode === "day") { + for (let cursor = new Date(range.start); cursor <= range.end; cursor = addDays(cursor, 1)) { + const startIso = toIsoDay(cursor); + buckets.push({ + key: startIso, + label: formatAnalyticsBucketLabel(cursor, cursor, "day"), + startIso, + endIso: startIso, + }); + } + return buckets; + } + + if (range.bucketMode === "week") { + for (let cursor = new Date(range.start); cursor <= range.end; cursor = addDays(cursor, 7)) { + const bucketStart = new Date(cursor); + const bucketEnd = addDays(bucketStart, 6); + const effectiveEnd = bucketEnd > range.end ? range.end : bucketEnd; + buckets.push({ + key: `${toIsoDay(bucketStart)}-${toIsoDay(effectiveEnd)}`, + label: formatAnalyticsBucketLabel(bucketStart, effectiveEnd, "week"), + startIso: toIsoDay(bucketStart), + endIso: toIsoDay(effectiveEnd), + }); + } + return buckets; + } + + for (let cursor = new Date(range.start.getFullYear(), range.start.getMonth(), 1); cursor <= range.end; cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1)) { + const bucketStart = new Date(cursor); + const bucketEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0); + const effectiveStart = bucketStart < range.start ? range.start : bucketStart; + const effectiveEnd = bucketEnd > range.end ? range.end : bucketEnd; + buckets.push({ + key: `${toIsoDay(effectiveStart)}-${toIsoDay(effectiveEnd)}`, + label: formatAnalyticsBucketLabel(effectiveStart, effectiveEnd, "month"), + startIso: toIsoDay(effectiveStart), + endIso: toIsoDay(effectiveEnd), + }); + } + + return buckets; +} + +export async function computeReservationAnalytics(period: AnalyticsPeriod, mode: "current" | "completed" = "current", referenceDate: Date = new Date()) { + const range = buildAnalyticsRangeFromReference(period, referenceDate, mode); + const buckets = buildAnalyticsBuckets(range); + const allRequests = await db.getAllRequests(); + const associations = await db.getAllAssociations(); + const associationMap = new Map(associations.map((association) => [association.id, association])); + + const roomUsage = new Map( + salleAnalyticsCatalog.map((room) => [room.id, { roomId: room.id, roomName: room.name, bookedDays: 0 }]) + ); + const materialUsage = new Map( + materialEventItems.map((item) => [item.key, { key: item.key, label: getMaterialEventLabel(item.key), quantity: 0, requests: 0 }]) + ); + const categoryUsage = new Map(); + const associationUsage = new Map(); + const timeline = buckets.map((bucket) => ({ + label: bucket.label, + salle: 0, + materiel: 0, + total: 0, + })); + const history: Array<{ + id: number; + title: string; + type: "demande_salle" | "demande_materiel_evenementiel"; + associationName: string; + status: string; + startDate: string; + endDate: string; + resources: string; + createdAt: Date; + }> = []; + + let salleReservations = 0; + let materialReservations = 0; + let totalMaterialUnits = 0; + let previousSalleReservations = 0; + let previousMaterialReservations = 0; + const currentAssociationIds = new Set(); + const previousAssociationIds = new Set(); + + const includeInUsage = (status: string) => !["brouillon", "refusee", "annulee"].includes(status); + + for (const request of allRequests) { + if (request.type !== "demande_salle" && request.type !== "demande_materiel_evenementiel") continue; + if (!includeInUsage(request.status)) continue; + + const association = associationMap.get(request.associationId); + const thematicValues = parseAssociationThematics(association?.thematique).map((value) => getAssociationThematicLabel(value)); + const categoryLabels = thematicValues.length > 0 ? thematicValues : ["Non renseignée"]; + + if (request.type === "demande_salle") { + const formData = parseRequestFormData(request.formData); + const startDate = normalizeDateString(formData.dateReservation); + const endDate = normalizeDateString(formData.dateFinReservation || formData.dateReservation || startDate); + if (!startDate || !endDate) continue; + + const inCurrentRange = dateRangesOverlap(startDate, endDate, range.startIso, range.endIso); + const inPreviousRange = dateRangesOverlap(startDate, endDate, range.previousStartIso, range.previousEndIso); + if (!inCurrentRange && !inPreviousRange) continue; + + if (inPreviousRange) { + previousSalleReservations += 1; + previousAssociationIds.add(request.associationId); + } + + if (!inCurrentRange) continue; + + salleReservations += 1; + currentAssociationIds.add(request.associationId); + categoryLabels.forEach((label) => { + const current = categoryUsage.get(label) || { key: label, label, count: 0 }; + current.count += 1; + categoryUsage.set(label, current); + }); + + const associationEntry = associationUsage.get(request.associationId) || { + associationId: request.associationId, + associationName: association?.nomAssociation || formData.nomAssociation || `Association #${request.associationId}`, + totalReservations: 0, + salleReservations: 0, + materialReservations: 0, + lastReservationAt: endDate, + thematics: categoryLabels, + }; + associationEntry.totalReservations += 1; + associationEntry.salleReservations += 1; + associationEntry.lastReservationAt = associationEntry.lastReservationAt > endDate ? associationEntry.lastReservationAt : endDate; + associationEntry.thematics = categoryLabels; + associationUsage.set(request.associationId, associationEntry); + + const salleIds = deriveSalleIdsForAnalytics(formData); + salleIds.forEach((salleId) => { + const room = roomUsage.get(salleId); + if (!room) return; + room.bookedDays += getOverlapDays(startDate, endDate, range.startIso, range.endIso); + }); + + buckets.forEach((bucket, index) => { + if (!dateRangesOverlap(startDate, endDate, bucket.startIso, bucket.endIso)) return; + timeline[index].salle += 1; + timeline[index].total += 1; + }); + + history.push({ + id: request.id, + title: request.titre, + type: "demande_salle", + associationName: association?.nomAssociation || formData.nomAssociation || `Association #${request.associationId}`, + status: request.status, + startDate, + endDate, + resources: Array.isArray(formData.sallesSelectionnees) ? formData.sallesSelectionnees.join(", ") : "", + createdAt: request.createdAt, + }); + continue; + } + + const materialEvent = getMaterialRequestEventData(request, association || null); + const startDate = materialEvent.useStartDate; + const endDate = materialEvent.useEndDate; + if (!startDate || !endDate) continue; + + const inCurrentRange = dateRangesOverlap(startDate, endDate, range.startIso, range.endIso); + const inPreviousRange = dateRangesOverlap(startDate, endDate, range.previousStartIso, range.previousEndIso); + if (!inCurrentRange && !inPreviousRange) continue; + + if (inPreviousRange) { + previousMaterialReservations += 1; + previousAssociationIds.add(request.associationId); + } + + if (!inCurrentRange) continue; + + materialReservations += 1; + currentAssociationIds.add(request.associationId); + categoryLabels.forEach((label) => { + const current = categoryUsage.get(label) || { key: label, label, count: 0 }; + current.count += 1; + categoryUsage.set(label, current); + }); + + const associationEntry = associationUsage.get(request.associationId) || { + associationId: request.associationId, + associationName: materialEvent.associationName, + totalReservations: 0, + salleReservations: 0, + materialReservations: 0, + lastReservationAt: endDate, + thematics: categoryLabels, + }; + associationEntry.totalReservations += 1; + associationEntry.materialReservations += 1; + associationEntry.lastReservationAt = associationEntry.lastReservationAt > endDate ? associationEntry.lastReservationAt : endDate; + associationEntry.thematics = categoryLabels; + associationUsage.set(request.associationId, associationEntry); + + materialEvent.requestedItems.forEach((item) => { + const current = materialUsage.get(item.key); + if (!current) return; + current.quantity += item.quantity; + if (item.quantity > 0) { + current.requests += 1; + totalMaterialUnits += item.quantity; + } + }); + + buckets.forEach((bucket, index) => { + if (!dateRangesOverlap(startDate, endDate, bucket.startIso, bucket.endIso)) return; + timeline[index].materiel += 1; + timeline[index].total += 1; + }); + + history.push({ + id: request.id, + title: request.titre, + type: "demande_materiel_evenementiel", + associationName: materialEvent.associationName, + status: request.status, + startDate, + endDate, + resources: materialEvent.requestedItems.map((item) => `${item.label}${item.quantity > 0 ? ` x${item.quantity}` : ""}`).join(", "), + createdAt: request.createdAt, + }); + } + + const totalRoomDays = salleAnalyticsCatalog.length * range.spanDays; + const bookedRoomDays = Array.from(roomUsage.values()).reduce((sum, room) => sum + room.bookedDays, 0); + const occupancyRate = totalRoomDays > 0 ? Math.round((bookedRoomDays / totalRoomDays) * 1000) / 10 : 0; + const previousTotalReservations = previousSalleReservations + previousMaterialReservations; + const currentTotalReservations = salleReservations + materialReservations; + const currentActiveAssociations = currentAssociationIds.size; + const previousActiveAssociations = previousAssociationIds.size; + const comparePercent = (current: number, previous: number) => { + if (previous === 0) return current > 0 ? 100 : 0; + return Math.round(((current - previous) / previous) * 1000) / 10; + }; + + return { + period, + periodLabel: range.label, + comparisonLabel: range.comparisonLabel, + startDate: range.startIso, + endDate: range.endIso, + summary: { + totalReservations: currentTotalReservations, + salleReservations, + materialReservations, + occupancyRate, + activeAssociations: currentActiveAssociations, + totalMaterialUnits, + averageReservationsPerAssociation: currentActiveAssociations > 0 + ? Math.round((currentTotalReservations / currentActiveAssociations) * 10) / 10 + : 0, + }, + comparison: { + totalReservationsPct: comparePercent(currentTotalReservations, previousTotalReservations), + salleReservationsPct: comparePercent(salleReservations, previousSalleReservations), + materialReservationsPct: comparePercent(materialReservations, previousMaterialReservations), + activeAssociationsPct: comparePercent(currentActiveAssociations, previousActiveAssociations), + }, + timeline, + roomOccupancy: Array.from(roomUsage.values()) + .map((room) => ({ + ...room, + occupancyRate: range.spanDays > 0 ? Math.round((room.bookedDays / range.spanDays) * 1000) / 10 : 0, + })) + .sort((left, right) => right.bookedDays - left.bookedDays), + materialUsage: Array.from(materialUsage.values()) + .filter((item) => item.quantity > 0 || item.requests > 0) + .sort((left, right) => right.quantity - left.quantity), + categoryUsage: Array.from(categoryUsage.values()).sort((left, right) => right.count - left.count), + associationUsage: Array.from(associationUsage.values()) + .sort((left, right) => right.totalReservations - left.totalReservations || right.lastReservationAt.localeCompare(left.lastReservationAt)) + .slice(0, 20), + history: history + .sort((left, right) => new Date(right.startDate || right.createdAt).getTime() - new Date(left.startDate || left.createdAt).getTime()) + .slice(0, 50), + }; +} + +async function generateAndArchiveStatsReport(input: { + period: StatsReportHistoryEntry["period"]; + analyticsPeriod: AnalyticsPeriod; + generatedBy: "scheduler" | "manual"; + cycleKey: string; + now?: Date; +}) { + const analytics = await computeReservationAnalytics(input.analyticsPeriod, input.generatedBy === "scheduler" ? "completed" : "current", input.now || new Date()); + const pdf = await generateReservationAnalyticsPdf(analytics); + const excel = generateReservationAnalyticsExcel(analytics); + const now = input.now || new Date(); + const pdfStored = await storagePut( + `portal/stats-reports/${input.period}/${nanoid()}-${pdf.fileName}`, + pdf.buffer, + pdf.contentType + ); + const excelStored = await storagePut( + `portal/stats-reports/${input.period}/${nanoid()}-${excel.fileName}`, + excel.buffer, + excel.contentType + ); + + const entry: StatsReportHistoryEntry = { + id: nanoid(), + period: input.period, + periodLabel: analytics.periodLabel, + startDate: analytics.startDate, + endDate: analytics.endDate, + generatedAt: now.toISOString(), + pdfUrl: pdfStored.url, + pdfName: pdf.fileName, + excelUrl: excelStored.url, + excelName: excel.fileName, + cycleKey: input.cycleKey, + generatedBy: input.generatedBy, + }; + + const currentHistory = await getStatsReportHistory(); + await saveStatsReportHistory([entry, ...currentHistory]); + return { entry, analytics, pdf, excel }; +} + +export async function runStatsReportScheduler(baseUrl: string) { + const settings = await getStatsAutomationSettings(); + if (!settings.enabled) return; + + const periods: Array = ["weekly", "month", "quarter", "semester", "year"]; + const now = new Date(); + + for (const period of periods) { + const enabled = settings.frequencies[period]; + if (!enabled) continue; + + const cycle = getStatsCycleForToday(period, now); + if (!cycle.due) continue; + if (settings.lastGeneratedCycleKeys[period] === cycle.cycleKey) continue; + + const { entry, pdf, excel } = await generateAndArchiveStatsReport({ + period, + analyticsPeriod: cycle.analyticsPeriod, + generatedBy: "scheduler", + cycleKey: cycle.cycleKey, + now, + }); + + settings.lastGeneratedCycleKeys[period] = cycle.cycleKey; + await saveStatsAutomationSettings(settings); + + await db.createAdminNotification({ + type: "systeme", + titre: `Rapport statistiques ${entry.periodLabel}`, + message: `Le rapport automatique ${entry.periodLabel.toLowerCase()} est prêt. PDF et Excel sont archivés dans le module Statistiques.`, + lien: "/admin?tab=analytics", + }); + + if (settings.recipientEmails.length > 0 && await canSendOperationalEmails()) { + await sendOperationalEmail({ + to: settings.recipientEmails, + subject: `Rapport statistiques CCDS - ${entry.periodLabel}`, + text: [ + `Bonjour,`, + ``, + `Le rapport automatique ${entry.periodLabel.toLowerCase()} a été généré.`, + `Période couverte : ${entry.startDate} au ${entry.endDate}.`, + `Consulter le module : ${baseUrl.replace(/\/$/, "")}/admin?tab=analytics`, + ].join("\n"), + attachments: [ + { filename: pdf.fileName, content: pdf.buffer, contentType: pdf.contentType }, + { filename: excel.fileName, content: excel.buffer, contentType: excel.contentType }, + ], + fromName: "Portail Associations CCDS", + }).catch((error) => { + console.error("[StatsScheduler] failed to send report email:", error); + }); + } + } +} + +function deriveSalleIdsForAnalytics(formData: Record): SalleAnalyticsRoomId[] { + const explicitIds = Array.isArray(formData?.sallesIds) + ? formData.sallesIds.filter((value: unknown): value is SalleAnalyticsRoomId => + typeof value === "string" && salleAnalyticsCatalog.some((entry) => entry.id === value) + ) + : []; + if (explicitIds.length > 0) { + return explicitIds; + } + + const names = Array.isArray(formData?.sallesSelectionnees) + ? formData.sallesSelectionnees.filter((value: unknown): value is string => typeof value === "string" && value.trim().length > 0) + : []; + + return names + .map((name) => { + const normalizedName = name.toLowerCase(); + const byId = salleAnalyticsCatalog.find((entry) => normalizedName.includes(entry.id.toLowerCase())); + if (byId) return byId.id; + const byName = salleAnalyticsCatalog.find((entry) => normalizedName.includes(entry.name.toLowerCase())); + if (byName) return byName.id; + if (normalizedName.includes("toucan")) return "salle_toucan"; + if (normalizedName.includes("ibis")) return "salle_ibis"; + if (normalizedName.includes("pelican") || normalizedName.includes("pélican")) return "salle_pelican"; + if (normalizedName.includes("dojo")) return "dojo"; + if (normalizedName.includes("hall")) return "hall_amphitheatre"; + if (normalizedName.includes("vestiaire")) return "vestiaires"; + if (normalizedName.includes("bureau")) return "bureau_11_17"; + return null; + }) + .filter((value): value is SalleAnalyticsRoomId => value !== null); +} + +function getMaterialRequestEventData(request: Awaited> | Awaited>[number], association?: Awaited> | null) { + const formData = parseRequestFormData(request?.formData); + const startDate = normalizeDateString(formData.dateDebutManifestation || formData.dateManifestation); + const manifestationEnd = normalizeDateString(formData.dateFinManifestation || formData.dateManifestation || startDate); + const restitutionDate = normalizeDateString(formData.dateRestitution || manifestationEnd); + const pickupDate = normalizeDateString(formData.datePriseEnCharge || startDate); + const grantedItems = formData.cadreDSU?.materielEvent?.itemsAccordes || {}; + const grantedQuantities = formData.cadreDSU?.materielEvent?.quantitesAccordees || {}; + const requestedItems = formData.materielsDemandes || {}; + const requestedQuantities = formData.quantitesDemandees || {}; + + const items = materialEventItems + .map((item) => { + const granted = Boolean(grantedItems[item.key]); + const requested = Boolean(requestedItems[item.key]); + if (!granted && !requested) return null; + const quantity = parseMaterialEventQuantity(granted ? grantedQuantities[item.key] : requestedQuantities[item.key]); + return { + key: item.key, + label: getMaterialEventLabel(item.key), + quantity, + granted, + requested, + extra: item.key === "autres" ? String(formData.autreMaterielPrecisions || formData.cadreDSU?.materielEvent?.autresPrecisions || "") : "", + }; + }) + .filter(Boolean) as Array<{ + key: MaterialEventItemKey; + label: string; + quantity: number; + granted: boolean; + requested: boolean; + extra: string; + }>; + + return { + requestId: request?.id, + title: request?.titre || "", + associationId: request?.associationId, + associationName: association?.nomAssociation || formData.nomAssociation || `Association #${request?.associationId}`, + commune: formData.commune || association?.ville || "", + manifestationStart: startDate, + manifestationEnd, + pickupDate, + restitutionDate, + useStartDate: pickupDate || startDate, + useEndDate: restitutionDate || manifestationEnd, + requestedItems: items, + motif: String(formData.motifDemande || request?.description || ""), + }; +} + +function getRequestedMaterialQuantityMap(request: Awaited> | Awaited>[number]) { + const event = getMaterialRequestEventData(request, null); + const next = emptyMaterialEventQuantityMap(); + + event.requestedItems.forEach((item) => { + next[item.key] = item.quantity; + }); + + return next; +} + +function parseBlockedItems(rawValue?: string | null) { + if (!rawValue) { + return emptyMaterialEventQuantityMap(); + } + + try { + return sanitizeMaterialEventQuantityMap(JSON.parse(rawValue)); + } catch { + return emptyMaterialEventQuantityMap(); + } +} + +function serializeBlockedItems(value: Partial>) { + return JSON.stringify(sanitizeMaterialEventQuantityMap(value)); +} + +function sumBlockedInventory( + followups: Array< + | Awaited> + | null + | undefined + > +) { + const totals = emptyMaterialEventQuantityMap(); + + followups.forEach((followup) => { + if (!followup || followup.litigationStatus !== "pending") return; + const blockedItems = parseBlockedItems(followup.blockedItems); + materialEventItems.forEach((item) => { + totals[item.key] += blockedItems[item.key] || 0; + }); + }); + + return totals; +} + +function decodeBase64DataUrl(input: string) { + const raw = input.includes(",") ? input.split(",")[1] || "" : input; + return Buffer.from(raw, "base64"); +} + +async function generateAndStoreMaterialConventionForRequest(input: { + request: NonNullable>>; + association: Awaited>; + decision: { + financialMode: MaterialContractFinancialMode; + depositRequired: boolean; + depositAmountCents: number; + rentalAmountCents: number; + pricingNotes?: string | null; + }; + validatedByUserId: number; +}) { + const contractGeneratedAt = new Date(); + const pdf = await generateMaterialConventionPdf({ + request: input.request, + association: input.association, + decision: { + ...input.decision, + contractStatus: "generee", + contractGeneratedAt, + contractValidatedByUserId: input.validatedByUserId, + }, + }); + + const { key, url } = await storagePut( + `materiel-evenementiel/contract-${input.request.id}-${Date.now()}.pdf`, + pdf.buffer, + "application/pdf" + ); + + return { + contractGeneratedAt, + contractPdfKey: key, + contractPdfUrl: url, + contractPdfName: pdf.fileName, + buffer: pdf.buffer, + }; +} + +async function generateAndStoreSalleConventionForRequest(input: { + request: NonNullable>>; + association: Awaited>; + decision: { + financialMode: MaterialContractFinancialMode; + depositRequired: boolean; + depositAmountCents: number; + rentalAmountCents: number; + pricingNotes?: string | null; + }; + validatedByUserId: number; +}) { + const contractGeneratedAt = new Date(); + const pdf = await generateSalleConventionPdf({ + request: input.request, + association: input.association, + decision: { + ...input.decision, + contractStatus: "generee", + contractGeneratedAt, + contractValidatedByUserId: input.validatedByUserId, + }, + }); + + const { key, url } = await storagePut( + `reservation-salle/contract-${input.request.id}-${Date.now()}.pdf`, + pdf.buffer, + "application/pdf" + ); + + return { + contractGeneratedAt, + contractPdfKey: key, + contractPdfUrl: url, + contractPdfName: pdf.fileName, + buffer: pdf.buffer, + }; +} + +async function generateAndStoreSalleQuoteForRequest(input: { + requestId: number; + requestTitle: string; + formData: any; + association: Awaited>; + pricing: NonNullable>; +}) { + const pdf = await generateSalleQuotePdf({ + requestId: input.requestId, + requestTitle: input.requestTitle, + formData: input.formData, + association: input.association, + pricing: input.pricing, + }); + const { key, url } = await storagePut( + `reservation-salle/devis-${input.requestId}-${Date.now()}.pdf`, + pdf.buffer, + "application/pdf" + ); + return { + quotePdfKey: key, + quotePdfUrl: url, + quotePdfName: pdf.fileName, + buffer: pdf.buffer, + }; +} + +async function generateAndStoreSalleAdministrativeDecisionForRequest(input: { + requestId: number; + decisionText: string; + signedAt?: string | Date | null; + signedByLabel?: string | null; +}) { + const pdf = await generateSalleAdministrativeDecisionPdf({ + requestId: input.requestId, + decisionText: input.decisionText, + signedAt: input.signedAt, + signedByLabel: input.signedByLabel, + }); + const { key, url } = await storagePut( + `reservation-salle/decision-${input.requestId}-${Date.now()}.pdf`, + pdf.buffer, + "application/pdf" + ); + return { + decisionPdfKey: key, + decisionPdfUrl: url, + decisionPdfName: pdf.fileName, + buffer: pdf.buffer, + }; +} + +async function generateAndStoreSalleInvoiceForRequest(input: { + requestId: number; + requestTitle: string; + associationName: string; + datesLabel: string; + pricing: NonNullable>; +}) { + const pdf = await generateSalleInvoicePdf({ + requestId: input.requestId, + requestTitle: input.requestTitle, + associationName: input.associationName, + datesLabel: input.datesLabel, + pricing: input.pricing, + }); + const { key, url } = await storagePut( + `reservation-salle/facture-${input.requestId}-${Date.now()}.pdf`, + pdf.buffer, + "application/pdf" + ); + return { + invoicePdfKey: key, + invoicePdfUrl: url, + invoicePdfName: pdf.fileName, + buffer: pdf.buffer, + }; +} + +async function generateSignedSalleWorkflowPackage(input: { + request: NonNullable>>; + association: Awaited>; + pricing: NonNullable>; + financialDecision: NonNullable>; + signedByUserId: number; + signedByName: string; +}) { + const signedAt = new Date().toISOString(); + let nextFormData = updateSalleWorkflowDirectorStatus(input.request.formData, "signee"); + nextFormData = mergeSalleWorkflowData(nextFormData, { + directorSignedAt: signedAt, + directorSignedByUserId: input.signedByUserId, + directorSignedByName: input.signedByName, + directorReturnComment: "", + directorReturnedAt: "", + }); + + const refreshedRequest = { + ...input.request, + formData: nextFormData, + }; + + const texts = buildDefaultSalleWorkflowTexts(nextFormData); + const decisionPdf = await generateAndStoreSalleAdministrativeDecisionForRequest({ + requestId: input.request.id, + decisionText: texts.decisionAdministrativeText, + signedAt, + signedByLabel: input.signedByName, + }); + const quotePdf = input.financialDecision.financialMode === "location_payante" + ? await generateAndStoreSalleQuoteForRequest({ + requestId: input.request.id, + requestTitle: input.request.titre, + formData: parseRequestFormData(nextFormData), + association: input.association, + pricing: input.pricing, + }) + : null; + + let signedQuoteAttachment: + | { + filename: string; + content: Buffer; + contentType: string; + } + | undefined; + if (quotePdf) { + const signedQuotePdf = await generateSalleQuotePdf({ + requestId: input.request.id, + requestTitle: input.request.titre, + formData: parseRequestFormData(nextFormData), + association: input.association, + pricing: input.pricing, + signedAt, + signedByLabel: input.signedByName, + }); + const { url } = await storagePut( + `reservation-salle/devis-signe-${input.request.id}-${Date.now()}.pdf`, + signedQuotePdf.buffer, + "application/pdf" + ); + nextFormData = mergeSalleWorkflowData(nextFormData, { + signedQuotePdfUrl: url, + signedQuotePdfName: signedQuotePdf.fileName, + }); + signedQuoteAttachment = { + filename: signedQuotePdf.fileName, + content: signedQuotePdf.buffer, + contentType: "application/pdf", + }; + } + + const signedRequestPdf = await generateRequestPdfDocument({ + ...refreshedRequest, + status: "en_cours_traitement", + dateTraitement: new Date(), + formData: nextFormData, + }); + const signedRequestStored = await storagePut( + `reservation-salle/formulaire-signe-${input.request.id}-${Date.now()}.pdf`, + signedRequestPdf.buffer, + signedRequestPdf.contentType + ); + nextFormData = mergeSalleWorkflowData(nextFormData, { + signedDecisionPdfUrl: decisionPdf.decisionPdfUrl, + signedDecisionPdfName: decisionPdf.decisionPdfName, + signedRequestPdfUrl: signedRequestStored.url, + signedRequestPdfName: signedRequestPdf.fileName, + }); + + let generatedContractAttachment: + | { + filename: string; + content: Buffer; + contentType?: string; + } + | undefined; + let invoiceAttachment: + | { + filename: string; + content: Buffer; + contentType: string; + } + | undefined; + const generatedContract = await generateAndStoreSalleConventionForRequest({ + request: { + ...input.request, + formData: nextFormData, + }, + association: input.association, + decision: { + financialMode: input.financialDecision.financialMode, + depositRequired: input.financialDecision.depositRequired, + depositAmountCents: input.financialDecision.depositAmountCents, + rentalAmountCents: input.financialDecision.rentalAmountCents, + pricingNotes: input.financialDecision.pricingNotes, + }, + validatedByUserId: input.signedByUserId, + }); + nextFormData = mergeMaterialFinancialDecision({ + rawFormData: nextFormData, + decision: { + financialMode: input.financialDecision.financialMode, + depositRequired: input.financialDecision.depositRequired, + depositAmountCents: input.financialDecision.depositAmountCents, + rentalAmountCents: input.financialDecision.rentalAmountCents, + pricingNotes: input.financialDecision.pricingNotes, + contractStatus: "signee", + contractPdfUrl: generatedContract.contractPdfUrl, + contractPdfName: generatedContract.contractPdfName, + contractGeneratedAt: generatedContract.contractGeneratedAt, + contractValidatedByUserId: input.signedByUserId, + }, + }); + generatedContractAttachment = { + filename: generatedContract.contractPdfName, + content: generatedContract.buffer, + contentType: "application/pdf", + }; + + if (input.financialDecision.financialMode === "location_payante") { + const invoiceGeneratedAt = new Date().toISOString(); + const invoice = await generateAndStoreSalleInvoiceForRequest({ + requestId: input.request.id, + requestTitle: input.request.titre, + associationName: + input.association?.nomAssociation + || parseRequestFormData(nextFormData).nomAssociation + || "Association", + datesLabel: buildSalleDatesLabel(parseRequestFormData(nextFormData)), + pricing: input.pricing, + }); + nextFormData = mergeSalleWorkflowData(nextFormData, { + invoicePdfUrl: invoice.invoicePdfUrl, + invoicePdfName: invoice.invoicePdfName, + invoiceGeneratedAt, + }); + invoiceAttachment = { + filename: invoice.invoicePdfName, + content: invoice.buffer, + contentType: "application/pdf", + }; + } + + nextFormData = ensureSallePaymentTracking({ + rawFormData: nextFormData, + billed: input.financialDecision.financialMode === "location_payante", + totalAmountCents: input.pricing.totalAmountCents, + }); + + return { + nextFormData, + signedAt, + quotePdfUrl: quotePdf?.quotePdfUrl || "", + quotePdfName: quotePdf?.quotePdfName || "", + signedQuoteAttachment, + signedDecisionAttachment: { + filename: decisionPdf.decisionPdfName, + content: decisionPdf.buffer, + contentType: "application/pdf", + }, + signedRequestAttachment: { + filename: signedRequestPdf.fileName, + content: signedRequestPdf.buffer, + contentType: signedRequestPdf.contentType, + }, + generatedContractAttachment, + invoiceAttachment, + }; +} + +async function ensureSalleInvoicePrepared(input: { + request: NonNullable>>; + association: Awaited>; + pricing: NonNullable>; + billed: boolean; +}) { + if (!input.billed) { + return { + nextFormData: input.request.formData || "", + invoicePdfUrl: "", + invoicePdfName: "", + invoiceGeneratedAt: "", + buffer: null as Buffer | null, + }; + } + + const workflow = getSalleWorkflowData(input.request.formData); + if (workflow.invoicePdfUrl && workflow.invoicePdfName) { + return { + nextFormData: input.request.formData || "", + invoicePdfUrl: workflow.invoicePdfUrl, + invoicePdfName: workflow.invoicePdfName, + invoiceGeneratedAt: workflow.invoiceGeneratedAt || "", + buffer: null as Buffer | null, + }; + } + + const formData = parseRequestFormData(input.request.formData); + const invoice = await generateAndStoreSalleInvoiceForRequest({ + requestId: input.request.id, + requestTitle: input.request.titre, + associationName: input.association?.nomAssociation || formData.nomAssociation || "Association", + datesLabel: buildSalleDatesLabel(formData), + pricing: input.pricing, + }); + const nextFormData = mergeSalleWorkflowData(input.request.formData, { + invoicePdfUrl: invoice.invoicePdfUrl, + invoicePdfName: invoice.invoicePdfName, + invoiceGeneratedAt: new Date().toISOString(), + }); + const nextFormDataWithPayment = ensureSallePaymentTracking({ + rawFormData: nextFormData, + billed: input.billed, + totalAmountCents: input.pricing.totalAmountCents, + }); + + return { + nextFormData: nextFormDataWithPayment, + invoicePdfUrl: invoice.invoicePdfUrl, + invoicePdfName: invoice.invoicePdfName, + invoiceGeneratedAt: new Date().toISOString(), + buffer: invoice.buffer, + }; +} + +async function sendSalleQuoteWorkflow(input: { + request: NonNullable>>; + association: Awaited>; + req: Parameters[0]; + user: { + id: number; + email?: string | null; + }; +}) { + const request = input.request; + if (request.type !== "demande_salle") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Seules les demandes de salle peuvent envoyer un devis" }); + } + + const associationRecipient = input.association?.emailContact?.trim() || parseRequestFormData(request.formData).emailAssociation?.trim(); + if (!associationRecipient) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Aucun email d'association n'est disponible pour envoyer le devis" }); + } + + const financialDecision = parseMaterialFinancialDecision(request.formData); + if (!financialDecision) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Les conditions financières doivent être renseignées avant l'envoi du devis" }); + } + + const pricing = computeSalleWorkflowPricing(request.formData); + if (!pricing || pricing.unsupportedSalles.length > 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Tarification automatique indisponible pour : ${pricing?.unsupportedSalles.map((item) => item.salleNom).join(", ") || "cette demande"}`, + }); + } + + const formData = parseRequestFormData(request.formData); + const texts = buildDefaultSalleWorkflowTexts(request.formData); + const decisionPdf = await generateAndStoreSalleAdministrativeDecisionForRequest({ + requestId: request.id, + decisionText: texts.decisionAdministrativeText, + }); + const quotePdf = financialDecision.financialMode === "location_payante" + ? await generateAndStoreSalleQuoteForRequest({ + requestId: request.id, + requestTitle: request.titre, + formData, + association: input.association, + pricing, + }) + : null; + + const acceptToken = nanoid(48); + const refuseToken = nanoid(48); + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + await Promise.all([ + db.createEmailActionToken({ token: acceptToken, requestId: request.id, action: "acceptation_devis_salle", expiresAt }), + db.createEmailActionToken({ token: refuseToken, requestId: request.id, action: "refus_devis_salle", expiresAt }), + ]); + + const baseUrl = buildAppBaseUrl(input.req); + const acceptUrl = `${baseUrl}/api/email-action/${acceptToken}`; + const refuseUrl = `${baseUrl}/api/email-action/${refuseToken}`; + const email = generateSalleQuoteEmail({ + associationName: input.association?.nomAssociation || formData.nomAssociation || "association", + requestTitle: request.titre, + pricing, + acceptUrl, + refuseUrl, + decisionText: texts.decisionAdministrativeText, + }); + + const sentAt = new Date(); + let updatedFormData = mergeSalleWorkflowData(request.formData, { + usageType: pricing.usageType, + frequency: pricing.frequency, + conditionsFinancieresText: CONDITIONS_FINANCIERES_SALLE_TEXT, + decisionAdministrativeText: texts.decisionAdministrativeText, + pricing, + quoteStatus: "en_attente_association", + quoteSentAt: sentAt.toISOString(), + quotePdfUrl: quotePdf?.quotePdfUrl || "", + quotePdfName: quotePdf?.quotePdfName || "", + administrativeDecisionPdfUrl: decisionPdf.decisionPdfUrl, + administrativeDecisionPdfName: decisionPdf.decisionPdfName, + directorStatus: "a_transmettre", + }); + updatedFormData = ensureSallePaymentTracking({ + rawFormData: updatedFormData, + billed: financialDecision.financialMode === "location_payante", + totalAmountCents: pricing.totalAmountCents, + }); + + const result = await sendOperationalEmail({ + to: [associationRecipient], + subject: email.subject, + text: email.text, + html: email.html, + replyTo: input.user.email || undefined, + fromName: "Maison de la Jeunesse des Savanes - CCDS", + attachments: [ + ...(quotePdf ? [{ filename: quotePdf.quotePdfName, content: quotePdf.buffer, contentType: "application/pdf" }] : []), + { filename: decisionPdf.decisionPdfName, content: decisionPdf.buffer, contentType: "application/pdf" }, + ], + }); + + if (!result.sent) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Le mail n'a pas pu être envoyé (${result.reason || "raison inconnue"})` }); + } + + return { + updatedFormData, + pricing, + financialMode: financialDecision.financialMode, + quotePdfUrl: quotePdf?.quotePdfUrl || null, + decisionPdfUrl: decisionPdf.decisionPdfUrl, + }; +} + +function getInvitationExpiry() { + return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); +} + +function getInvitationStatusReason(invitation: { + usedAt: Date | null; + revokedAt: Date | null; + expiresAt: Date; +}) { + if (invitation.usedAt) return "used"; + if (invitation.revokedAt) return "revoked"; + if (new Date() > invitation.expiresAt) return "expired"; + return null; +} + +export const appRouter = router({ + system: systemRouter, + + auth: router({ + me: publicProcedure.query(async opts => withEffectiveInternalAccess(opts.ctx.user)), + databaseStatus: publicProcedure.query(() => ({ + configured: db.isDatabaseConfigured(), + })), + providers: publicProcedure.query(() => getOAuthProviderStatus()), + register: publicProcedure + .input(z.object({ + name: z.string().min(2, "Le nom est requis"), + email: z.string().email("Adresse email invalide"), + password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caracteres"), + invitationToken: z.string().min(1).optional(), + privacyConsent: dataPrivacyConsentSchema, + })) + .mutation(async ({ ctx, input }) => { + try { + assertDataPrivacyConsent(input.privacyConsent); + let registrationEmail = input.email; + let invitation: Awaited> | null = null; + let directoryEntry: Awaited> | undefined; + + if (input.invitationToken) { + invitation = await db.getAssociationInvitationByToken(input.invitationToken); + if (!invitation) { + throw new Error("Invitation introuvable"); + } + + const invalidReason = getInvitationStatusReason(invitation); + if (invalidReason === "used") { + throw new Error("Cette invitation a déjà été utilisée"); + } + if (invalidReason === "revoked") { + throw new Error("Cette invitation a été remplacée par une nouvelle"); + } + if (invalidReason === "expired") { + throw new Error("Cette invitation a expiré"); + } + + directoryEntry = await db.getAssociationDirectoryEntryById(invitation.directoryEntryId); + if (!directoryEntry) { + throw new Error("Association du bordereau introuvable"); + } + + const existingAssociation = await db.getAssociationBySourceDirectoryEntryId(invitation.directoryEntryId); + if (existingAssociation) { + throw new Error("Cette association a déjà activé son espace"); + } + + if (normalizeEmail(input.email) !== invitation.emailOfficielNormalise) { + throw new Error("L'email saisi ne correspond pas à l'invitation"); + } + + registrationEmail = invitation.emailOfficiel; + } + + const user = await registerLocalUser({ + name: input.name, + email: registrationEmail, + password: input.password, + }); + await recordDataPrivacyConsent(user.id, ctx.req, input.privacyConsent); + + const matchedDirectoryEntry = directoryEntry + || await db.getAssociationDirectoryEntryByNormalizedEmail(normalizeEmail(registrationEmail)); + + if (matchedDirectoryEntry) { + await ensureAssociationLinkedToDirectoryEntry(user.id, matchedDirectoryEntry.id); + await resolvePendingAssociationDirectoryReviewAfterAutoLink({ + userId: user.id, + directoryEntryId: matchedDirectoryEntry.id, + sourceLabel: "l'inscription portail", + }); + } else if (!invitation) { + const matchResult = await db.findAssociationDirectoryMatch({ + nomAssociation: input.name, + email: registrationEmail, + }); + await queueAssociationDirectoryReview({ + userId: user.id, + sourceType: "portal_signup", + sourceLabel: "Inscription portail", + nomAssociation: input.name, + email: registrationEmail, + matchResult, + }); + } + + if (invitation && matchedDirectoryEntry) { + await db.markAssociationInvitationUsed(invitation.token, user.id); + } + + const token = await createSessionToken(user); + setSessionCookie(ctx.req, ctx.res, token); + return withEffectiveInternalAccess(user); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error.message : "Inscription impossible", + }); + } + }), + login: publicProcedure + .input(z.object({ + email: z.string().email("Adresse email invalide"), + password: z.string().min(1, "Le mot de passe est requis"), + })) + .mutation(async ({ ctx, input }) => { + try { + const user = await loginLocalUser(input); + const token = await createSessionToken(user); + setSessionCookie(ctx.req, ctx.res, token); + return { + mfaRequired: false as const, + user: await withEffectiveInternalAccess(user), + }; + } catch (error) { + if (isMfaCodeRequiredError(error)) { + return { + mfaRequired: true as const, + challengeType: error.challengeType, + challengeToken: error.challengeToken, + expiresAt: error.expiresAt, + maskedEmail: error.maskedEmail, + }; + } + + if (isAccountLockedError(error)) { + throw new TRPCError({ + code: "TOO_MANY_REQUESTS", + message: error.message, + }); + } + + throw new TRPCError({ + code: "UNAUTHORIZED", + message: error instanceof Error ? error.message : "Email ou mot de passe incorrect", + }); + } + }), + verifyLoginMfa: publicProcedure + .input(z.object({ + challengeToken: z.string().min(12), + code: z.string().trim().regex(/^\d{6}$/, "Le code doit contenir 6 chiffres"), + })) + .mutation(async ({ ctx, input }) => { + try { + const user = await verifyLocalUserMfa(input); + const token = await createSessionToken(user); + setSessionCookie(ctx.req, ctx.res, token); + return await withEffectiveInternalAccess(user); + } catch (error) { + if (isAccountLockedError(error)) { + throw new TRPCError({ + code: "TOO_MANY_REQUESTS", + message: error.message, + }); + } + + throw new TRPCError({ + code: "UNAUTHORIZED", + message: error instanceof Error ? error.message : "Verification impossible", + }); + } + }), + resendLoginMfa: publicProcedure + .input(z.object({ + challengeToken: z.string().min(12), + })) + .mutation(async ({ input }) => { + try { + const payload = await resendLocalUserMfaChallenge(input.challengeToken); + return { + challengeType: "email" as const, + ...payload, + }; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error.message : "Impossible de renvoyer le code", + }); + } + }), + logout: publicProcedure.mutation(({ ctx }) => { + clearSessionCookie(ctx.req, ctx.res); + return { success: true } as const; + }), + getSecuritySettings: protectedProcedure.query(async ({ ctx }) => { + const freshUser = await db.getUserById(ctx.user.id); + const loginMethod = freshUser?.loginMethod || ctx.user.loginMethod || null; + const smtpReady = await canSendOperationalEmails(); + const canManageMfa = loginMethod === "local_jwt" && Boolean(freshUser?.email); + const effectiveRole = freshUser?.role || ctx.user.role; + const mfaPolicy = getInternalMfaPolicy(effectiveRole); + const mfaRequiredByRole = Boolean(mfaPolicy?.required); + const hasAuthenticator = Boolean( + freshUser?.mfaMethod === "authenticator_app" && freshUser?.mfaTotpSecretEncrypted + ); + const emailMfaActive = Boolean((freshUser?.mfaEnabled || mfaRequiredByRole) && freshUser?.email); + + return { + email: freshUser?.email || ctx.user.email || "", + role: effectiveRole, + loginMethod, + mfaEnabled: Boolean(freshUser?.mfaEnabled || mfaRequiredByRole), + canManageMfa, + smtpReady, + passwordHashAlgorithm: freshUser?.passwordHash?.split(":")[0] || null, + mfaMethod: hasAuthenticator ? "authenticator_app" : emailMfaActive ? "email_otp" : null, + mfaRequiredByRole, + hasPendingAuthenticatorSetup: Boolean(freshUser?.mfaTotpPendingSecretEncrypted), + mfaPolicy: { + required: mfaRequiredByRole, + preferredMethod: mfaPolicy?.preferredMethod || null, + enforcedMethod: mfaPolicy?.enforcedMethod || null, + emailFallbackAllowed: mfaPolicy?.emailFallbackAllowed ?? true, + phaseLabel: mfaPolicy?.phaseLabel || null, + currentCompliance: + hasAuthenticator + ? "aligned" + : mfaPolicy?.enforcedMethod === "authenticator_app" + ? "transition" + : emailMfaActive + ? "aligned" + : mfaRequiredByRole + ? "missing" + : "optional", + }, + deletionRequestedAt: freshUser?.deletionRequestedAt || null, + purgeScheduledAt: freshUser?.purgeScheduledAt || null, + lockoutPolicy: { + maxAttempts: 5, + lockMinutes: 15, + }, + }; + }), + setMfaEnabled: protectedProcedure + .input(z.object({ + enabled: z.boolean(), + })) + .mutation(async ({ ctx, input }) => { + const freshUser = await db.getUserById(ctx.user.id); + if (!freshUser) { + throw new TRPCError({ code: "NOT_FOUND", message: "Compte introuvable" }); + } + + if (freshUser.loginMethod !== "local_jwt" || !freshUser.email) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le MFA par email est disponible uniquement pour les comptes locaux avec une adresse email valide.", + }); + } + + if (input.enabled && !(await canSendOperationalEmails())) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le SMTP doit etre configure avant d'activer le MFA par email.", + }); + } + + const rolePolicy = getInternalMfaPolicy(freshUser.role); + const isRequiredByRole = Boolean(rolePolicy?.required); + + if (!input.enabled && isRequiredByRole) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le MFA est obligatoire pour ce rôle interne. Passez sur Authenticator ou gardez le code email.", + }); + } + + if (input.enabled && rolePolicy?.emailFallbackAllowed === false) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Ce rôle interne doit utiliser l'application Authenticator. Le code email n'est plus autorisé comme méthode normale.", + }); + } + + await db.updateUser(ctx.user.id, { + mfaEnabled: input.enabled, + mfaMethod: input.enabled ? "email_otp" : null, + mfaChallengeToken: null, + mfaCodeHash: null, + mfaCodeExpiresAt: null, + mfaCodeAttempts: 0, + mfaTotpSecretEncrypted: null, + mfaTotpPendingSecretEncrypted: null, + }); + + await db.createAuditLog({ + userId: ctx.user.id, + action: input.enabled ? "activation_mfa" : "desactivation_mfa", + entityType: "user", + entityId: ctx.user.id, + details: JSON.stringify({ + method: input.enabled ? "email_otp" : null, + }), + ipAddress: extractClientIp(ctx.req) || undefined, + }); + + return { success: true }; + }), + startAuthenticatorSetup: protectedProcedure.mutation(async ({ ctx }) => { + const user = await db.getUserById(ctx.user.id); + if (!user) { + throw new TRPCError({ code: "NOT_FOUND", message: "Compte introuvable" }); + } + try { + return await startAuthenticatorSetup(user); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error.message : "Impossible de démarrer la configuration Authenticator", + }); + } + }), + confirmAuthenticatorSetup: protectedProcedure + .input(z.object({ + code: z.string().trim().regex(/^\d{6}$/, "Le code doit contenir 6 chiffres"), + })) + .mutation(async ({ ctx, input }) => { + const user = await db.getUserById(ctx.user.id); + if (!user) { + throw new TRPCError({ code: "NOT_FOUND", message: "Compte introuvable" }); + } + try { + await confirmAuthenticatorSetup(user, input.code); + return { success: true }; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error.message : "Impossible de confirmer Authenticator", + }); + } + }), + cancelAuthenticatorSetup: protectedProcedure.mutation(async ({ ctx }) => { + const user = await db.getUserById(ctx.user.id); + if (!user) { + throw new TRPCError({ code: "NOT_FOUND", message: "Compte introuvable" }); + } + await cancelAuthenticatorSetup(user); + return { success: true }; + }), + exportMyData: protectedProcedure.query(async ({ ctx }) => { + const [user, association] = await Promise.all([ + db.getUserById(ctx.user.id), + db.getAssociationByUserId(ctx.user.id), + ]); + + const documents = association ? await db.getDocumentsByAssociationId(association.id) : []; + const requests = association ? await db.getRequestsByAssociationId(association.id) : []; + const requestHistories = await Promise.all( + requests.map(async (request) => ({ + requestId: request.id, + history: await db.getRequestHistoryByRequestId(request.id), + })) + ); + + return { + generatedAt: new Date().toISOString(), + exportVersion: "2026-06-12", + user: user + ? { + id: user.id, + name: user.name, + email: user.email, + role: user.role, + loginMethod: user.loginMethod, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + lastSignedIn: user.lastSignedIn, + privacyConsentVersion: user.privacyConsentVersion, + privacyConsentAcceptedAt: user.privacyConsentAcceptedAt, + privacyConsentContext: user.privacyConsentContext, + } + : null, + association, + documents: documents.map((document) => ({ + id: document.id, + nom: document.nom, + type: document.type, + description: document.description, + fileUrl: document.fileUrl, + mimeType: document.mimeType, + fileSize: document.fileSize, + uploadedAt: document.uploadedAt, + updatedAt: document.updatedAt, + })), + requests, + requestHistories, + }; + }), + deleteMyAccount: protectedProcedure + .input(z.object({ + confirm: z.literal(true), + })) + .mutation(async ({ ctx }) => { + if (ctx.user.role !== "user") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Les comptes internes doivent etre clotures par l'administration.", + }); + } + + const user = await db.getUserById(ctx.user.id); + if (!user) { + throw new TRPCError({ code: "NOT_FOUND", message: "Compte introuvable" }); + } + + let deletion; + try { + deletion = await scheduleAccountDeletion(user); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error.message : "Impossible de planifier la suppression du compte", + }); + } + + await db.createAuditLog({ + userId: ctx.user.id, + action: "demande_suppression_compte_utilisateur", + entityType: "user", + entityId: ctx.user.id, + details: JSON.stringify({ + purgeScheduledAt: deletion.purgeScheduledAt, + ipAddress: extractClientIp(ctx.req), + }), + ipAddress: extractClientIp(ctx.req) || undefined, + }); + + clearSessionCookie(ctx.req, ctx.res); + + return { success: true, purgeScheduledAt: deletion.purgeScheduledAt }; + }), + cancelAccountDeletion: protectedProcedure.mutation(async ({ ctx }) => { + const user = await db.getUserById(ctx.user.id); + if (!user) { + throw new TRPCError({ code: "NOT_FOUND", message: "Compte introuvable" }); + } + await cancelScheduledAccountDeletion(user); + await db.createAuditLog({ + userId: ctx.user.id, + action: "annulation_suppression_compte_utilisateur", + entityType: "user", + entityId: ctx.user.id, + details: JSON.stringify({ + ipAddress: extractClientIp(ctx.req), + }), + ipAddress: extractClientIp(ctx.req) || undefined, + }); + return { success: true }; + }), + }), + + // ============== ASSOCIATION ROUTES ============== + association: router({ + getMyProfile: protectedProcedure.query(async ({ ctx }) => { + const existing = await db.getAssociationByUserId(ctx.user.id); + if (existing) { + return existing; + } + + if (!ctx.user.email) { + return null; + } + + const directoryEntry = await db.getAssociationDirectoryEntryByNormalizedEmail(normalizeEmail(ctx.user.email)); + if (!directoryEntry) { + return null; + } + + const id = await db.createAssociation(createAssociationProfileFromDirectoryEntry(ctx.user.id, directoryEntry)); + return db.getAssociationById(id); + }), + + upsertProfile: protectedProcedure + .input(z.object({ + nomAssociation: z.string().min(1), + siret: z.string().optional(), + rna: z.string().optional(), + thematiques: z.array(z.enum(associationThematicValues)).optional(), + adresse: z.string().optional(), + codePostal: z.string().optional(), + ville: z.string().optional(), + telephone: z.string().optional(), + emailContact: optionalEmailFieldSchema, + siteWeb: z.string().url().optional().or(z.literal('')), + facebookUrl: socialUrlSchema, + instagramUrl: socialUrlSchema, + dateCreation: z.string().optional(), + objetAssociation: z.string().optional(), + statutJuridique: z.enum(['association_loi_1901', 'association_reconnue_utilite_publique', 'fondation', 'autre']).optional(), + nomRepresentant: z.string().optional(), + fonctionRepresentant: z.string().optional(), + gouvernance: z.object({ + representantLegal: z.object({ + nom: z.string().min(1), + prenom: z.string().min(1), + email: z.string().email().optional().or(z.literal("")), + telephone: z.string().optional(), + fonction: z.enum(legalRepresentativeRoleValues), + }), + membres: z.array( + z.object({ + nom: z.string().optional(), + prenom: z.string().optional(), + email: z.string().email().optional().or(z.literal("")), + telephone: z.string().optional(), + fonction: z.enum(governanceMemberRoleValues), + }) + ).optional(), + }).optional(), + privacyConsent: dataPrivacyConsentSchema, + })) + .mutation(async ({ ctx, input }) => { + assertDataPrivacyConsent(input.privacyConsent); + validateSocialUrl("facebookUrl", input.facebookUrl); + validateSocialUrl("instagramUrl", input.instagramUrl); + + const existing = await db.getAssociationByUserId(ctx.user.id); + const rawGovernance: AssociationGovernance = input.gouvernance + ? { + representantLegal: { + nom: input.gouvernance.representantLegal.nom, + prenom: input.gouvernance.representantLegal.prenom, + email: input.gouvernance.representantLegal.email || "", + telephone: input.gouvernance.representantLegal.telephone || "", + fonction: input.gouvernance.representantLegal.fonction, + }, + membres: (input.gouvernance.membres ?? []).map((member) => ({ + nom: member.nom || "", + prenom: member.prenom || "", + email: member.email || "", + telephone: member.telephone || "", + fonction: member.fonction, + })), + } + : buildGovernanceFromLegacyRepresentative(input.nomRepresentant, input.fonctionRepresentant); + const governance = sanitizeGovernance(rawGovernance); + const representantLegal = governance.representantLegal; + if (!representantLegal?.nom || !representantLegal?.prenom || !representantLegal?.fonction) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le représentant légal principal est obligatoire.", + }); + } + const nomRepresentant = buildDisplayName(representantLegal); + + const data = { + nomAssociation: input.nomAssociation, + siret: input.siret, + rna: input.rna, + adresse: input.adresse, + codePostal: input.codePostal, + ville: input.ville, + telephone: input.telephone, + emailContact: input.emailContact?.trim() || null, + objetAssociation: input.objetAssociation, + statutJuridique: input.statutJuridique, + userId: ctx.user.id, + dateCreation: input.dateCreation ? new Date(input.dateCreation) : undefined, + siteWeb: input.siteWeb || null, + facebookUrl: normalizeSocialUrl(input.facebookUrl) || null, + instagramUrl: normalizeSocialUrl(input.instagramUrl) || null, + thematique: serializeAssociationThematics(input.thematiques), + nomRepresentant, + fonctionRepresentant: representantLegal.fonction, + gouvernance: serializeAssociationGovernance(governance), + profileComplete: !!(input.nomAssociation && input.adresse && input.ville && nomRepresentant), + }; + + if (existing) { + const sourceDirectoryEntryId = existing.sourceDirectoryEntryId || null; + let nextSourceDirectoryEntryId = sourceDirectoryEntryId; + + if (!sourceDirectoryEntryId) { + const matchResult = await db.findAssociationDirectoryMatch({ + nomAssociation: input.nomAssociation, + email: input.emailContact || ctx.user.email || null, + siret: input.siret, + rna: input.rna, + ville: input.ville, + telephone: input.telephone, + nomRepresentant, + }); + + if (matchResult.status === "matched" && matchResult.matchedEntry) { + const occupied = await db.getAssociationBySourceDirectoryEntryId(matchResult.matchedEntry.id); + if (!occupied || occupied.id === existing.id) { + nextSourceDirectoryEntryId = matchResult.matchedEntry.id; + } else { + await queueAssociationDirectoryReview({ + userId: ctx.user.id, + sourceType: "portal_signup", + sourceLabel: "Profil portail", + nomAssociation: input.nomAssociation, + email: input.emailContact || ctx.user.email || null, + siret: input.siret, + rna: input.rna, + ville: input.ville, + telephone: input.telephone, + nomRepresentant, + matchResult: { + ...matchResult, + status: "ambiguous", + reason: "Une fiche du bordereau compatible existe déjà, mais elle est rattachée à un autre compte portail.", + }, + }); + } + } else { + await queueAssociationDirectoryReview({ + userId: ctx.user.id, + sourceType: "portal_signup", + sourceLabel: "Profil portail", + nomAssociation: input.nomAssociation, + email: input.emailContact || ctx.user.email || null, + siret: input.siret, + rna: input.rna, + ville: input.ville, + telephone: input.telephone, + nomRepresentant, + matchResult, + }); + } + } + + if (nextSourceDirectoryEntryId) { + await resolvePendingAssociationDirectoryReviewAfterAutoLink({ + userId: ctx.user.id, + directoryEntryId: nextSourceDirectoryEntryId, + sourceLabel: "la mise à jour de la fiche association", + }); + } + + await db.updateAssociation(existing.id, { + ...data, + ...(nextSourceDirectoryEntryId ? { sourceDirectoryEntryId: nextSourceDirectoryEntryId } : {}), + }); + const updatedAssociation = await db.getAssociationById(existing.id); + await syncAssociationBackToDirectoryEntry(updatedAssociation); + return { id: existing.id, updated: true }; + } else { + let sourceDirectoryEntryId: number | undefined; + const matchResult = await db.findAssociationDirectoryMatch({ + nomAssociation: input.nomAssociation, + email: input.emailContact || ctx.user.email || null, + siret: input.siret, + rna: input.rna, + ville: input.ville, + telephone: input.telephone, + nomRepresentant, + }); + if (matchResult.status === "matched" && matchResult.matchedEntry) { + const occupied = await db.getAssociationBySourceDirectoryEntryId(matchResult.matchedEntry.id); + if (!occupied) { + sourceDirectoryEntryId = matchResult.matchedEntry.id; + } else { + await queueAssociationDirectoryReview({ + userId: ctx.user.id, + sourceType: "portal_signup", + sourceLabel: "Profil portail", + nomAssociation: input.nomAssociation, + email: input.emailContact || ctx.user.email || null, + siret: input.siret, + rna: input.rna, + ville: input.ville, + telephone: input.telephone, + nomRepresentant, + matchResult: { + ...matchResult, + status: "ambiguous", + reason: "Une fiche du bordereau compatible existe déjà, mais elle est déjà rattachée à un autre compte portail.", + }, + }); + } + } else { + await queueAssociationDirectoryReview({ + userId: ctx.user.id, + sourceType: "portal_signup", + sourceLabel: "Profil portail", + nomAssociation: input.nomAssociation, + email: input.emailContact || ctx.user.email || null, + siret: input.siret, + rna: input.rna, + ville: input.ville, + telephone: input.telephone, + nomRepresentant, + matchResult, + }); + } + + if (sourceDirectoryEntryId) { + await resolvePendingAssociationDirectoryReviewAfterAutoLink({ + userId: ctx.user.id, + directoryEntryId: sourceDirectoryEntryId, + sourceLabel: "la création de la fiche association", + }); + } + + const id = await db.createAssociation({ + ...(data as any), + ...(sourceDirectoryEntryId ? { sourceDirectoryEntryId } : {}), + }); + const createdAssociation = await db.getAssociationById(id); + await syncAssociationBackToDirectoryEntry(createdAssociation); + // Create notification for new association + await db.createAdminNotification({ + type: 'nouvelle_association', + titre: 'Nouvelle association inscrite', + message: `L'association "${input.nomAssociation}" vient de s'inscrire sur le portail.`, + lien: `/admin?tab=associations`, + }); + return { id, updated: false }; + } + + await recordDataPrivacyConsent(ctx.user.id, ctx.req, input.privacyConsent); + }), + + getById: adminProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + const association = await db.getAssociationById(input.id); + if (!association) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Association non trouvée' }); + } + // Get related data + const [documents, requests] = await Promise.all([ + db.getDocumentsByAssociationId(input.id), + db.getRequestsByAssociationId(input.id), + ]); + return { ...association, documents, requests }; + }), + + listAll: adminProcedure.query(async () => { + return db.getAllAssociations(); + }), + + search: adminProcedure + .input(z.object({ + search: z.string().optional(), + ville: z.string().optional(), + profileComplete: z.boolean().optional(), + isActive: z.boolean().optional(), + limit: z.number().optional(), + offset: z.number().optional(), + })) + .query(async ({ input }) => { + return db.searchAssociations(input); + }), + + getCommuneCounts: adminProcedure.query(async () => { + return db.getAssociationCommuneCounts(); + }), + + toggleStatus: adminProcedure + .input(z.object({ + id: z.number(), + isActive: z.boolean(), + })) + .mutation(async ({ ctx, input }) => { + await db.toggleAssociationStatus(input.id, input.isActive); + await logAdminAction(ctx.user.id, input.isActive ? 'activation' : 'desactivation', 'association', input.id); + return { success: true }; + }), + + export: adminProcedure.query(async () => { + const associations = await db.getAllAssociations(); + return associations.map(a => ({ + id: a.id, + sourceDirectoryEntryId: a.sourceDirectoryEntryId, + nomAssociation: a.nomAssociation, + siret: a.siret || '', + rna: a.rna || '', + thematique: a.thematique || '', + adresse: a.adresse || '', + codePostal: a.codePostal || '', + ville: a.ville || '', + telephone: a.telephone || '', + emailContact: a.emailContact || '', + statutJuridique: a.statutJuridique || '', + nomRepresentant: a.nomRepresentant || '', + fonctionRepresentant: a.fonctionRepresentant || '', + profileComplete: a.profileComplete ? 'Oui' : 'Non', + isActive: a.isActive ? 'Actif' : 'Inactif', + createdAt: a.createdAt?.toISOString() || '', + })); + }), + }), + + associationDirectory: router({ + getSummary: adminProcedure.query(async () => { + return db.getAssociationDirectoryEntriesSummary(); + }), + + listReviewQueue: adminProcedure + .input(z.object({ + status: z.enum(["pending", "linked", "created", "ignored"]).optional(), + sourceType: z.enum(["portal_signup", "helloasso"]).optional(), + limit: z.number().min(1).max(100).optional(), + }).optional()) + .query(async ({ input }) => { + const reviews = await db.listAssociationDirectoryReviews({ + status: input?.status, + sourceType: input?.sourceType, + limit: input?.limit || 50, + }); + + return reviews.map((review) => { + let payload: Record | null = null; + try { + payload = review.payload ? JSON.parse(review.payload) : null; + } catch { + payload = null; + } + + return { + ...review, + candidates: Array.isArray(payload?.candidates) ? payload?.candidates : [], + reason: typeof payload?.reason === "string" ? payload.reason : "", + proposedTelephone: typeof payload?.proposedTelephone === "string" ? payload.proposedTelephone : "", + proposedNomRepresentant: typeof payload?.proposedNomRepresentant === "string" ? payload.proposedNomRepresentant : "", + }; + }); + }), + + getPendingUpdateProposal: adminProcedure + .input(z.object({ + directoryEntryId: z.number(), + sourceType: z.enum(["official_registry", "helloasso"]).optional(), + })) + .query(async ({ input }) => { + const proposal = await db.getLatestPendingAssociationDirectoryUpdateProposalByEntryId( + input.directoryEntryId, + input.sourceType + ); + + if (!proposal) return null; + + let payload: Record | null = null; + try { + payload = proposal.payload ? JSON.parse(proposal.payload) : null; + } catch { + payload = null; + } + + return { + ...proposal, + updates: payload?.updates ?? {}, + changes: Array.isArray(payload?.changes) ? payload.changes : [], + }; + }), + + getById: adminProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + const details = await db.getAssociationDirectoryEntryDetails(input.id); + if (!details) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + + return details; + }), + + createManual: adminProcedure + .input(associationDirectoryManualInputSchema) + .mutation(async ({ ctx, input }) => { + validateSocialUrl("facebookUrl", input.facebookUrl); + validateSocialUrl("instagramUrl", input.instagramUrl); + + const entryId = await db.createAssociationDirectoryEntry({ + nomAssociation: input.nomAssociation, + emailOfficiel: input.emailOfficiel || null, + emailOfficielNormalise: input.emailOfficiel ? normalizeEmail(input.emailOfficiel) : null, + siret: normalizeSiretValue(input.siret), + rna: normalizeRnaValue(input.rna), + thematique: serializeAssociationThematics(input.thematiques), + adresse: input.adresse || null, + codePostal: input.codePostal || null, + ville: input.ville || null, + telephone: input.telephone || null, + siteWeb: input.siteWeb || null, + facebookUrl: normalizeSocialUrl(input.facebookUrl) || null, + instagramUrl: normalizeSocialUrl(input.instagramUrl) || null, + dateCreation: input.dateCreation ? new Date(input.dateCreation) : null, + objetAssociation: input.objetAssociation || null, + statutJuridique: input.statutJuridique || "association_loi_1901", + nomRepresentant: input.nomRepresentant || null, + fonctionRepresentant: input.fonctionRepresentant || null, + sourceFileName: "saisie_manuelle_admin", + sourceRowNumber: null, + sourceFingerprint: `manual:${Date.now()}:${normalizeEmail(ctx.user.email || String(ctx.user.id))}`, + isActive: input.isActive ?? true, + importedAt: new Date(), + externalSourceStatus: "manual_entry", + externalSourceLabel: "Saisie manuelle admin", + }); + + if (input.reviewId) { + const review = await db.getAssociationDirectoryReviewById(input.reviewId); + if (review) { + await db.updateAssociationDirectoryReview(input.reviewId, { + status: "created", + resolvedDirectoryEntryId: entryId, + resolutionNote: `Fiche créée manuellement par ${ctx.user.email || ctx.user.name || "admin"}.`, + }); + if (review.userId) { + await ensureAssociationLinkedToDirectoryEntry(review.userId, entryId); + } + } + } + + await logAdminAction(ctx.user.id, "create_manual_directory_entry", "association_directory", entryId, { + reviewId: input.reviewId || null, + nomAssociation: input.nomAssociation, + }); + + return { success: true, id: entryId } as const; + }), + + updateManual: adminProcedure + .input(associationDirectoryManualInputSchema.extend({ + id: z.number(), + })) + .mutation(async ({ ctx, input }) => { + validateSocialUrl("facebookUrl", input.facebookUrl); + validateSocialUrl("instagramUrl", input.instagramUrl); + + const existing = await db.getAssociationDirectoryEntryById(input.id); + if (!existing) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + + await db.updateAssociationDirectoryEntry(input.id, { + nomAssociation: input.nomAssociation, + emailOfficiel: input.emailOfficiel || null, + emailOfficielNormalise: input.emailOfficiel ? normalizeEmail(input.emailOfficiel) : null, + siret: normalizeSiretValue(input.siret), + rna: normalizeRnaValue(input.rna), + thematique: serializeAssociationThematics(input.thematiques), + adresse: input.adresse || null, + codePostal: input.codePostal || null, + ville: input.ville || null, + telephone: input.telephone || null, + siteWeb: input.siteWeb || null, + facebookUrl: normalizeSocialUrl(input.facebookUrl) || null, + instagramUrl: normalizeSocialUrl(input.instagramUrl) || null, + dateCreation: input.dateCreation ? new Date(input.dateCreation) : null, + objetAssociation: input.objetAssociation || null, + statutJuridique: input.statutJuridique || existing.statutJuridique || "association_loi_1901", + nomRepresentant: input.nomRepresentant || null, + fonctionRepresentant: input.fonctionRepresentant || null, + isActive: input.isActive ?? existing.isActive, + externalSourceStatus: "manual_entry_updated", + externalSourceLabel: "Fiche bordereau modifiée dans l'admin", + }); + + await logAdminAction(ctx.user.id, "update_manual_directory_entry", "association_directory", input.id, { + nomAssociation: input.nomAssociation, + }); + + return { success: true } as const; + }), + + listLatest: adminProcedure + .input(z.object({ + search: z.string().optional(), + registrationStatus: z.enum(["all", "registered", "unregistered"]).optional(), + commune: z.enum(associationCommuneOptions.map(option => option.value) as [string, ...string[]]).optional(), + thematique: z.enum(["all", ...associationThematicValues] as [string, ...string[]]).optional(), + limit: z.number().min(1).max(200).optional(), + }).optional()) + .query(async ({ input }) => { + const result = await db.listAssociationDirectoryEntriesWithStatus({ + search: input?.search, + registrationStatus: input?.registrationStatus, + commune: input?.commune as any, + thematique: input?.thematique as any, + limit: input?.limit || 50, + }); + + const invitationSummaries = await db.getAssociationInvitationSummariesForDirectoryEntryIds( + result.data.map(entry => entry.id) + ); + + return { + total: result.total, + data: result.data.map((entry) => ({ + ...entry, + thematiques: parseAssociationThematics(entry.thematique), + invitationStatus: invitationSummaries[entry.id], + })), + }; + }), + + listPortal: publicProcedure + .input(z.object({ + search: z.string().optional(), + registrationStatus: z.enum(["all", "registered", "unregistered"]).optional(), + commune: z.enum(associationCommuneOptions.map(option => option.value) as [string, ...string[]]).optional(), + thematique: z.enum(["all", ...associationThematicValues] as [string, ...string[]]).optional(), + limit: z.number().min(1).max(200).optional(), + }).optional()) + .query(async ({ input }) => { + const result = await db.listAssociationDirectoryEntriesWithStatus({ + search: input?.search, + registrationStatus: input?.registrationStatus, + commune: input?.commune as any, + thematique: input?.thematique as any, + limit: input?.limit || 50, + }); + const invitationSummaries = await db.getAssociationInvitationSummariesForDirectoryEntryIds( + result.data.map(entry => entry.id) + ); + + return { + total: result.total, + data: result.data.map((entry) => ({ + id: entry.id, + nomAssociation: entry.nomAssociation, + emailOfficiel: entry.emailOfficiel, + siret: entry.siret, + rna: entry.rna, + thematiques: parseAssociationThematics(entry.thematique), + adresse: entry.adresse, + codePostal: entry.codePostal, + ville: entry.ville, + geoSource: entry.geoSource, + geoPrecision: entry.geoPrecision, + siteWeb: entry.siteWeb, + facebookUrl: entry.facebookUrl, + instagramUrl: entry.instagramUrl, + importedAt: entry.importedAt, + registered: entry.registered, + registeredAt: entry.registeredAt, + invitationStatus: invitationSummaries[entry.id], + })), + }; + }), + + listMap: publicProcedure + .input(z.object({ + search: z.string().optional(), + registrationStatus: z.enum(["all", "registered", "unregistered"]).optional(), + commune: z.enum(associationCommuneOptions.map(option => option.value) as [string, ...string[]]).optional(), + thematique: z.enum(["all", ...associationThematicValues] as [string, ...string[]]).optional(), + limit: z.number().min(1).max(1000).optional(), + }).optional()) + .query(async ({ input }) => { + const result = await db.listAssociationDirectoryMapEntries({ + search: input?.search, + registrationStatus: input?.registrationStatus, + commune: input?.commune as any, + thematique: input?.thematique as any, + limit: input?.limit || 500, + }); + + return { + total: result.total, + data: result.data.map((entry: any) => ({ + ...entry, + thematiques: parseAssociationThematics(entry.thematique), + })), + }; + }), + + getMapEntry: publicProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + const details = await db.getAssociationDirectoryEntryDetails(input.id); + if (!details) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association introuvable" }); + } + + const payload = buildPublicAssociationDirectoryPayload(details); + if (!payload) { + throw new TRPCError({ code: "FORBIDDEN", message: "Cette association n'est pas visible sur la carte publique" }); + } + + return { + ...payload, + sourceLabel: details.entry.externalSourceLabel, + geoSource: details.entry.geoSource, + }; + }), + + setManualCoordinates: adminProcedure + .input(z.object({ + id: z.number(), + latitude: z.number().min(-90).max(90).nullable(), + longitude: z.number().min(-180).max(180).nullable(), + geoPrecision: z.enum(["exact_address", "commune_center", "hidden"]), + })) + .mutation(async ({ ctx, input }) => { + const details = await db.getAssociationDirectoryEntryDetails(input.id); + if (!details) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + + const hasCoordinates = input.latitude !== null && input.longitude !== null; + await db.updateAssociationDirectoryEntry(input.id, { + latitude: hasCoordinates && input.latitude !== null ? input.latitude.toFixed(6) : null, + longitude: hasCoordinates && input.longitude !== null ? input.longitude.toFixed(6) : null, + geoPrecision: input.geoPrecision, + geoSource: hasCoordinates ? "manual" : null, + geoLastSyncedAt: new Date(), + externalSourceStatus: hasCoordinates ? "manual_coordinates" : "manual_coordinates_cleared", + externalSourceLabel: hasCoordinates ? "Coordonnées définies manuellement" : null, + }); + + await logAdminAction(ctx.user.id, "set_manual_geo", "association_directory", input.id, { + latitude: input.latitude, + longitude: input.longitude, + geoPrecision: input.geoPrecision, + }); + + return { success: true }; + }), + + syncExternalData: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const details = await db.getAssociationDirectoryEntryDetails(input.id); + if (!details) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + + const updates = await computeAssociationDirectoryGeoUpdate(details.entry, details.association); + await db.updateAssociationDirectoryEntry(input.id, updates); + await logAdminAction(ctx.user.id, "sync_geo", "association_directory", input.id, { + externalSourceStatus: updates.externalSourceStatus, + geoSource: updates.geoSource, + }); + + return { + success: true, + externalSourceStatus: updates.externalSourceStatus, + geoSource: updates.geoSource, + }; + }), + + syncExternalDataBatch: adminProcedure + .input(z.object({ + commune: z.enum(associationCommuneOptions.map(option => option.value) as [string, ...string[]]).optional(), + limit: z.number().min(1).max(500).optional(), + }).optional()) + .mutation(async ({ ctx, input }) => { + const result = await db.listAssociationDirectoryEntriesWithStatus({ + commune: input?.commune as any, + limit: input?.limit || 200, + registrationStatus: "all", + }); + + let updated = 0; + let unresolved = 0; + + for (const entry of result.data) { + const details = await db.getAssociationDirectoryEntryDetails(entry.id); + if (!details) continue; + + const updates = await computeAssociationDirectoryGeoUpdate(details.entry, details.association); + await db.updateAssociationDirectoryEntry(entry.id, updates); + if (updates.latitude && updates.longitude) { + updated += 1; + } else { + unresolved += 1; + } + } + + await logAdminAction(ctx.user.id, "sync_geo_batch", "association_directory", undefined, { + commune: input?.commune || "all", + updated, + unresolved, + total: result.data.length, + }); + + return { + success: true, + total: result.data.length, + updated, + unresolved, + }; + }), + + syncReferenceData: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const details = await db.getAssociationDirectoryEntryDetails(input.id); + if (!details) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + + const proposal = await computeAssociationDirectoryReferenceProposal(details.entry, details.association); + + if (!proposal.hasChanges) { + await logAdminAction(ctx.user.id, "scan_reference_data", "association_directory", input.id, { + hasChanges: false, + sourceLabel: proposal.sourceLabel, + summary: proposal.summary, + }); + + return { + success: true, + hasChanges: false, + proposalId: null, + summary: proposal.summary, + sourceLabel: proposal.sourceLabel, + changes: [], + }; + } + + const existingPending = await db.getLatestPendingAssociationDirectoryUpdateProposalByEntryId( + input.id, + "official_registry" + ); + + const payload = JSON.stringify({ + updates: proposal.updates, + changes: proposal.changes, + }); + + let proposalId = existingPending?.id || null; + if (existingPending) { + await db.updateAssociationDirectoryUpdateProposal(existingPending.id, { + sourceLabel: proposal.sourceLabel, + summary: proposal.summary, + payload, + status: "pending", + dismissedAt: null, + appliedAt: null, + }); + } else { + proposalId = await db.createAssociationDirectoryUpdateProposal({ + directoryEntryId: input.id, + sourceType: "official_registry", + sourceLabel: proposal.sourceLabel, + summary: proposal.summary, + payload, + status: "pending", + }); + } + + await logAdminAction(ctx.user.id, "sync_reference_data", "association_directory", input.id, { + hasChanges: true, + proposalId, + sourceLabel: proposal.sourceLabel, + changedFields: proposal.changes.map((change) => change.field), + }); + + return { + success: true, + hasChanges: true, + proposalId, + summary: proposal.summary, + sourceLabel: proposal.sourceLabel, + changes: proposal.changes, + }; + }), + + syncReferenceDataBatch: adminProcedure + .input(z.object({ + search: z.string().optional(), + registrationStatus: z.enum(["all", "registered", "unregistered"]).optional(), + commune: z.enum(associationCommuneOptions.map(option => option.value) as [string, ...string[]]).optional(), + thematique: z.enum(["all", ...associationThematicValues] as [string, ...string[]]).optional(), + limit: z.number().min(1).max(200).optional(), + }).optional()) + .mutation(async ({ ctx, input }) => { + const result = await db.listAssociationDirectoryEntriesWithStatus({ + search: input?.search, + registrationStatus: input?.registrationStatus, + commune: input?.commune as any, + thematique: input?.thematique as any, + limit: input?.limit || 50, + }); + + let proposalsCreated = 0; + let unchanged = 0; + let failed = 0; + + for (const row of result.data) { + try { + const details = await db.getAssociationDirectoryEntryDetails(row.id); + if (!details) continue; + + const proposal = await computeAssociationDirectoryReferenceProposal(details.entry, details.association); + if (!proposal.hasChanges) { + unchanged += 1; + continue; + } + + const existingPending = await db.getLatestPendingAssociationDirectoryUpdateProposalByEntryId( + row.id, + "official_registry" + ); + const payload = JSON.stringify({ + updates: proposal.updates, + changes: proposal.changes, + }); + + if (existingPending) { + await db.updateAssociationDirectoryUpdateProposal(existingPending.id, { + sourceLabel: proposal.sourceLabel, + summary: proposal.summary, + payload, + status: "pending", + dismissedAt: null, + appliedAt: null, + }); + } else { + await db.createAssociationDirectoryUpdateProposal({ + directoryEntryId: row.id, + sourceType: "official_registry", + sourceLabel: proposal.sourceLabel, + summary: proposal.summary, + payload, + status: "pending", + }); + } + + proposalsCreated += 1; + } catch { + failed += 1; + } + } + + await logAdminAction(ctx.user.id, "sync_reference_data_batch", "association_directory", undefined, { + search: input?.search || "", + registrationStatus: input?.registrationStatus || "all", + commune: input?.commune || "all", + thematique: input?.thematique || "all", + total: result.data.length, + proposalsCreated, + unchanged, + failed, + }); + + return { + success: true, + total: result.data.length, + proposalsCreated, + unchanged, + failed, + }; + }), + + applyUpdateProposal: adminProcedure + .input(z.object({ proposalId: z.number() })) + .mutation(async ({ ctx, input }) => { + const proposal = await db.getAssociationDirectoryUpdateProposalById(input.proposalId); + if (!proposal) { + throw new TRPCError({ code: "NOT_FOUND", message: "Proposition d’enrichissement introuvable" }); + } + if (proposal.status !== "pending") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette proposition a déjà été traitée" }); + } + + let payload: Record | null = null; + try { + payload = proposal.payload ? JSON.parse(proposal.payload) : null; + } catch { + payload = null; + } + + const updates = payload?.updates; + if (!updates || typeof updates !== "object") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le contenu de la proposition est invalide" }); + } + + await db.updateAssociationDirectoryEntry(proposal.directoryEntryId, updates); + await db.updateAssociationDirectoryUpdateProposal(proposal.id, { + status: "applied", + appliedAt: new Date(), + }); + + await logAdminAction(ctx.user.id, "apply_directory_update_proposal", "association_directory", proposal.directoryEntryId, { + proposalId: proposal.id, + sourceType: proposal.sourceType, + sourceLabel: proposal.sourceLabel, + }); + + return { success: true } as const; + }), + + dismissUpdateProposal: adminProcedure + .input(z.object({ proposalId: z.number() })) + .mutation(async ({ ctx, input }) => { + const proposal = await db.getAssociationDirectoryUpdateProposalById(input.proposalId); + if (!proposal) { + throw new TRPCError({ code: "NOT_FOUND", message: "Proposition d’enrichissement introuvable" }); + } + if (proposal.status !== "pending") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette proposition a déjà été traitée" }); + } + + await db.updateAssociationDirectoryUpdateProposal(proposal.id, { + status: "dismissed", + dismissedAt: new Date(), + }); + + await logAdminAction(ctx.user.id, "dismiss_directory_update_proposal", "association_directory", proposal.directoryEntryId, { + proposalId: proposal.id, + sourceType: proposal.sourceType, + sourceLabel: proposal.sourceLabel, + }); + + return { success: true } as const; + }), + + syncHelloAssoById: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const details = await db.getAssociationDirectoryEntryDetails(input.id); + if (!details) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + + const settings = await getHelloAssoSettings(); + const result = await computeAssociationDirectoryHelloAssoUpdate(details.entry, details.association, settings); + await db.updateAssociationDirectoryEntry(input.id, result.updates); + + await logAdminAction(ctx.user.id, "sync_helloasso", "association_directory", input.id, { + matched: result.matched, + reason: result.reason, + slug: result.slug, + candidateCount: result.candidateCount, + externalSourceStatus: result.updates.externalSourceStatus, + }); + + return { + success: true, + ...result, + }; + }), + + syncHelloAssoBatch: adminProcedure + .input(z.object({ + search: z.string().optional(), + registrationStatus: z.enum(["all", "registered", "unregistered"]).optional(), + commune: z.enum(associationCommuneOptions.map(option => option.value) as [string, ...string[]]).optional(), + thematique: z.enum(["all", ...associationThematicValues] as [string, ...string[]]).optional(), + limit: z.number().min(1).max(200).optional(), + }).optional()) + .mutation(async ({ ctx, input }) => { + const settings = await getHelloAssoSettings(); + const client = new HelloAssoSyncClient(settings); + const result = await db.listAssociationDirectoryEntriesWithStatus({ + search: input?.search, + registrationStatus: input?.registrationStatus, + commune: input?.commune as any, + thematique: input?.thematique as any, + limit: input?.limit || 50, + }); + + let updated = 0; + let skipped = 0; + let matched = 0; + + for (const entry of result.data) { + const details = await db.getAssociationDirectoryEntryDetails(entry.id); + if (!details) continue; + + const syncResult = await computeAssociationDirectoryHelloAssoUpdate( + details.entry, + details.association, + settings, + client + ); + await db.updateAssociationDirectoryEntry(entry.id, syncResult.updates); + if (syncResult.matched) { + matched += 1; + updated += 1; + } else { + skipped += 1; + } + } + + await logAdminAction(ctx.user.id, "sync_helloasso_batch", "association_directory", undefined, { + search: input?.search || "", + registrationStatus: input?.registrationStatus || "all", + commune: input?.commune || "all", + thematique: input?.thematique || "all", + total: result.data.length, + updated, + skipped, + }); + + return { + success: true, + total: result.data.length, + updated, + matched, + skipped, + }; + }), + + resolveReviewLink: adminProcedure + .input(z.object({ + reviewId: z.number(), + directoryEntryId: z.number(), + })) + .mutation(async ({ ctx, input }) => { + const [review, entry] = await Promise.all([ + db.getAssociationDirectoryReviewById(input.reviewId), + db.getAssociationDirectoryEntryById(input.directoryEntryId), + ]); + + if (!review) { + throw new TRPCError({ code: "NOT_FOUND", message: "Correspondance à valider introuvable" }); + } + if (!entry) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + + await db.updateAssociationDirectoryReview(input.reviewId, { + status: "linked", + resolvedDirectoryEntryId: input.directoryEntryId, + resolutionNote: `Rapprochement validé manuellement par ${ctx.user.email || ctx.user.name || "admin"}.`, + }); + + if (review.userId) { + await ensureAssociationLinkedToDirectoryEntry(review.userId, input.directoryEntryId); + } + + await logAdminAction(ctx.user.id, "resolve_directory_review_link", "association_directory_review", input.reviewId, { + directoryEntryId: input.directoryEntryId, + }); + + return { success: true } as const; + }), + + ignoreReview: adminProcedure + .input(z.object({ + reviewId: z.number(), + resolutionNote: z.string().optional(), + })) + .mutation(async ({ ctx, input }) => { + const review = await db.getAssociationDirectoryReviewById(input.reviewId); + if (!review) { + throw new TRPCError({ code: "NOT_FOUND", message: "Correspondance à valider introuvable" }); + } + + await db.updateAssociationDirectoryReview(input.reviewId, { + status: "ignored", + resolutionNote: input.resolutionNote || `Revue ignorée par ${ctx.user.email || ctx.user.name || "admin"}.`, + }); + + await logAdminAction(ctx.user.id, "ignore_directory_review", "association_directory_review", input.reviewId); + return { success: true } as const; + }), + + previewImport: superAdminProcedure + .input(z.object({ + fileData: z.string(), + fileName: z.string(), + mimeType: z.string(), + })) + .mutation(async ({ input }) => { + const fileBuffer = Buffer.from(input.fileData, "base64"); + const preview = parseAssociationDirectoryWorkbook(fileBuffer, input.fileName); + return { + fileName: preview.fileName, + totalRows: preview.totalRows, + validRows: preview.validRows, + missingEmailRows: preview.missingEmailRows, + duplicateEmailRows: preview.duplicateEmailRows, + duplicateGroups: preview.duplicateGroups, + previewRows: preview.previewRows.slice(0, 100), + }; + }), + + importWorkbook: superAdminProcedure + .input(z.object({ + fileData: z.string(), + fileName: z.string(), + mimeType: z.string(), + duplicateSelections: z.record(z.string(), z.number()).optional(), + })) + .mutation(async ({ ctx, input }) => { + const fileBuffer = Buffer.from(input.fileData, "base64"); + const preview = parseAssociationDirectoryWorkbook(fileBuffer, input.fileName); + const unresolvedDuplicateGroups = preview.duplicateGroups.filter( + group => !input.duplicateSelections?.[group.emailOfficielNormalise] + ); + + if (unresolvedDuplicateGroups.length > 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Choisis une ligne à conserver pour chaque email dupliqué avant de lancer l'import", + }); + } + + const rowsToImport = resolveAssociationDirectoryImportRows(preview, input.duplicateSelections); + + let created = 0; + let updated = 0; + + for (const row of rowsToImport) { + const result = await db.upsertAssociationDirectoryEntry({ + nomAssociation: row.nomAssociation, + emailOfficiel: row.emailOfficiel, + emailOfficielNormalise: row.emailOfficielNormalise, + siret: row.siret, + rna: row.rna, + adresse: row.adresse, + codePostal: row.codePostal, + ville: row.ville, + telephone: row.telephone, + siteWeb: row.siteWeb, + facebookUrl: row.facebookUrl, + instagramUrl: row.instagramUrl, + dateCreation: row.dateCreation, + objetAssociation: row.objetAssociation, + statutJuridique: row.statutJuridique, + nomRepresentant: row.nomRepresentant, + fonctionRepresentant: row.fonctionRepresentant, + sourceFileName: input.fileName, + sourceRowNumber: row.rowNumber, + sourceFingerprint: row.sourceFingerprint, + isActive: true, + importedAt: new Date(), + }); + + if (result === "created") created += 1; + if (result === "updated") updated += 1; + } + + await logAdminAction(ctx.user.id, "import", "association_directory", undefined, { + fileName: input.fileName, + totalRows: preview.totalRows, + validRows: preview.validRows, + missingEmailRows: preview.missingEmailRows, + duplicateEmailRows: preview.duplicateEmailRows, + duplicateSelections: input.duplicateSelections || {}, + created, + updated, + }); + + return { + fileName: input.fileName, + totalRows: preview.totalRows, + validRows: preview.validRows, + missingEmailRows: preview.missingEmailRows, + duplicateEmailRows: preview.duplicateEmailRows, + importedRows: rowsToImport.length, + created, + updated, + }; + }), + }), + + associationInvitation: router({ + getPublic: publicProcedure + .input(z.object({ + token: z.string().min(1), + })) + .query(async ({ input }) => { + const invitation = await db.getAssociationInvitationByToken(input.token); + if (!invitation) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invitation introuvable" }); + } + + const directoryEntry = await db.getAssociationDirectoryEntryById(invitation.directoryEntryId); + if (!directoryEntry) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + + const existingAssociation = await db.getAssociationBySourceDirectoryEntryId(invitation.directoryEntryId); + const invalidReason = getInvitationStatusReason(invitation); + + return { + token: invitation.token, + valid: !invalidReason && !existingAssociation, + invalidReason: existingAssociation ? "already_registered" : invalidReason, + association: { + id: directoryEntry.id, + nomAssociation: directoryEntry.nomAssociation, + emailOfficiel: directoryEntry.emailOfficiel, + nomRepresentant: directoryEntry.nomRepresentant, + ville: directoryEntry.ville, + }, + expiresAt: invitation.expiresAt, + }; + }), + + send: adminProcedure + .input(z.object({ + directoryEntryId: z.number(), + })) + .mutation(async ({ ctx, input }) => { + const directoryEntry = await db.getAssociationDirectoryEntryById(input.directoryEntryId); + if (!directoryEntry) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + if (!directoryEntry.emailOfficiel || !directoryEntry.emailOfficielNormalise) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Aucun email officiel n'est renseigné pour cette association" }); + } + + const existingAssociation = await db.getAssociationBySourceDirectoryEntryId(directoryEntry.id); + if (existingAssociation) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette association a déjà activé son espace" }); + } + + await db.revokeAssociationInvitationsByDirectoryEntryId(directoryEntry.id); + + const token = randomBytes(32).toString("hex"); + const expiresAt = getInvitationExpiry(); + const invitationLink = buildAssociationInvitationLink(ctx.req, token); + const canSend = await canSendOperationalEmails(); + let emailSent = false; + + if (canSend) { + try { + const email = generateAssociationInvitationEmail({ + associationName: directoryEntry.nomAssociation, + invitationLink, + expiresAt, + }); + const delivery = await sendOperationalEmail({ + to: [directoryEntry.emailOfficiel], + subject: email.subject, + text: email.text, + html: email.html, + }); + emailSent = delivery.sent; + } catch (error) { + console.error("[AssociationInvitation] SMTP send failed:", error); + emailSent = false; + } + } + + await db.createAssociationInvitation({ + directoryEntryId: directoryEntry.id, + emailOfficiel: directoryEntry.emailOfficiel, + emailOfficielNormalise: directoryEntry.emailOfficielNormalise, + token, + deliveryMode: "email", + emailSent, + sentByUserId: ctx.user.id, + sentAt: new Date(), + expiresAt, + }); + + await logAdminAction(ctx.user.id, "invitation_association", "association_directory", directoryEntry.id, { + emailOfficiel: directoryEntry.emailOfficiel, + invitationLink, + emailSent, + expiresAt, + }); + + return { + invitationLink, + emailSent, + expiresAt, + }; + }), + + getSecureLink: adminProcedure + .input(z.object({ + directoryEntryId: z.number(), + })) + .mutation(async ({ ctx, input }) => { + const directoryEntry = await db.getAssociationDirectoryEntryById(input.directoryEntryId); + if (!directoryEntry) { + throw new TRPCError({ code: "NOT_FOUND", message: "Association du bordereau introuvable" }); + } + if (!directoryEntry.emailOfficiel || !directoryEntry.emailOfficielNormalise) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Aucun email officiel n'est renseigné pour cette association" }); + } + + const existingAssociation = await db.getAssociationBySourceDirectoryEntryId(directoryEntry.id); + if (existingAssociation) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette association a déjà activé son espace" }); + } + + let invitation = await db.getActiveAssociationInvitationByDirectoryEntryId(directoryEntry.id); + if (!invitation) { + const token = randomBytes(32).toString("hex"); + const expiresAt = getInvitationExpiry(); + const invitationId = await db.createAssociationInvitation({ + directoryEntryId: directoryEntry.id, + emailOfficiel: directoryEntry.emailOfficiel, + emailOfficielNormalise: directoryEntry.emailOfficielNormalise, + token, + deliveryMode: "manual_link", + emailSent: false, + sentByUserId: ctx.user.id, + sentAt: new Date(), + expiresAt, + }); + invitation = await db.getAssociationInvitationByToken(token); + await logAdminAction(ctx.user.id, "copie_invitation_association", "association_directory", directoryEntry.id, { + invitationId, + expiresAt, + }); + } + + if (!invitation) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Impossible de générer le lien d'invitation" }); + } + + return { + invitationLink: buildAssociationInvitationLink(ctx.req, invitation.token), + expiresAt: invitation.expiresAt, + }; + }), + + revoke: adminProcedure + .input(z.object({ + directoryEntryId: z.number(), + })) + .mutation(async ({ ctx, input }) => { + await db.revokeAssociationInvitationsByDirectoryEntryId(input.directoryEntryId); + await logAdminAction(ctx.user.id, "revocation_invitation_association", "association_directory", input.directoryEntryId); + return { success: true } as const; + }), + }), + + materialReturn: router({ + listMine: terrainProcedure.query(async ({ ctx }) => { + const currentEmail = normalizeEmail(String(ctx.user.email || "")); + if (!currentEmail) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le compte terrain doit disposer d'une adresse email pour retrouver ses missions", + }); + } + + const logisticsSettings = await getLogisticsSettings(); + const requests = (await db.searchRequests({ + type: "demande_materiel_evenementiel", + status: "validee", + limit: 500, + })).data; + + const entries = await Promise.all( + requests.map(async (request) => { + const association = await db.getAssociationById(request.associationId); + const followup = await db.getMaterialReturnFollowupByRequestId(request.id); + const payload = serializeMaterialReturnFollowup(followup); + if (!payload) return null; + + const isAssignedRecipient = payload.recipientEmails.some((email) => normalizeEmail(email) === currentEmail); + const hasAlreadyWorkedOnMission = normalizeEmail(String(payload.uploadedByEmail || "")) === currentEmail; + if (!isAssignedRecipient && !hasAlreadyWorkedOnMission) return null; + + const formData = (() => { + try { + return request.formData ? JSON.parse(request.formData) : {}; + } catch { + return {}; + } + })(); + + const restitutionDate = payload.restitutionDate || formData.dateRestitution || null; + const boardState = computeMaterialReturnBoardState({ + requestStatus: request.status, + restitutionDate, + followup, + graceDays: logisticsSettings.materialReturnGraceDays, + }); + + return { + requestId: request.id, + title: request.titre, + associationName: association?.nomAssociation || `Association #${request.associationId}`, + commune: formData.commune || association?.ville || "", + pickupDate: formData.datePriseEnCharge || formData.dateDebutManifestation || formData.dateManifestation || null, + restitutionDate, + boardState, + boardStateLabel: materialReturnBoardStateLabels[boardState], + serviceLabel: payload.serviceLabel || "", + plannedSendAt: payload.plannedSendAt || null, + sentAt: payload.sentAt || null, + uploadedAt: payload.uploadedAt || null, + validatedAt: payload.validatedAt || null, + closedAt: payload.closedAt || null, + issueFlag: Boolean(payload.issueFlag), + compliance: payload.compliance || null, + finalPdfUrl: payload.finalPdfUrl || payload.signedFileUrl || null, + finalPdfName: payload.finalPdfName || payload.signedFileName || null, + uploadLink: payload.uploadToken + ? buildMaterialReturnUploadLink(buildAppBaseUrl(ctx.req), payload.uploadToken) + : null, + uploadLinkState: getMaterialReturnUploadLinkState({ + status: payload.status, + uploadTokenExpiresAt: payload.uploadTokenExpiresAt || null, + }), + uploadTokenExpiresAt: payload.uploadTokenExpiresAt || null, + lastReminderSentAt: payload.lastReminderSentAt || null, + uploadedByName: payload.uploadedByName || "", + uploadedByEmail: payload.uploadedByEmail || "", + }; + }) + ); + + const boardOrder = { + terrain: 0, + retard: 1, + planifie: 2, + litige: 3, + conforme: 4, + a_attribuer: 5, + } as const; + + return entries + .filter((entry): entry is NonNullable => Boolean(entry)) + .sort((left, right) => { + const orderDiff = boardOrder[left.boardState] - boardOrder[right.boardState]; + if (orderDiff !== 0) return orderDiff; + const leftDate = left.restitutionDate ? new Date(left.restitutionDate).getTime() : 0; + const rightDate = right.restitutionDate ? new Date(right.restitutionDate).getTime() : 0; + return leftDate - rightDate; + }); + }), + + getByRequestId: logisticsProcedure + .input(z.object({ requestId: z.number() })) + .query(async ({ input, ctx }) => { + const logisticsSettings = await getLogisticsSettings(); + const followup = await db.getMaterialReturnFollowupByRequestId(input.requestId); + const payload = serializeMaterialReturnFollowup(followup); + if (!payload) return null; + const request = await db.getRequestById(input.requestId); + const reactivatedByUser = payload.reactivatedByUserId + ? await db.getUserById(payload.reactivatedByUserId) + : null; + const boardState = computeMaterialReturnBoardState({ + requestStatus: request?.status, + restitutionDate: payload.restitutionDate, + followup, + graceDays: logisticsSettings.materialReturnGraceDays, + }); + return { + ...payload, + boardState, + boardStateLabel: materialReturnBoardStateLabels[boardState], + uploadLink: payload.uploadToken + ? buildMaterialReturnUploadLink(buildAppBaseUrl(ctx.req), payload.uploadToken) + : null, + uploadLinkState: getMaterialReturnUploadLinkState({ + status: payload.status, + uploadTokenExpiresAt: payload.uploadTokenExpiresAt, + }), + reactivatedByName: reactivatedByUser?.name || null, + }; + }), + + listAdminBoard: logisticsProcedure + .query(async ({ ctx }) => { + const logisticsSettings = await getLogisticsSettings(); + const requests = (await db.searchRequests({ + type: "demande_materiel_evenementiel", + status: "validee", + limit: 500, + })).data; + + const entries = await Promise.all( + requests.map(async (request) => { + const association = await db.getAssociationById(request.associationId); + const followup = await db.getMaterialReturnFollowupByRequestId(request.id); + const payload = serializeMaterialReturnFollowup(followup); + const formData = (() => { + try { + return request.formData ? JSON.parse(request.formData) : {}; + } catch { + return {}; + } + })(); + + const restitutionDate = payload?.restitutionDate || formData.dateRestitution || null; + const boardState = computeMaterialReturnBoardState({ + requestStatus: request.status, + restitutionDate, + followup, + graceDays: logisticsSettings.materialReturnGraceDays, + }); + + return { + requestId: request.id, + title: request.titre, + status: request.status, + associationId: request.associationId, + associationName: association?.nomAssociation || `Association #${request.associationId}`, + commune: formData.commune || association?.ville || "", + pickupDate: formData.datePriseEnCharge || formData.dateDebutManifestation || formData.dateManifestation || null, + restitutionDate, + boardState, + boardStateLabel: materialReturnBoardStateLabels[boardState], + serviceLabel: payload?.serviceLabel || "", + recipientEmails: payload?.recipientEmails || [], + supervisionServiceLabel: payload?.supervisionServiceLabel || "", + supervisionRecipientEmails: payload?.supervisionRecipientEmails || [], + plannedSendAt: payload?.plannedSendAt || null, + sentAt: payload?.sentAt || null, + uploadedAt: payload?.uploadedAt || null, + validatedAt: payload?.validatedAt || null, + closedAt: payload?.closedAt || null, + issueFlag: Boolean(payload?.issueFlag), + compliance: payload?.compliance || null, + finalPdfUrl: payload?.finalPdfUrl || payload?.signedFileUrl || null, + uploadLink: payload?.uploadToken + ? buildMaterialReturnUploadLink(buildAppBaseUrl(ctx.req), payload.uploadToken) + : null, + uploadLinkState: getMaterialReturnUploadLinkState({ + status: payload?.status, + uploadTokenExpiresAt: payload?.uploadTokenExpiresAt || null, + }), + uploadTokenExpiresAt: payload?.uploadTokenExpiresAt || null, + reactivatedAt: payload?.reactivatedAt || null, + lastReminderSentAt: payload?.lastReminderSentAt || null, + }; + }) + ); + + const boardOrder = { + a_attribuer: 0, + planifie: 1, + terrain: 2, + retard: 3, + litige: 4, + conforme: 5, + } as const; + + return entries.sort((a, b) => { + const orderDiff = boardOrder[a.boardState] - boardOrder[b.boardState]; + if (orderDiff !== 0) return orderDiff; + const dateA = a.restitutionDate ? new Date(a.restitutionDate).getTime() : 0; + const dateB = b.restitutionDate ? new Date(b.restitutionDate).getTime() : 0; + return dateA - dateB; + }); + }), + + assign: logisticsProcedure + .input( + z.object({ + requestId: z.number(), + recapServiceId: z.number().optional(), + recapServiceLabel: z.string().optional(), + recapRecipients: z.array(z.string()).optional(), + supervisionServiceId: z.number().optional(), + supervisionServiceLabel: z.string().optional(), + supervisionRecipients: z.array(z.string()).optional(), + }) + ) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.requestId); + if (!request || request.type !== "demande_materiel_evenementiel") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de matériel introuvable" }); + } + if (request.status !== "validee") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Seules les demandes validées peuvent être assignées à la logistique" }); + } + const existingFollowup = await db.getMaterialReturnFollowupByRequestId(request.id); + if (existingFollowup?.status === "cloture") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette restitution est déjà clôturée" }); + } + + const selectedRecapService = input.recapServiceId + ? await db.getOperationalRecapServiceById(input.recapServiceId) + : undefined; + const selectedSupervisionService = input.supervisionServiceId + ? await db.getOperationalRecapServiceById(input.supervisionServiceId) + : undefined; + const resolvedRecipients = mergeRecipients( + parseStoredRecipientEmails(selectedRecapService?.recipientEmails), + parseRecipients(input.recapRecipients) + ); + const resolvedServiceLabel = input.recapServiceLabel?.trim() || selectedRecapService?.label || undefined; + const resolvedSupervisionRecipients = mergeRecipients( + parseStoredRecipientEmails(selectedSupervisionService?.recipientEmails), + parseRecipients(input.supervisionRecipients) + ); + const resolvedSupervisionServiceLabel = + input.supervisionServiceLabel?.trim() + || selectedSupervisionService?.label + || undefined; + + if (resolvedRecipients.length === 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Sélectionne un service ou ajoute au moins une adresse email de récupération", + }); + } + + let updatedFormData = request.formData; + try { + const existingFormData = request.formData ? JSON.parse(request.formData) : {}; + existingFormData.notificationTrace = { + serviceId: selectedRecapService?.id, + serviceLabel: resolvedServiceLabel, + recipients: resolvedRecipients, + supervisionServiceId: selectedSupervisionService?.id, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + supervisionRecipients: resolvedSupervisionRecipients, + processedBy: ctx.user.name || `Admin #${ctx.user.id}`, + processedAt: new Date().toISOString(), + channel: "smtp", + }; + updatedFormData = JSON.stringify(existingFormData); + await db.updateRequest(request.id, { formData: updatedFormData }); + } catch (error) { + console.error("Failed to persist logistics assignment trace:", error); + } + + const scheduledFollowup = await scheduleMaterialReturnFollowup({ + request: { + ...request, + formData: updatedFormData, + }, + serviceLabel: resolvedServiceLabel, + recipientEmails: resolvedRecipients, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + supervisionRecipientEmails: resolvedSupervisionRecipients, + }, { + forceNewToken: Boolean(existingFollowup), + forceImmediateSendWindow: Boolean(existingFollowup), + reactivatedByUserId: existingFollowup ? ctx.user.id : null, + reactivatedAt: existingFollowup ? new Date() : null, + reactivationReason: existingFollowup ? "Assignation logistique mise à jour" : null, + }); + + if (!scheduledFollowup) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Impossible de planifier la restitution : la date de restitution est manquante sur la demande", + }); + } + + const association = await db.getAssociationById(request.associationId); + const mailResult = await sendMaterialReturnFollowupEmail({ + followup: scheduledFollowup, + request: { + ...request, + formData: updatedFormData, + }, + association, + baseUrl: buildAppBaseUrl(ctx.req), + tone: existingFollowup ? "reactivation" : "assignment", + }); + + if (mailResult.sent) { + await db.updateMaterialReturnFollowup(scheduledFollowup.id, { + status: "en_attente", + sentAt: new Date(), + lastReminderSentAt: new Date(), + }); + } + + await db.createRequestHistory({ + requestId: request.id, + action: "ajout_commentaire", + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: existingFollowup + ? `Assignation logistique mise à jour — ${resolvedServiceLabel || resolvedRecipients.join(", ")}` + : `Récupération assignée à ${resolvedServiceLabel || resolvedRecipients.join(", ")}`, + }); + + await logAdminAction(ctx.user.id, "assignation_logistique", "request", request.id, { + recapServiceId: selectedRecapService?.id, + recapRecipients: resolvedRecipients, + recapServiceLabel: resolvedServiceLabel, + supervisionServiceId: selectedSupervisionService?.id, + supervisionRecipients: resolvedSupervisionRecipients, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + emailSent: mailResult.sent, + reason: mailResult.sent ? undefined : mailResult.reason, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true as const, + emailSent: mailResult.sent, + reason: mailResult.sent ? null : mailResult.reason, + }; + }), + + reactivate: logisticsProcedure + .input( + z.object({ + requestId: z.number(), + recapServiceId: z.number().optional(), + recapServiceLabel: z.string().optional(), + recapRecipients: z.array(z.string()).optional(), + supervisionServiceId: z.number().optional(), + supervisionServiceLabel: z.string().optional(), + supervisionRecipients: z.array(z.string()).optional(), + reason: z.string().optional(), + }) + ) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.requestId); + if (!request || request.type !== "demande_materiel_evenementiel") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de matériel introuvable" }); + } + if (request.status !== "validee") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Seules les demandes validées peuvent être réactivées" }); + } + + const existingFollowup = await db.getMaterialReturnFollowupByRequestId(request.id); + if (!existingFollowup) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Aucun suivi de restitution n'existe encore pour cette demande" }); + } + if (existingFollowup.status === "cloture") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette restitution est déjà clôturée" }); + } + if (existingFollowup.litigationStatus === "pending") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le dossier est en litige et ne peut pas être réactivé tant que l'arbitrage n'est pas clôturé" }); + } + + const selectedRecapService = input.recapServiceId + ? await db.getOperationalRecapServiceById(input.recapServiceId) + : undefined; + const selectedSupervisionService = input.supervisionServiceId + ? await db.getOperationalRecapServiceById(input.supervisionServiceId) + : undefined; + const resolvedRecipients = mergeRecipients( + parseStoredRecipientEmails(existingFollowup.recipientEmails), + parseStoredRecipientEmails(selectedRecapService?.recipientEmails), + parseRecipients(input.recapRecipients) + ); + const resolvedServiceLabel = + input.recapServiceLabel?.trim() + || selectedRecapService?.label + || existingFollowup.serviceLabel + || undefined; + const resolvedSupervisionRecipients = mergeRecipients( + parseStoredRecipientEmails(existingFollowup.supervisionRecipientEmails), + parseStoredRecipientEmails(selectedSupervisionService?.recipientEmails), + parseRecipients(input.supervisionRecipients) + ); + const resolvedSupervisionServiceLabel = + input.supervisionServiceLabel?.trim() + || selectedSupervisionService?.label + || existingFollowup.supervisionServiceLabel + || undefined; + + if (resolvedRecipients.length === 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Ajoute au moins une adresse email pour réactiver la restitution", + }); + } + + const reactivatedAt = new Date(); + const reactivationReason = input.reason?.trim() || "Suivi rouvert"; + + const scheduledFollowup = await scheduleMaterialReturnFollowup({ + request, + serviceLabel: resolvedServiceLabel, + recipientEmails: resolvedRecipients, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + supervisionRecipientEmails: resolvedSupervisionRecipients, + }, { + forceNewToken: true, + forceImmediateSendWindow: true, + reactivatedByUserId: ctx.user.id, + reactivatedAt, + reactivationReason, + }); + + if (!scheduledFollowup) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Impossible de réactiver la restitution : la date de restitution est manquante sur la demande", + }); + } + + const association = await db.getAssociationById(request.associationId); + const mailResult = await sendMaterialReturnFollowupEmail({ + followup: scheduledFollowup, + request, + association, + baseUrl: buildAppBaseUrl(ctx.req), + tone: "reactivation", + }); + + if (mailResult.sent) { + await db.updateMaterialReturnFollowup(scheduledFollowup.id, { + status: "en_attente", + sentAt: reactivatedAt, + lastReminderSentAt: reactivatedAt, + }); + } + + await db.createRequestHistory({ + requestId: request.id, + action: "ajout_commentaire", + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: `${reactivationReason} — nouvelle échéance de dépôt : ${formatDateFr(scheduledFollowup.uploadTokenExpiresAt)}`, + }); + + await logAdminAction(ctx.user.id, "reactivation_restitution", "request", request.id, { + recapServiceId: selectedRecapService?.id, + recapRecipients: resolvedRecipients, + recapServiceLabel: resolvedServiceLabel, + supervisionServiceId: selectedSupervisionService?.id, + supervisionRecipients: resolvedSupervisionRecipients, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + reason: reactivationReason, + emailSent: mailResult.sent, + emailFailureReason: mailResult.sent ? undefined : mailResult.reason, + uploadTokenExpiresAt: scheduledFollowup.uploadTokenExpiresAt, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true as const, + emailSent: mailResult.sent, + uploadTokenExpiresAt: scheduledFollowup.uploadTokenExpiresAt, + uploadLink: buildMaterialReturnUploadLink(buildAppBaseUrl(ctx.req), scheduledFollowup.uploadToken), + }; + }), + + getPublic: publicProcedure + .input(z.object({ token: z.string().min(1) })) + .query(async ({ input, ctx }) => { + const followup = await db.getMaterialReturnFollowupByToken(input.token); + if (!followup) { + throw new TRPCError({ code: "NOT_FOUND", message: "Lien de restitution introuvable" }); + } + + const request = await db.getRequestById(followup.requestId); + if (!request || request.type !== "demande_materiel_evenementiel") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande liée introuvable" }); + } + + const association = await db.getAssociationById(followup.associationId); + const payload = serializeMaterialReturnFollowup(followup); + const formData = (() => { + try { + return request.formData ? JSON.parse(request.formData) : {}; + } catch { + return {}; + } + })(); + const requestedItems = Object.entries(formData.materielsDemandes || {}) + .filter(([, checked]) => Boolean(checked)) + .map(([key]) => ({ + key, + label: + key === "tente3x3" + ? "Tente 3x3" + : key === "chapiteau5x5" + ? "Chapiteau 5x5" + : key === "podium" + ? "Podium" + : "Autres", + quantity: String(formData.quantitesDemandees?.[key] || ""), + extra: key === "autres" ? String(formData.autreMaterielPrecisions || "") : "", + })); + + return { + ...payload, + request: { + id: request.id, + titre: request.titre, + commune: formData.commune || association?.ville || "", + manifestationStart: formData.dateDebutManifestation || formData.dateManifestation || "", + manifestationEnd: formData.dateFinManifestation || formData.dateManifestation || "", + requestedItems, + }, + association: association + ? { + nomAssociation: association.nomAssociation, + telephone: association.telephone, + emailContact: association.emailContact, + } + : null, + currentUser: ctx.user + ? { + id: ctx.user.id, + name: ctx.user.name, + email: ctx.user.email, + role: ctx.user.role, + } + : null, + optionSets: { + agentRoles: [...agentRoleOptions], + borrowerRoles: [...borrowerRoleOptions], + discrepancyCategories: materialReturnDiscrepancyOptions, + }, + canUpload: + followup.status !== "cloture" && + new Date() <= new Date(followup.uploadTokenExpiresAt), + uploadDeadlineLabel: formatDateFr(followup.uploadTokenExpiresAt), + uploadLink: buildMaterialReturnUploadLink(buildAppBaseUrl(ctx.req), input.token), + }; + }), + + complete: publicProcedure + .input( + z.object({ + token: z.string().min(1), + agentName: z.string().min(2), + agentEmail: z.string().email().optional().or(z.literal("")), + agentRole: z.enum(agentRoleOptions), + borrowerName: z.string().min(2), + borrowerRole: z.enum(borrowerRoleOptions), + compliance: z.enum(["conforme", "non_conforme"]), + discrepancyCategories: z.array( + z.enum([ + "materiel_manquant", + "accessoires_manquants", + "structure_deformee", + "toile_dechiree", + "choc_important", + "materiel_sale", + "materiel_humide", + ]) + ), + discrepancyDetails: z.string().optional(), + agentSignatureData: z.string().min(1), + borrowerSignatureData: z.string().min(1), + geoLatitude: z.string().optional(), + geoLongitude: z.string().optional(), + geoStatus: z.enum(["available", "unavailable"]).optional(), + geoFailureReason: z.string().optional(), + }) + ) + .mutation(async ({ input, ctx }) => { + const followup = await db.getMaterialReturnFollowupByToken(input.token); + if (!followup) { + throw new TRPCError({ code: "NOT_FOUND", message: "Lien de restitution introuvable" }); + } + if (followup.status === "cloture") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette restitution a déjà été validée" }); + } + if (new Date() > new Date(followup.uploadTokenExpiresAt)) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le délai de validation est dépassé" }); + } + if ((input.compliance === "non_conforme" || input.discrepancyCategories.length > 0) + && !input.discrepancyDetails?.trim()) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Précise les réserves constatées pour une restitution non conforme", + }); + } + + const request = await db.getRequestById(followup.requestId); + if (!request || request.type !== "demande_materiel_evenementiel") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande liée introuvable" }); + } + const association = await db.getAssociationById(followup.associationId); + const requestFormData = (() => { + try { + return request.formData ? JSON.parse(request.formData) : {}; + } catch { + return {}; + } + })(); + const validatedAt = new Date(); + const issueFlag = input.compliance === "non_conforme"; + const blockedItems = issueFlag ? getRequestedMaterialQuantityMap(request) : emptyMaterialEventQuantityMap(); + const geoLatitude = input.geoLatitude?.trim() || null; + const geoLongitude = input.geoLongitude?.trim() || null; + const hasGeolocation = Boolean(geoLatitude && geoLongitude); + const geoStatus = hasGeolocation ? "available" : "unavailable"; + const geoFailureReason = hasGeolocation + ? null + : input.geoFailureReason?.trim() + || (input.geoStatus === "unavailable" ? "Signal faible, refus d'autorisation ou GPS indisponible" : null); + const locationReference = [ + requestFormData.commune, + requestFormData.direction, + requestFormData.service, + ] + .map((value: unknown) => String(value || "").trim()) + .filter(Boolean) + .join(" • ") || association?.ville || request.titre; + + const agentSignatureBuffer = decodeBase64DataUrl(input.agentSignatureData); + const borrowerSignatureBuffer = decodeBase64DataUrl(input.borrowerSignatureData); + const { key: agentSignatureKey, url: agentSignatureUrl } = await storagePut( + `materiel-restitution/${followup.requestId}/signature-agent-${Date.now()}.png`, + agentSignatureBuffer, + "image/png" + ); + const { key: borrowerSignatureKey, url: borrowerSignatureUrl } = await storagePut( + `materiel-restitution/${followup.requestId}/signature-emprunteur-${Date.now()}.png`, + borrowerSignatureBuffer, + "image/png" + ); + + const discrepancyLabelMap = new Map( + materialReturnDiscrepancyOptions.map((entry) => [entry.value, entry.label]) + ); + const discrepancyLabels = input.discrepancyCategories.map( + (value) => discrepancyLabelMap.get(value) || value + ); + + const pdf = await generateCompletedMaterialReturnStatementPdf({ + request, + association, + completion: { + compliance: input.compliance, + discrepancyLabels, + discrepancyDetails: input.discrepancyDetails?.trim() || null, + agentName: input.agentName.trim(), + agentRole: input.agentRole, + borrowerName: input.borrowerName.trim(), + borrowerRole: input.borrowerRole, + validatedAt, + geoLatitude, + geoLongitude, + geoStatus, + geoFailureReason, + locationReference, + agentSignatureBuffer, + borrowerSignatureBuffer, + }, + }); + const { key: finalPdfKey, url: finalPdfUrl } = await storagePut( + `materiel-restitution/${followup.requestId}/fiche-finalisee-${Date.now()}.pdf`, + pdf.buffer, + "application/pdf" + ); + + await db.updateMaterialReturnFollowup(followup.id, { + status: "cloture", + uploadedByName: input.agentName.trim(), + uploadedByEmail: input.agentEmail?.trim() || null, + agentUserId: ctx.user?.id || null, + agentRole: input.agentRole, + borrowerName: input.borrowerName.trim(), + borrowerRole: input.borrowerRole, + compliance: input.compliance, + discrepancyCategories: JSON.stringify(input.discrepancyCategories), + discrepancyDetails: input.discrepancyDetails?.trim() || null, + issueFlag, + litigationStatus: issueFlag ? "pending" : "none", + blockedItems: issueFlag ? serializeBlockedItems(blockedItems) : null, + estimatedDamageAmount: null, + estimatedDamageSource: null, + arbitrationDecision: null, + arbitrationAmount: null, + arbitrationNotes: null, + arbitratedByUserId: null, + arbitratedAt: null, + litigationLetterKey: null, + litigationLetterUrl: null, + litigationLetterName: null, + agentSignatureKey, + agentSignatureUrl, + borrowerSignatureKey, + borrowerSignatureUrl, + finalPdfKey, + finalPdfUrl, + finalPdfName: pdf.fileName, + signedFileKey: finalPdfKey, + signedFileUrl: finalPdfUrl, + signedFileName: pdf.fileName, + signedMimeType: "application/pdf", + geoLatitude, + geoLongitude, + geoStatus, + geoFailureReason, + uploadedAt: validatedAt, + validatedAt, + closedAt: validatedAt, + }); + + await db.createRequestHistory({ + requestId: request.id, + action: "ajout_commentaire", + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user?.id || 0, + commentaire: issueFlag + ? `Restitution finalisée avec réserves par ${input.agentName.trim()}` + : `Restitution finalisée sans réserve par ${input.agentName.trim()}`, + }); + + await db.createAdminNotification({ + type: "systeme", + titre: issueFlag + ? `Alerte dégradation / litige - ${request.titre}` + : `Matériel récupéré / dossier clôturé - ${request.titre}`, + message: issueFlag + ? `Des réserves ont été enregistrées sur la restitution du matériel pour "${request.titre}".` + : `La restitution du matériel a été finalisée pour "${request.titre}".`, + lien: `/dashboard/requests/${request.id}`, + }); + + if (await canSendOperationalEmails()) { + const completionEmail = generateMaterialReturnCompletionEmail({ + associationName: association?.nomAssociation || "Association", + requestTitle: request.titre, + restitutionDate: followup.restitutionDate, + serviceLabel: followup.serviceLabel, + issueFlag, + compliance: input.compliance, + discrepancyDetails: input.discrepancyDetails?.trim() || null, + }); + const attachment = { + filename: pdf.fileName, + content: pdf.buffer, + contentType: "application/pdf", + }; + + const internalRecipients = mergeRecipients( + parseStoredRecipientEmails(followup.supervisionRecipientEmails), + parseStoredRecipientEmails(followup.recipientEmails) + ); + const associationRecipient = association?.emailContact?.trim() + || (() => { + try { + const formData = request.formData ? JSON.parse(request.formData) : {}; + return String(formData.emailAssociation || "").trim(); + } catch { + return ""; + } + })(); + + try { + if (internalRecipients.length > 0) { + await sendOperationalEmail({ + to: internalRecipients, + subject: completionEmail.subject, + text: completionEmail.text, + html: completionEmail.html, + attachments: [attachment], + replyTo: input.agentEmail?.trim() || undefined, + fromName: input.agentName.trim() ? `${input.agentName.trim()} via Portail Associations` : "Portail Associations", + }); + } + + if (associationRecipient) { + await sendOperationalEmail({ + to: [associationRecipient], + subject: completionEmail.subject, + text: completionEmail.text, + html: completionEmail.html, + attachments: [attachment], + replyTo: input.agentEmail?.trim() || undefined, + fromName: "Portail Associations", + }); + } + } catch (error) { + console.error("[MaterialReturn] Completion email failed:", error); + } + } + + return { + success: true, + issueFlag, + finalPdfUrl, + }; + }), + + resolveLitigation: logisticsProcedure + .input( + z.object({ + requestId: z.number(), + blockedItems: z.record(z.string(), z.number().int().min(0)), + estimatedDamageAmount: z.number().int().min(0).nullable().optional(), + estimatedDamageSource: z.enum(["manual", "replacement_value"]).default("manual"), + arbitrationDecision: z.enum(["partial_retention", "full_retention", "dismissed"]), + arbitrationAmount: z.number().int().min(0), + arbitrationNotes: z.string().optional(), + }) + ) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.requestId); + if (!request || request.type !== "demande_materiel_evenementiel") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de matériel introuvable" }); + } + + const followup = await db.getMaterialReturnFollowupByRequestId(input.requestId); + if (!followup || !followup.issueFlag) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Aucun litige actif n'est disponible sur ce dossier" }); + } + if (followup.litigationStatus !== "pending") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le litige a déjà été arbitré" }); + } + + const logisticsSettings = await getLogisticsSettings(); + const blockedItems = sanitizeMaterialEventQuantityMap(input.blockedItems); + const calculatedReplacementAmount = materialEventItems.reduce((sum, item) => { + const replacementValue = logisticsSettings.replacementValues[item.key]; + if (replacementValue === null) return sum; + return sum + ((blockedItems[item.key] || 0) * replacementValue); + }, 0); + + const estimatedDamageAmount = input.estimatedDamageSource === "replacement_value" + ? calculatedReplacementAmount + : Math.max(0, input.estimatedDamageAmount || 0); + + const association = await db.getAssociationById(request.associationId); + const serializedFollowup = serializeMaterialReturnFollowup(followup); + const litigationLetterPdf = await generateMaterialReturnLitigationLetterPdf({ + request, + association, + followup: { + restitutionDate: followup.restitutionDate, + discrepancyCategories: serializedFollowup?.discrepancyCategories || [], + discrepancyDetails: followup.discrepancyDetails, + }, + arbitration: { + blockedItems, + decision: input.arbitrationDecision, + amountCents: input.arbitrationAmount, + notes: input.arbitrationNotes?.trim() || null, + }, + }); + const { key: litigationLetterKey, url: litigationLetterUrl } = await storagePut( + `materiel-restitution/${followup.requestId}/courrier-litige-${Date.now()}.pdf`, + litigationLetterPdf.buffer, + "application/pdf" + ); + + const arbitratedAt = new Date(); + await db.updateMaterialReturnFollowup(followup.id, { + litigationStatus: "resolved", + blockedItems: serializeBlockedItems(blockedItems), + estimatedDamageAmount, + estimatedDamageSource: input.estimatedDamageSource, + arbitrationDecision: input.arbitrationDecision, + arbitrationAmount: input.arbitrationAmount, + arbitrationNotes: input.arbitrationNotes?.trim() || null, + arbitratedByUserId: ctx.user.id, + arbitratedAt, + litigationLetterKey, + litigationLetterUrl, + litigationLetterName: litigationLetterPdf.fileName, + }); + + await db.createRequestHistory({ + requestId: request.id, + action: "ajout_commentaire", + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: `Arbitrage litige finalisé (${input.arbitrationDecision}) par ${ctx.user.name || `Admin #${ctx.user.id}`}`, + }); + + await db.createAdminNotification({ + type: "systeme", + titre: `Arbitrage clôturé - ${request.titre}`, + message: `Le litige matériel de "${request.titre}" a été arbitré et archivé.`, + lien: `/dashboard/requests/${request.id}`, + }); + + await logAdminAction(ctx.user.id, "arbitrage", "request", request.id, { + decision: input.arbitrationDecision, + arbitrationAmount: input.arbitrationAmount, + estimatedDamageAmount, + blockedItems, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true, + litigationLetterUrl, + litigationLetterName: litigationLetterPdf.fileName, + }; + }), + + uploadSigned: publicProcedure + .input( + z.object({ + token: z.string().min(1), + uploadedByName: z.string().min(2, "Le nom du déposant est requis"), + uploadedByEmail: z.string().email("Adresse email invalide").optional().or(z.literal("")), + fileData: z.string().min(1), + fileName: z.string().min(1), + mimeType: z.string().min(1), + }) + ) + .mutation(async ({ input }) => { + const followup = await db.getMaterialReturnFollowupByToken(input.token); + if (!followup) { + throw new TRPCError({ code: "NOT_FOUND", message: "Lien de restitution introuvable" }); + } + + if (followup.status === "cloture") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette restitution a déjà été clôturée" }); + } + + if (new Date() > new Date(followup.uploadTokenExpiresAt)) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le délai de dépôt de la fiche est dépassé" }); + } + + const allowedMimeTypes = [ + "application/pdf", + "image/jpeg", + "image/png", + "image/webp", + "image/heic", + ]; + if (!allowedMimeTypes.includes(input.mimeType)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "La fiche signée doit être un PDF ou une photo", + }); + } + + const request = await db.getRequestById(followup.requestId); + if (!request) { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande liée introuvable" }); + } + + const fileBuffer = Buffer.from(input.fileData, "base64"); + const storageName = `materiel-restitution/${followup.requestId}/${Date.now()}-${input.fileName}`; + const { key, url } = await storagePut(storageName, fileBuffer, input.mimeType); + + await db.updateMaterialReturnFollowup(followup.id, { + status: "cloture", + signedFileKey: key, + signedFileUrl: url, + signedFileName: input.fileName, + signedMimeType: input.mimeType, + uploadedByName: input.uploadedByName.trim(), + uploadedByEmail: input.uploadedByEmail?.trim() || null, + uploadedAt: new Date(), + closedAt: new Date(), + }); + + await db.createAdminNotification({ + type: "systeme", + titre: `Restitution clôturée - ${request.titre}`, + message: `La fiche signée de restitution du matériel a été déposée pour la demande "${request.titre}".`, + lien: `/dashboard/requests/${request.id}`, + }); + + return { + success: true, + fileUrl: url, + }; + }), + + generatePdf: publicProcedure + .input(z.object({ token: z.string().min(1) })) + .mutation(async ({ input }) => { + const followup = await db.getMaterialReturnFollowupByToken(input.token); + if (!followup) { + throw new TRPCError({ code: "NOT_FOUND", message: "Lien de restitution introuvable" }); + } + + const request = await db.getRequestById(followup.requestId); + if (!request) { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande liée introuvable" }); + } + + const association = await db.getAssociationById(followup.associationId); + const pdf = await generateMaterialReturnStatementPdf({ + request, + association, + }); + const storageName = `materiel-restitution/${followup.requestId}/fiche-pre-remplie-${Date.now()}.pdf`; + const { url } = await storagePut(storageName, pdf.buffer, "application/pdf"); + + return { + url, + fileName: pdf.fileName, + }; + }), + }), + + // ============== DOCUMENT ROUTES ============== + document: router({ + getMyDocuments: protectedProcedure.query(async ({ ctx }) => { + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association) return []; + return db.getDocumentsByAssociationId(association.id); + }), + + upload: protectedProcedure + .input(z.object({ + nom: z.string().min(1), + type: z.enum(['statuts', 'recepisse_declaration', 'rib', 'rapport_activite', 'rapport_financier', 'pv_assemblee', 'liste_dirigeants', 'attestation_assurance', 'autre']), + description: z.string().optional(), + fileData: z.string(), + fileName: z.string(), + mimeType: z.string(), + })) + .mutation(async ({ ctx, input }) => { + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Veuillez d\'abord créer votre profil association' }); + } + + const fileBuffer = Buffer.from(input.fileData, 'base64'); + const fileKey = `associations/${association.id}/documents/${nanoid()}-${input.fileName}`; + + const { url } = await storagePut(fileKey, fileBuffer, input.mimeType); + + const docId = await db.createDocument({ + associationId: association.id, + nom: input.nom, + type: input.type, + description: input.description, + fileKey, + fileUrl: url, + mimeType: input.mimeType, + fileSize: fileBuffer.length, + }); + + return { id: docId, url }; + }), + + delete: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Association non trouvée' }); + } + + const doc = await db.getDocumentById(input.id); + if (!doc || doc.associationId !== association.id) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Document non trouvé' }); + } + + await db.deleteDocument(input.id); + return { success: true }; + }), + + getByAssociationId: adminProcedure + .input(z.object({ associationId: z.number() })) + .query(async ({ input }) => { + return db.getDocumentsByAssociationId(input.associationId); + }), + + getDownloadUrl: protectedProcedure + .input(z.object({ id: z.number() })) + .query(async ({ ctx, input }) => { + const association = await db.getAssociationByUserId(ctx.user.id); + const doc = await db.getDocumentById(input.id); + + if (!doc) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Document non trouvé' }); + } + + if (!canReadInternalRequest(ctx.user) && (!association || doc.associationId !== association.id)) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } + + const { url } = await storageGet(doc.fileKey); + return { url }; + }), + }), + + salleSignature: router({ + getDelegation: directriceProcedure.query(async ({ ctx }) => { + const [candidates, delegatedUsers, legacyDelegatedUser] = await Promise.all([ + getSalleSignatureDelegateCandidates(ctx.user.id), + getSalleSignatureDelegates(ctx.user.id), + ctx.user.delegatedSalleSignerUserId + ? db.getUserById(ctx.user.delegatedSalleSignerUserId) + : Promise.resolve(undefined), + ]); + + const normalizedDelegatedUsers = legacyDelegatedUser && !delegatedUsers.some((entry) => entry.id === legacyDelegatedUser.id) + ? delegatedUsers.concat({ + id: legacyDelegatedUser.id, + name: legacyDelegatedUser.name || "Sans nom", + email: legacyDelegatedUser.email || "", + role: legacyDelegatedUser.role, + }) + : delegatedUsers; + + return { + delegatedUserId: normalizedDelegatedUsers[0]?.id ?? ctx.user.delegatedSalleSignerUserId ?? null, + delegatedUserName: normalizedDelegatedUsers[0]?.name || legacyDelegatedUser?.name || null, + delegatedUserEmail: normalizedDelegatedUsers[0]?.email || legacyDelegatedUser?.email || null, + delegatedUsers: normalizedDelegatedUsers, + candidates, + }; + }), + + setDelegation: directriceProcedure + .input(z.object({ + delegateUserIds: z.array(z.number()).max(20), + })) + .mutation(async ({ ctx, input }) => { + if (ctx.user.role !== "directrice") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Seule la Directrice peut choisir son délégataire de signature", + }); + } + + const candidates = await getSalleSignatureDelegateCandidates(ctx.user.id); + const candidateIds = new Set(candidates.map((entry) => entry.id)); + const invalidUserId = input.delegateUserIds.find((userId) => !candidateIds.has(userId)); + if (invalidUserId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "La personne choisie ne peut pas recevoir la délégation de signature", + }); + } + + await db.updateUser(ctx.user.id, { + delegatedSalleSignerUserId: input.delegateUserIds[0] ?? null, + }); + await setDelegatedSignerIdsForDirectrice(ctx.user.id, input.delegateUserIds); + + await logAdminAction(ctx.user.id, "delegation_signature_salle", "user", ctx.user.id, { + delegatedSalleSignerUserIds: input.delegateUserIds, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true, + delegatedUsers: candidates.filter((entry) => input.delegateUserIds.includes(entry.id)), + }; + }), + + addDelegateByEmail: directriceProcedure + .input(z.object({ + email: z.string().trim().email("Adresse email invalide"), + name: z.string().trim().min(2).optional(), + })) + .mutation(async ({ ctx, input }) => { + if (ctx.user.role !== "directrice") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Seule la Directrice peut ajouter un signataire remplaçant", + }); + } + + const normalizedEmail = input.email.trim().toLowerCase(); + let user = await db.getUserByEmail(normalizedEmail); + + if (!user) { + const generatedPassword = randomBytes(24).toString("base64url"); + const fallbackName = normalizedEmail.split("@")[0]?.replace(/[._-]+/g, " ").trim() || "Signataire"; + user = await registerLocalUser({ + name: input.name?.trim() || fallbackName, + email: normalizedEmail, + password: generatedPassword, + }); + await db.updateUser(user.id, { + name: input.name?.trim() || user.name || fallbackName, + email: normalizedEmail, + role: "user", + canManageLogistics: false, + isActive: true, + loginMethod: "local_jwt", + }); + } else { + await db.updateUser(user.id, { + name: input.name?.trim() || user.name || normalizedEmail, + email: normalizedEmail, + isActive: true, + }); + } + + const candidates = await getSalleSignatureDelegateCandidates(ctx.user.id); + const candidate = candidates.find((entry) => entry.id === user.id); + if (!candidate) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cette adresse ne peut pas être utilisée pour la délégation de signature", + }); + } + + const delegateUserIds = await addDelegatedSignerForDirectrice(ctx.user.id, user.id); + await db.updateUser(ctx.user.id, { + delegatedSalleSignerUserId: delegateUserIds[0] ?? null, + }); + + await logAdminAction(ctx.user.id, "ajout_delegataire_signature_salle", "user", user.id, { + email: normalizedEmail, + delegatedSalleSignerUserIds: delegateUserIds, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true, + addedUser: candidate, + }; + }), + + removeDelegate: directriceProcedure + .input(z.object({ + userId: z.number(), + })) + .mutation(async ({ ctx, input }) => { + if (ctx.user.role !== "directrice") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Seule la Directrice peut retirer un signataire remplaçant", + }); + } + + const currentDelegateIds = await getDelegatedSignerIdsForDirectrice(ctx.user.id); + if (!currentDelegateIds.includes(input.userId)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cette personne n'est pas dans la délégation active", + }); + } + + const nextDelegateIds = await removeDelegatedSignerForDirectrice(ctx.user.id, input.userId); + await db.updateUser(ctx.user.id, { + delegatedSalleSignerUserId: nextDelegateIds[0] ?? null, + }); + + await logAdminAction(ctx.user.id, "suppression_delegataire_signature_salle", "user", input.userId, { + delegatedSalleSignerUserIds: nextDelegateIds, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true, + delegateUserIds: nextDelegateIds, + }; + }), + }), + + // ============== REQUEST ROUTES ============== + request: router({ + getMyRequests: protectedProcedure.query(async ({ ctx }) => { + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association) return []; + return db.getRequestsByAssociationId(association.id); + }), + + getById: protectedProcedure + .input(z.object({ id: z.number() })) + .query(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + if (!canReadInternalRequest(ctx.user)) { + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association || request.associationId !== association.id) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } + } + + // Get history for admins + let history: any[] = []; + if (canReadInternalRequest(ctx.user)) { + history = await db.getRequestHistoryByRequestId(input.id); + } + + // Get association info + const association = await db.getAssociationById(request.associationId); + + // Get attached documents + let attachedDocuments: any[] = []; + if (request.documentsJoints) { + try { + const docIds = JSON.parse(request.documentsJoints) as number[]; + if (Array.isArray(docIds) && docIds.length > 0) { + const docs = await Promise.all( + docIds.map(async (docId) => { + const doc = await db.getDocumentById(docId); + return doc || null; + }) + ); + attachedDocuments = docs.filter(Boolean); + } + } catch { + // ignore parse errors + } + } + + return { ...request, history, associationInfo: association || null, attachedDocuments }; + }), + + generatePdf: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + // Vérifier l'accès : admin ou propriétaire + if (!canReadInternalRequest(ctx.user)) { + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association || request.associationId !== association.id) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } + } + + const pdfDocument = await generateRequestPdfDocument(request); + const { url } = await storagePut(pdfDocument.storageName, pdfDocument.buffer, pdfDocument.contentType); + return { url, fileName: pdfDocument.fileName }; + }), + + generateMaterialConvention: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== 'demande_materiel_evenementiel') { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande de matériel introuvable' }); + } + if (request.status !== 'validee') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'La convention ne peut être générée que pour une demande validée' }); + } + + const financialDecision = parseMaterialFinancialDecision(request.formData); + if (!financialDecision) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Les conditions financières ne sont pas encore renseignées' }); + } + + const association = await db.getAssociationById(request.associationId); + const generatedContract = await generateAndStoreMaterialConventionForRequest({ + request, + association, + decision: { + financialMode: financialDecision.financialMode, + depositRequired: financialDecision.depositRequired, + depositAmountCents: financialDecision.depositAmountCents, + rentalAmountCents: financialDecision.rentalAmountCents, + pricingNotes: financialDecision.pricingNotes, + }, + validatedByUserId: ctx.user.id, + }); + + const nextFormData = mergeMaterialFinancialDecision({ + rawFormData: request.formData, + decision: { + financialMode: financialDecision.financialMode, + depositRequired: financialDecision.depositRequired, + depositAmountCents: financialDecision.depositAmountCents, + rentalAmountCents: financialDecision.rentalAmountCents, + pricingNotes: financialDecision.pricingNotes, + contractStatus: 'generee', + contractPdfUrl: generatedContract.contractPdfUrl, + contractPdfName: generatedContract.contractPdfName, + contractGeneratedAt: generatedContract.contractGeneratedAt, + contractValidatedByUserId: ctx.user.id, + }, + }); + + await db.updateRequest(input.id, { + formData: nextFormData, + }); + + await db.createRequestHistory({ + requestId: input.id, + action: 'modification', + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: 'Convention de mise à disposition / location générée', + }); + + await logAdminAction(ctx.user.id, 'generation_convention', 'request', input.id, { + financialMode: financialDecision.financialMode, + depositAmountCents: financialDecision.depositAmountCents, + rentalAmountCents: financialDecision.rentalAmountCents, + contractPdfName: generatedContract.contractPdfName, + ipAddress: extractClientIp(ctx.req), + }); + + return { + url: generatedContract.contractPdfUrl, + fileName: generatedContract.contractPdfName, + }; + }), + + generateSalleConvention: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== 'demande_salle') { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande de salle introuvable' }); + } + if (request.status !== 'validee') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'La convention ne peut être générée que pour une demande validée' }); + } + + const financialDecision = parseMaterialFinancialDecision(request.formData); + if (!financialDecision) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Les conditions financières ne sont pas encore renseignées' }); + } + + const association = await db.getAssociationById(request.associationId); + const generatedContract = await generateAndStoreSalleConventionForRequest({ + request, + association, + decision: { + financialMode: financialDecision.financialMode, + depositRequired: financialDecision.depositRequired, + depositAmountCents: financialDecision.depositAmountCents, + rentalAmountCents: financialDecision.rentalAmountCents, + pricingNotes: financialDecision.pricingNotes, + }, + validatedByUserId: ctx.user.id, + }); + + const nextFormData = mergeMaterialFinancialDecision({ + rawFormData: request.formData, + decision: { + financialMode: financialDecision.financialMode, + depositRequired: financialDecision.depositRequired, + depositAmountCents: financialDecision.depositAmountCents, + rentalAmountCents: financialDecision.rentalAmountCents, + pricingNotes: financialDecision.pricingNotes, + contractStatus: 'generee', + contractPdfUrl: generatedContract.contractPdfUrl, + contractPdfName: generatedContract.contractPdfName, + contractGeneratedAt: generatedContract.contractGeneratedAt, + contractValidatedByUserId: ctx.user.id, + }, + }); + + await db.updateRequest(input.id, { + formData: nextFormData, + }); + + await db.createRequestHistory({ + requestId: input.id, + action: 'modification', + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: 'Convention de mise à disposition / location de salle générée', + }); + + await logAdminAction(ctx.user.id, 'generation_convention_salle', 'request', input.id, { + financialMode: financialDecision.financialMode, + depositAmountCents: financialDecision.depositAmountCents, + rentalAmountCents: financialDecision.rentalAmountCents, + contractPdfName: generatedContract.contractPdfName, + ipAddress: extractClientIp(ctx.req), + }); + + return { + url: generatedContract.contractPdfUrl, + fileName: generatedContract.contractPdfName, + }; + }), + + sendSalleQuote: accueilAdminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== "demande_salle") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de salle introuvable" }); + } + if (request.status === "validee" || request.status === "refusee" || request.status === "annulee") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette demande n'est plus éligible à l'envoi du devis" }); + } + + const association = await db.getAssociationById(request.associationId); + const quoteWorkflow = await sendSalleQuoteWorkflow({ + request, + association, + req: ctx.req, + user: ctx.user, + }); + + await db.updateRequest(request.id, { + status: request.status === "soumise" ? "en_cours_traitement" : request.status, + formData: quoteWorkflow.updatedFormData, + traitePar: ctx.user.id, + }); + + await db.createRequestHistory({ + requestId: request.id, + action: "modification", + ancienStatut: request.status, + nouveauStatut: request.status === "soumise" ? "en_cours_traitement" : request.status, + userId: ctx.user.id, + commentaire: "Devis et décision administrative prioritaire envoyés à l'association", + }); + + await logAdminAction(ctx.user.id, "envoi_devis_salle", "request", request.id, { + pricingTotalCents: quoteWorkflow.pricing.totalAmountCents, + financialMode: quoteWorkflow.financialMode, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true, + quotePdfUrl: quoteWorkflow.quotePdfUrl, + decisionPdfUrl: quoteWorkflow.decisionPdfUrl, + quoteStatusLabel: SALLE_WORKFLOW_QUOTE_STATUS_LABELS.en_attente_association, + }; + }), + + respondSalleQuote: protectedProcedure + .input(z.object({ + id: z.number(), + decision: z.enum(["accepte", "refuse"]), + })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== "demande_salle") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de salle introuvable" }); + } + + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association || request.associationId !== association.id) { + throw new TRPCError({ code: "FORBIDDEN", message: "Accès non autorisé" }); + } + + if (request.status === "validee" || request.status === "refusee" || request.status === "annulee") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Cette demande ne peut plus recevoir de réponse" }); + } + + const workflow = getSalleWorkflowData(request.formData); + if (workflow.quoteStatus !== "en_attente_association") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "La réponse de l'association a déjà été enregistrée pour ce dossier", + }); + } + + const isAccepted = input.decision === "accepte"; + const nextStatus = isAccepted ? request.status : "refusee"; + const nextFormData = updateSalleWorkflowQuoteStatus(request.formData, input.decision); + + await db.updateRequest(request.id, { + formData: nextFormData, + status: nextStatus, + dateTraitement: isAccepted ? request.dateTraitement : new Date(), + commentaireAdmin: isAccepted + ? request.commentaireAdmin + : ((request.commentaireAdmin ? `${request.commentaireAdmin}\n\n` : "") + "Devis refusé par l'association depuis le portail"), + }); + + await db.createRequestHistory({ + requestId: request.id, + action: isAccepted ? "modification" : "refus", + ancienStatut: request.status, + nouveauStatut: nextStatus, + userId: ctx.user.id, + commentaire: isAccepted + ? "Devis accepté par l'association depuis le portail" + : "Devis refusé par l'association depuis le portail", + }); + + await db.createAdminNotification({ + type: "systeme", + titre: isAccepted ? `Devis accepté : ${request.titre}` : `Devis refusé : ${request.titre}`, + message: isAccepted + ? `L'association a accepté le devis pour la demande "${request.titre}" depuis son espace.` + : `L'association a refusé le devis pour la demande "${request.titre}" depuis son espace.`, + lien: `/dashboard/requests/${request.id}`, + }); + + return { + success: true, + quoteStatus: input.decision, + quoteStatusLabel: isAccepted + ? SALLE_WORKFLOW_QUOTE_STATUS_LABELS.accepte + : SALLE_WORKFLOW_QUOTE_STATUS_LABELS.refuse, + nextStatus, + }; + }), + + sendSalleToDirector: accueilAdminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== "demande_salle") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de salle introuvable" }); + } + + const workflow = getSalleWorkflowData(request.formData); + if (workflow.quoteStatus !== "accepte") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le dossier doit d'abord être accepté par l'association" }); + } + + const nextFormData = updateSalleWorkflowDirectorStatus(request.formData, "en_attente_signature"); + await db.updateRequest(request.id, { formData: nextFormData, traitePar: ctx.user.id }); + await db.createRequestHistory({ + requestId: request.id, + action: "modification", + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: "Dossier transmis à la Directrice pour signature", + }); + await db.createAdminNotification({ + type: "systeme", + titre: "Une demande est en attente de signature.", + message: `Le dossier "${request.titre}" a été transmis à la Direction et attend maintenant une signature.`, + lien: `/dashboard/requests/${request.id}`, + }); + await logAdminAction(ctx.user.id, "transmission_directrice_salle", "request", request.id, { + directorStatus: "en_attente_signature", + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true, + directorStatusLabel: SALLE_WORKFLOW_DIRECTOR_STATUS_LABELS.en_attente_signature, + }; + }), + + returnSalleToHostess: salleSignerProcedure + .input(z.object({ + id: z.number(), + comment: z.string().trim().max(1000).optional(), + })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== "demande_salle") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de salle introuvable" }); + } + + const workflow = getSalleWorkflowData(request.formData); + if (workflow.directorStatus !== "en_attente_signature") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Ce dossier n'est pas en attente de signature", + }); + } + + const nextFormData = mergeSalleWorkflowData( + updateSalleWorkflowDirectorStatus(request.formData, "a_transmettre"), + { + directorReturnComment: input.comment?.trim() || "", + directorReturnedAt: new Date().toISOString(), + } + ); + await db.updateRequest(request.id, { + formData: nextFormData, + }); + + const comment = input.comment?.trim(); + await db.createRequestHistory({ + requestId: request.id, + action: "modification", + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: comment + ? `Dossier renvoyé à l’hôtesse par la Directrice : ${comment}` + : "Dossier renvoyé à l’hôtesse par la Directrice", + }); + + await db.createAdminNotification({ + type: "systeme", + titre: `Retour Directrice : ${request.titre}`, + message: comment + ? `La Directrice a renvoyé le dossier "${request.titre}" à l’hôtesse avec la note suivante : ${comment}` + : `La Directrice a renvoyé le dossier "${request.titre}" à l’hôtesse pour reprise.`, + lien: `/dashboard/requests/${request.id}`, + }); + + await logAdminAction(ctx.user.id, "retour_hotesse_salle", "request", request.id, { + comment: comment || null, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true, + directorStatusLabel: SALLE_WORKFLOW_DIRECTOR_STATUS_LABELS.a_transmettre, + }; + }), + + finalizeSalleReservation: salleSignerProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== "demande_salle") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de salle introuvable" }); + } + + const workflow = getSalleWorkflowData(request.formData); + if (workflow.quoteStatus !== "accepte") { + throw new TRPCError({ code: "BAD_REQUEST", message: "La demande doit être acceptée par l'association avant finalisation" }); + } + if (workflow.directorStatus !== "en_attente_signature") { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le dossier doit d'abord être transmis à la Directrice" }); + } + + const association = await db.getAssociationById(request.associationId); + const pricing = workflow.pricing || computeSalleWorkflowPricing(request.formData); + const financialDecision = parseMaterialFinancialDecision(request.formData); + if (!pricing || !financialDecision) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le dossier ne contient pas les données financières nécessaires à la finalisation" }); + } + + const signaturePackage = await generateSignedSalleWorkflowPackage({ + request, + association, + pricing, + financialDecision, + signedByUserId: ctx.user.id, + signedByName: ctx.user.name || `Directrice #${ctx.user.id}`, + }); + await db.updateRequest(request.id, { + formData: signaturePackage.nextFormData, + }); + + await db.createRequestHistory({ + requestId: request.id, + action: "validation", + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: "Dossier signé par la Direction et renvoyé à l’hôtesse pour confirmation finale", + }); + + await db.createAdminNotification({ + type: "systeme", + titre: `Dossier signé : ${request.titre}`, + message: `La Direction a signé le dossier "${request.titre}". L’hôtesse retrouve maintenant le devis signé, la décision signée, le formulaire signé et la facture générée pour préparer l’envoi final à l’association.`, + lien: `/dashboard/requests/${request.id}`, + }); + + await logAdminAction(ctx.user.id, "signature_direction_salle", "request", request.id, { + directorStatus: "signee", + pricingTotalCents: pricing.totalAmountCents, + financialMode: financialDecision.financialMode, + ipAddress: extractClientIp(ctx.req), + }); + + return { success: true }; + }), + + prepareSalleFinalNotification: accueilAdminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== "demande_salle") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de salle introuvable" }); + } + + const workflow = getSalleWorkflowData(request.formData); + if (workflow.quoteStatus !== "accepte" || workflow.directorStatus !== "signee") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le dossier doit d'abord être signé par la Direction avant l'envoi final", + }); + } + if (workflow.finalNotificationSentAt) { + throw new TRPCError({ code: "BAD_REQUEST", message: "La confirmation finale a déjà été envoyée" }); + } + + const association = await db.getAssociationById(request.associationId); + const pricing = workflow.pricing || computeSalleWorkflowPricing(request.formData); + const financialDecision = parseMaterialFinancialDecision(request.formData); + const salleBilling = await getSalleBillingSettings(); + if (!pricing || !financialDecision) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Les données financières de ce dossier sont incomplètes" }); + } + + const invoicePreview = await ensureSalleInvoicePrepared({ + request, + association, + pricing, + billed: financialDecision.financialMode === "location_payante", + }); + if (invoicePreview.nextFormData !== request.formData) { + await db.updateRequest(request.id, { + formData: invoicePreview.nextFormData, + }); + } + + const effectiveFormData = parseRequestFormData(invoicePreview.nextFormData); + const finalEmail = generateSalleFinalEmail({ + associationName: association?.nomAssociation || effectiveFormData.nomAssociation || "association", + datesLabel: buildSalleDatesLabel(effectiveFormData), + pricingTotalCents: pricing.totalAmountCents, + billed: financialDecision.financialMode === "location_payante", + }); + + return { + success: true, + associationName: association?.nomAssociation || effectiveFormData.nomAssociation || "Association", + datesLabel: buildSalleDatesLabel(effectiveFormData), + billed: financialDecision.financialMode === "location_payante", + pricingTotalCents: pricing.totalAmountCents, + quotePdfUrl: workflow.signedQuotePdfUrl || workflow.quotePdfUrl || "", + quotePdfName: workflow.signedQuotePdfName || workflow.quotePdfName || "", + decisionPdfUrl: workflow.signedDecisionPdfUrl || workflow.administrativeDecisionPdfUrl || "", + decisionPdfName: workflow.signedDecisionPdfName || workflow.administrativeDecisionPdfName || "", + requestPdfUrl: workflow.signedRequestPdfUrl || "", + requestPdfName: workflow.signedRequestPdfName || "", + invoicePdfUrl: invoicePreview.invoicePdfUrl, + invoicePdfName: invoicePreview.invoicePdfName, + invoiceGeneratedAt: invoicePreview.invoiceGeneratedAt, + ribDocumentUrl: salleBilling.ribDocumentUrl, + ribDocumentName: salleBilling.ribDocumentName, + mailPreview: finalEmail.text, + }; + }), + + sendSalleFinalNotification: accueilAdminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== "demande_salle") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de salle introuvable" }); + } + + const workflow = getSalleWorkflowData(request.formData); + if (workflow.quoteStatus !== "accepte" || workflow.directorStatus !== "signee") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le dossier doit être signé par la Direction avant l'envoi final", + }); + } + if (workflow.finalNotificationSentAt) { + throw new TRPCError({ code: "BAD_REQUEST", message: "La confirmation finale a déjà été envoyée" }); + } + + const association = await db.getAssociationById(request.associationId); + const associationRecipient = association?.emailContact?.trim() || parseRequestFormData(request.formData).emailAssociation?.trim(); + if (!associationRecipient) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Aucun email d'association n'est disponible pour la notification finale" }); + } + + const pricing = workflow.pricing || computeSalleWorkflowPricing(request.formData); + const financialDecision = parseMaterialFinancialDecision(request.formData); + const salleBilling = await getSalleBillingSettings(); + if (!pricing || !financialDecision) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le dossier ne contient pas les données financières nécessaires à l'envoi final" }); + } + + let effectiveFormDataRaw = request.formData || ""; + const invoicePreview = await ensureSalleInvoicePrepared({ + request, + association, + pricing, + billed: financialDecision.financialMode === "location_payante", + }); + effectiveFormDataRaw = invoicePreview.nextFormData; + + const signedWorkflow = getSalleWorkflowData(effectiveFormDataRaw); + const finalEmail = generateSalleFinalEmail({ + associationName: association?.nomAssociation || parseRequestFormData(effectiveFormDataRaw).nomAssociation || "association", + datesLabel: buildSalleDatesLabel(parseRequestFormData(effectiveFormDataRaw)), + pricingTotalCents: pricing.totalAmountCents, + billed: financialDecision.financialMode === "location_payante", + }); + + const attachments: Array<{ filename: string; content: Buffer; contentType?: string }> = []; + + const pushStoredPdf = async (url: string | undefined, filename: string | undefined) => { + if (!url || !filename) return; + const stored = await readStoredUploadBufferFromUrl(url); + if (!stored) return; + attachments.push({ + filename, + content: stored, + contentType: "application/pdf", + }); + }; + + if (financialDecision.financialMode === "location_payante") { + await pushStoredPdf(signedWorkflow.signedQuotePdfUrl || workflow.signedQuotePdfUrl, signedWorkflow.signedQuotePdfName || workflow.signedQuotePdfName); + } + await pushStoredPdf(signedWorkflow.signedDecisionPdfUrl || workflow.signedDecisionPdfUrl, signedWorkflow.signedDecisionPdfName || workflow.signedDecisionPdfName); + await pushStoredPdf(signedWorkflow.signedRequestPdfUrl || workflow.signedRequestPdfUrl, signedWorkflow.signedRequestPdfName || workflow.signedRequestPdfName); + + if (invoicePreview.buffer) { + attachments.push({ + filename: invoicePreview.invoicePdfName, + content: invoicePreview.buffer, + contentType: "application/pdf", + }); + } else { + await pushStoredPdf(invoicePreview.invoicePdfUrl, invoicePreview.invoicePdfName); + } + if (financialDecision.financialMode === "location_payante" && salleBilling.ribDocumentUrl && salleBilling.ribDocumentName) { + const ribBuffer = await readStoredUploadBufferFromUrl(salleBilling.ribDocumentUrl); + if (ribBuffer) { + attachments.push({ + filename: salleBilling.ribDocumentName, + content: ribBuffer, + contentType: salleBilling.ribMimeType || "application/pdf", + }); + } + } + + const result = await sendOperationalEmail({ + to: [associationRecipient], + subject: finalEmail.subject, + text: finalEmail.text, + html: finalEmail.html, + replyTo: ctx.user.email || undefined, + fromName: "Maison de la Jeunesse des Savanes - CCDS", + attachments, + }); + + if (!result.sent) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Le mail final n'a pas pu être envoyé (${result.reason || "raison inconnue"})` }); + } + + const nextFormData = mergeSalleWorkflowData(effectiveFormDataRaw, { + finalNotificationSentAt: new Date().toISOString(), + }); + + await db.updateRequest(request.id, { + status: "validee", + dateTraitement: new Date(), + traitePar: ctx.user.id, + formData: nextFormData, + }); + + await db.createRequestHistory({ + requestId: request.id, + action: "validation", + ancienStatut: request.status, + nouveauStatut: "validee", + userId: ctx.user.id, + commentaire: "Confirmation finale envoyée à l'association avec les documents signés et la facture", + }); + + await logAdminAction(ctx.user.id, "envoi_final_salle", "request", request.id, { + directorStatus: "signee", + pricingTotalCents: pricing.totalAmountCents, + financialMode: financialDecision.financialMode, + ipAddress: extractClientIp(ctx.req), + }); + + return { success: true }; + }), + + updateSallePaymentTracking: accueilAdminProcedure + .input(z.object({ + id: z.number(), + paymentStatus: z.enum(["en_attente_paiement", "paiement_partiel", "paiement_recu", "paiement_valide"]), + amountReceivedCents: z.number().min(0).optional(), + paymentReceivedAt: z.string().optional(), + paymentReference: z.string().trim().max(255).optional(), + paymentNotes: z.string().trim().max(2000).optional(), + })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request || request.type !== "demande_salle") { + throw new TRPCError({ code: "NOT_FOUND", message: "Demande de salle introuvable" }); + } + + const financialDecision = parseMaterialFinancialDecision(request.formData); + if (!financialDecision || financialDecision.financialMode !== "location_payante") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le suivi de paiement est réservé aux locations de salle payantes", + }); + } + + const existingPayment = getSallePaymentTracking(request.formData); + const nextPaymentReceivedAt = + input.paymentStatus === "paiement_recu" || input.paymentStatus === "paiement_valide" + ? (input.paymentReceivedAt?.trim() + || existingPayment.paymentReceivedAt + || new Date().toISOString()) + : ""; + const nextPaymentValidatedAt = + input.paymentStatus === "paiement_valide" + ? (existingPayment.paymentValidatedAt || new Date().toISOString()) + : ""; + + const nextFormData = mergeSalleWorkflowData( + ensureSallePaymentTracking({ + rawFormData: request.formData, + billed: true, + totalAmountCents: Number.isFinite(Number(getSalleWorkflowData(request.formData).pricing?.totalAmountCents)) + ? Number(getSalleWorkflowData(request.formData).pricing?.totalAmountCents) + : financialDecision.rentalAmountCents, + }), + { + paymentStatus: input.paymentStatus, + amountReceivedCents: input.amountReceivedCents ?? existingPayment.amountReceivedCents, + paymentReceivedAt: nextPaymentReceivedAt, + paymentValidatedAt: nextPaymentValidatedAt, + paymentReference: input.paymentReference?.trim() || "", + paymentNotes: input.paymentNotes?.trim() || "", + } + ); + + await db.updateRequest(request.id, { + formData: nextFormData, + traitePar: ctx.user.id, + }); + + await db.createRequestHistory({ + requestId: request.id, + action: "modification", + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: `Suivi de paiement mis à jour : ${SALLE_WORKFLOW_PAYMENT_STATUS_LABELS[input.paymentStatus]}`, + }); + + await logAdminAction(ctx.user.id, "mise_a_jour_paiement_salle", "request", request.id, { + paymentStatus: input.paymentStatus, + amountReceivedCents: input.amountReceivedCents ?? existingPayment.amountReceivedCents, + ipAddress: extractClientIp(ctx.req), + }); + + return { + success: true, + paymentStatusLabel: SALLE_WORKFLOW_PAYMENT_STATUS_LABELS[input.paymentStatus], + }; + }), + + create: protectedProcedure + .input(z.object({ + type: z.enum(['subvention_fonctionnement', 'subvention_projet', 'agrement_jeunesse_education', 'agrement_sport', 'autorisation_occupation', 'demande_salle', 'demande_materiel_evenementiel', 'autre']), + titre: z.string().min(1), + description: z.string().optional(), + formData: z.string().optional(), + montantDemande: z.number().optional(), + documentsJoints: z.array(z.number()).optional(), + submitImmediately: z.boolean().optional(), // true = créer ET soumettre en une seule opération + privacyConsent: dataPrivacyConsentSchema, + })) + .mutation(async ({ ctx, input }) => { + assertDataPrivacyConsent(input.privacyConsent); + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Veuillez d\'abord créer votre profil association' }); + } + await recordDataPrivacyConsent(ctx.user.id, ctx.req, input.privacyConsent); + + // Enrichir le formData avec les infos association actuelles pour garantir la persistance + let enrichedFormData = input.formData; + if (input.formData) { + try { + const fd = JSON.parse(input.formData); + // S'assurer que les infos association sont toujours présentes depuis la source de vérité + fd.nomAssociation = fd.nomAssociation || association.nomAssociation || ''; + fd.adresseAssociation = fd.adresseAssociation || (association.adresse ? `${association.adresse}, ${association.codePostal || ''} ${association.ville || ''}`.trim() : ''); + fd.communeSiege = fd.communeSiege || association.ville || ''; + fd.representantLegal = fd.representantLegal || association.nomRepresentant || ''; + fd.telephoneAssociation = fd.telephoneAssociation || association.telephone || ''; + fd.emailAssociation = fd.emailAssociation || association.emailContact || ''; + enrichedFormData = JSON.stringify(fd); + } catch (e) { + // keep original if parse fails + } + } + + if (input.type === 'demande_salle') { + validateReservationSalleFormData(enrichedFormData); + } + + // Calculate deadline (30 days by default) + const dateLimite = new Date(); + dateLimite.setDate(dateLimite.getDate() + 30); + + const requestId = await db.createRequest({ + associationId: association.id, + type: input.type, + titre: input.titre, + description: input.description, + formData: enrichedFormData, + montantDemande: input.montantDemande, + documentsJoints: input.documentsJoints ? JSON.stringify(input.documentsJoints) : null, + status: 'brouillon', + dateLimiteTraitement: dateLimite, + }); + + // Log creation + await db.createRequestHistory({ + requestId, + action: 'creation', + userId: ctx.user.id, + commentaire: 'Création de la demande', + }); + + // Si submitImmediately, soumettre directement + if (input.submitImmediately) { + await db.updateRequest(requestId, { + status: 'soumise', + dateSubmission: new Date(), + }); + + await db.createRequestHistory({ + requestId, + action: 'soumission', + ancienStatut: 'brouillon', + nouveauStatut: 'soumise', + userId: ctx.user.id, + commentaire: 'Soumission immédiate de la demande', + }); + + // Notification admin + await db.createAdminNotification({ + type: 'nouvelle_demande', + titre: `Nouvelle demande: ${input.titre}`, + message: `L'association "${association.nomAssociation}" a soumis une demande de type "${input.type}".`, + lien: `/dashboard/requests/${requestId}`, + }); + + // Generate email action tokens (valid for 7 days) + const validateToken = nanoid(48); + const refuseToken = nanoid(48); + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + await Promise.all([ + db.createEmailActionToken({ token: validateToken, requestId, action: 'validee', expiresAt }), + db.createEmailActionToken({ token: refuseToken, requestId, action: 'refusee', expiresAt }), + ]); + + const baseUrl = ctx.req.headers.origin || `${ctx.req.protocol}://${ctx.req.headers.host}`; + const validateUrl = `${baseUrl}/api/email-action/${validateToken}`; + const refuseUrl = `${baseUrl}/api/email-action/${refuseToken}`; + const viewUrl = `${baseUrl}/dashboard/requests/${requestId}`; + + const requestTypeLabels: Record = { + subvention_fonctionnement: 'Subvention de fonctionnement', + subvention_projet: 'Subvention de projet', + agrement_jeunesse_education: 'Agrément Jeunesse et Éducation Populaire', + agrement_sport: 'Agrément Sport', + autorisation_occupation: 'Autorisation d\'occupation', + demande_salle: 'Demande de salle', + demande_materiel_evenementiel: 'Demande du matériel événementiel', + autre: 'Autre demande', + }; + const typeLabel = requestTypeLabels[input.type] || input.type; + + try { + await notifyOwner({ + title: `📨 Nouvelle demande: ${input.titre}`, + content: `**Association:** ${association.nomAssociation}\n**Type:** ${typeLabel}\n**Description:** ${input.description || 'Non spécifiée'}\n\n---\n\n✅ **Valider la demande:** ${validateUrl}\n\n❌ **Refuser la demande:** ${refuseUrl}\n\n🔍 **Voir le détail:** ${viewUrl}\n\n_Ces liens sont valides pendant 7 jours._`, + }); + } catch (e) { + console.error('Failed to notify owner:', e); + } + + return { id: requestId, submitted: true }; + } + + return { id: requestId, submitted: false }; + }), + + update: protectedProcedure + .input(z.object({ + id: z.number(), + titre: z.string().optional(), + description: z.string().optional(), + formData: z.string().optional(), + montantDemande: z.number().optional(), + documentsJoints: z.array(z.number()).optional(), + })) + .mutation(async ({ ctx, input }) => { + const association = await db.getAssociationByUserId(ctx.user.id); + const request = await db.getRequestById(input.id); + + if (!request || !association || request.associationId !== association.id) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + if (request.status !== 'brouillon' && request.status !== 'information_complementaire') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Seules les demandes en brouillon ou revenues pour complément peuvent être modifiées' }); + } + + if (request.type === 'demande_salle' && input.formData) { + validateReservationSalleFormData(input.formData); + } + + await db.updateRequest(input.id, { + titre: input.titre, + description: input.description, + formData: input.formData, + montantDemande: input.montantDemande, + documentsJoints: input.documentsJoints ? JSON.stringify(input.documentsJoints) : undefined, + }); + + await db.createRequestHistory({ + requestId: input.id, + action: 'modification', + userId: ctx.user.id, + commentaire: 'Modification de la demande', + }); + + // Notify admin when the request comes back with requested complementary information + if (request.status === 'information_complementaire') { + await db.createAdminNotification({ + type: 'modification_demande', + titre: `Demande modifiée : ${request.titre}`, + message: `L'association "${association.nomAssociation}" a modifié sa demande "${request.titre}" (statut : ${request.status}).`, + lien: `/dashboard/requests/${input.id}`, + }); + + try { + await notifyOwner({ + title: `✏️ Demande modifiée : ${request.titre}`, + content: `L'association "${association.nomAssociation}" a modifié sa demande "${request.titre}".\n\nType : ${request.type}\nStatut actuel : ${request.status}`, + }); + } catch (e) { + console.error('Failed to notify owner about modification:', e); + } + } + + return { success: true }; + }), + + submit: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const association = await db.getAssociationByUserId(ctx.user.id); + const request = await db.getRequestById(input.id); + + if (!request || !association || request.associationId !== association.id) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + if (request.status !== 'brouillon' && request.status !== 'information_complementaire') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cette demande ne peut pas être soumise dans son état actuel' }); + } + + await db.updateRequest(input.id, { + status: 'soumise', + dateSubmission: new Date(), + }); + + await db.createRequestHistory({ + requestId: input.id, + action: 'soumission', + ancienStatut: request.status, + nouveauStatut: 'soumise', + userId: ctx.user.id, + commentaire: 'Soumission de la demande', + }); + + // Create admin notification + await db.createAdminNotification({ + type: 'nouvelle_demande', + titre: `Nouvelle demande: ${request.titre}`, + message: `L'association "${association.nomAssociation}" a soumis une demande de type "${request.type}".`, + lien: `/dashboard/requests/${input.id}`, + }); + + // Generate email action tokens (valid for 7 days) + const validateToken = nanoid(48); + const refuseToken = nanoid(48); + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + + await Promise.all([ + db.createEmailActionToken({ token: validateToken, requestId: input.id, action: 'validee', expiresAt }), + db.createEmailActionToken({ token: refuseToken, requestId: input.id, action: 'refusee', expiresAt }), + ]); + + // Build base URL for email links + const baseUrl = ctx.req.headers.origin || `${ctx.req.protocol}://${ctx.req.headers.host}`; + const validateUrl = `${baseUrl}/api/email-action/${validateToken}`; + const refuseUrl = `${baseUrl}/api/email-action/${refuseToken}`; + const viewUrl = `${baseUrl}/dashboard/requests/${input.id}`; + + // Notify owner with action buttons + const requestTypeLabels: Record = { + subvention_fonctionnement: 'Subvention de fonctionnement', + subvention_projet: 'Subvention de projet', + agrement_jeunesse_education: 'Agrément Jeunesse et Éducation Populaire', + agrement_sport: 'Agrément Sport', + autorisation_occupation: 'Autorisation d\'occupation', + demande_salle: 'Demande de salle', + demande_materiel_evenementiel: 'Demande du matériel événementiel', + autre: 'Autre demande', + }; + const typeLabel = requestTypeLabels[request.type] || request.type; + const montantStr = request.montantDemande ? `${(request.montantDemande / 100).toLocaleString('fr-FR')} €` : 'Non spécifié'; + + await notifyOwner({ + title: `📨 Nouvelle demande: ${request.titre}`, + content: `**Association:** ${association.nomAssociation}\n**Type:** ${typeLabel}\n**Montant demandé:** ${montantStr}\n**Description:** ${request.description || 'Non spécifiée'}\n\n---\n\n✅ **Valider la demande:** ${validateUrl}\n\n❌ **Refuser la demande:** ${refuseUrl}\n\n🔍 **Voir le détail:** ${viewUrl}\n\n_Ces liens sont valides pendant 7 jours._`, + }); + + return { success: true }; + }), + + duplicate: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const association = await db.getAssociationByUserId(ctx.user.id); + const request = await db.getRequestById(input.id); + + if (!request || !association || request.associationId !== association.id) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + const dateLimite = new Date(); + dateLimite.setDate(dateLimite.getDate() + 30); + + const duplicatedRequestId = await db.createRequest({ + associationId: association.id, + type: request.type, + titre: `Copie - ${request.titre}`, + description: request.description, + formData: sanitizeDuplicatedRequestFormData(request.formData), + montantDemande: request.montantDemande, + documentsJoints: request.documentsJoints || null, + status: 'brouillon', + priority: request.priority, + dateLimiteTraitement: dateLimite, + }); + + await db.createRequestHistory({ + requestId: duplicatedRequestId, + action: 'creation', + userId: ctx.user.id, + commentaire: `Demande dupliquée à partir du dossier #${request.id}`, + }); + + return { id: duplicatedRequestId, type: request.type }; + }), + + listAll: adminProcedure.query(async () => { + return db.getAllRequests(); + }), + + search: accueilAdminProcedure + .input(z.object({ + search: z.string().optional(), + type: z.string().optional(), + status: z.string().optional(), + priority: z.string().optional(), + assigneA: z.number().optional(), + associationId: z.number().optional(), + dateFrom: z.string().optional(), + dateTo: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), + })) + .query(async ({ input }) => { + return db.searchRequests({ + ...input, + dateFrom: input.dateFrom ? new Date(input.dateFrom) : undefined, + dateTo: input.dateTo ? new Date(input.dateTo) : undefined, + }); + }), + + getPending: accueilAdminProcedure.query(async () => { + return db.getPendingRequests(); + }), + + getOverdue: accueilAdminProcedure.query(async () => { + return db.getOverdueRequests(); + }), + + getSalleSignatureQueue: salleSignerProcedure.query(async () => { + const requests = await db.getAllRequests(); + const salleRequests = requests.filter((request) => { + if (request.type !== "demande_salle") return false; + if (request.status === "refusee" || request.status === "annulee") return false; + const workflow = getSalleWorkflowData(request.formData); + return workflow.quoteStatus === "accepte" && workflow.directorStatus === "en_attente_signature"; + }); + + const queue = await Promise.all( + salleRequests.map(async (request) => { + const association = await db.getAssociationById(request.associationId); + const formData = parseRequestFormData(request.formData); + const workflow = getSalleWorkflowData(request.formData); + const financialDecision = parseMaterialFinancialDecision(request.formData); + const paymentTracking = getSallePaymentTracking(request.formData); + + let attachedDocuments: Array<{ + id: number; + nom: string; + type: string | null; + fileUrl: string; + }> = []; + if (request.documentsJoints) { + try { + const docIds = JSON.parse(request.documentsJoints) as number[]; + if (Array.isArray(docIds) && docIds.length > 0) { + const docs = await Promise.all(docIds.map((docId) => db.getDocumentById(docId))); + attachedDocuments = docs + .filter((doc): doc is NonNullable => Boolean(doc)) + .map((doc) => ({ + id: doc.id, + nom: doc.nom, + type: doc.type ?? null, + fileUrl: doc.fileUrl, + })); + } + } catch { + attachedDocuments = []; + } + } + + return { + id: request.id, + titre: request.titre, + status: request.status, + createdAt: request.createdAt, + associationName: association?.nomAssociation || formData.nomAssociation || `Association #${request.associationId}`, + sallesSelectionnees: Array.isArray(formData.sallesSelectionnees) ? formData.sallesSelectionnees : [], + dateReservation: String(formData.dateReservation || ""), + dateFinReservation: String(formData.dateFinReservation || ""), + heureDebut: String(formData.heureDebut || ""), + heureFin: String(formData.heureFin || ""), + useDetailedSchedule: Boolean(formData.useDetailedSchedule), + horairesParJour: Array.isArray(formData.horairesParJour) ? formData.horairesParJour : [], + quotePdfUrl: typeof workflow.quotePdfUrl === "string" ? workflow.quotePdfUrl : "", + quotePdfName: typeof workflow.quotePdfName === "string" ? workflow.quotePdfName : "", + administrativeDecisionPdfUrl: + typeof workflow.administrativeDecisionPdfUrl === "string" ? workflow.administrativeDecisionPdfUrl : "", + administrativeDecisionPdfName: + typeof workflow.administrativeDecisionPdfName === "string" ? workflow.administrativeDecisionPdfName : "", + directorTransmissionAt: typeof workflow.directorTransmissionAt === "string" ? workflow.directorTransmissionAt : "", + pricing: workflow.pricing || computeSalleWorkflowPricing(request.formData), + financialMode: financialDecision?.financialMode || null, + paymentStatus: paymentTracking.paymentStatus || null, + paymentDueDate: paymentTracking.paymentDueDate || null, + amountReceivedCents: paymentTracking.amountReceivedCents || 0, + paymentReceivedAt: paymentTracking.paymentReceivedAt || null, + attachedDocuments, + }; + }) + ); + + return queue.sort((left, right) => { + const leftDate = left.directorTransmissionAt ? new Date(left.directorTransmissionAt).getTime() : 0; + const rightDate = right.directorTransmissionAt ? new Date(right.directorTransmissionAt).getTime() : 0; + return rightDate - leftDate; + }); + }), + + getPastForSignature: salleReadProcedure.query(async () => { + const allRequests = await db.getAllRequests(); + const pastStatuses = new Set(["validee", "refusee", "annulee"]); + + return Promise.all( + allRequests + .filter((request) => pastStatuses.has(request.status)) + .slice(0, 25) + .map(async (request) => { + const association = await db.getAssociationById(request.associationId); + const formData = parseRequestFormData(request.formData); + const workflow = request.type === "demande_salle" ? getSalleWorkflowData(request.formData) : {}; + const paymentTracking = request.type === "demande_salle" ? getSallePaymentTracking(request.formData) : null; + const financialDecision = request.type === "demande_salle" ? parseMaterialFinancialDecision(request.formData) : null; + + return { + id: request.id, + type: request.type, + titre: request.titre, + status: request.status, + associationName: association?.nomAssociation || formData.nomAssociation || "Association inconnue", + createdAt: request.createdAt, + updatedAt: request.updatedAt, + dateReservation: formData.dateReservation || null, + dateFinReservation: formData.dateFinReservation || null, + sallesSelectionnees: Array.isArray(formData.sallesSelectionnees) ? formData.sallesSelectionnees : [], + quoteStatus: workflow.quoteStatus || null, + directorStatus: workflow.directorStatus || null, + financialMode: financialDecision?.financialMode || null, + paymentStatus: paymentTracking?.paymentStatus || null, + }; + }) + ); + }), + + getSalleBillingBoard: salleReadProcedure.query(async () => { + const allRequests = await db.getAllRequests(); + + const entries = await Promise.all( + allRequests + .filter((request) => request.type === "demande_salle") + .map(async (request) => { + const formData = parseRequestFormData(request.formData); + const workflow = getSalleWorkflowData(request.formData); + const financialDecision = parseMaterialFinancialDecision(request.formData); + if (financialDecision?.financialMode !== "location_payante") { + return null; + } + + const association = await db.getAssociationById(request.associationId); + const paymentTracking = getSallePaymentTracking(request.formData); + const amountDueCents = + workflow.pricing?.totalAmountCents + || financialDecision.rentalAmountCents + || 0; + const paymentStatus = paymentTracking.paymentStatus || "en_attente_paiement"; + const paymentDueDate = paymentTracking.paymentDueDate || null; + const isLate = Boolean( + paymentDueDate + && paymentStatus !== "paiement_recu" + && paymentStatus !== "paiement_valide" + && paymentDueDate < new Date().toISOString().slice(0, 10) + ); + + return { + id: request.id, + titre: request.titre, + status: request.status, + createdAt: request.createdAt, + updatedAt: request.updatedAt, + associationName: association?.nomAssociation || formData.nomAssociation || "Association inconnue", + sallesSelectionnees: Array.isArray(formData.sallesSelectionnees) ? formData.sallesSelectionnees : [], + dateReservation: formData.dateReservation || null, + dateFinReservation: formData.dateFinReservation || null, + amountDueCents, + amountReceivedCents: paymentTracking.amountReceivedCents || 0, + paymentStatus, + paymentDueDate, + paymentReceivedAt: paymentTracking.paymentReceivedAt || null, + paymentValidatedAt: paymentTracking.paymentValidatedAt || null, + paymentReference: paymentTracking.paymentReference || null, + finalNotificationSentAt: + typeof workflow.finalNotificationSentAt === "string" ? workflow.finalNotificationSentAt : null, + isLate, + }; + }) + ); + + return entries + .filter((entry): entry is NonNullable => Boolean(entry)) + .sort((left, right) => { + if (left.isLate !== right.isLate) { + return left.isLate ? -1 : 1; + } + if (left.paymentDueDate && right.paymentDueDate) { + return new Date(left.paymentDueDate).getTime() - new Date(right.paymentDueDate).getTime(); + } + return new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime(); + }); + }), + + assign: adminProcedure + .input(z.object({ + id: z.number(), + adminId: z.number(), + })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + await db.assignRequest(input.id, input.adminId); + + await db.createRequestHistory({ + requestId: input.id, + action: 'assignation', + ancienStatut: request.status, + nouveauStatut: 'en_cours_traitement', + userId: ctx.user.id, + commentaire: `Assignation à l'agent #${input.adminId}`, + }); + + await logAdminAction(ctx.user.id, 'assignation', 'request', input.id, { adminId: input.adminId }); + + return { success: true }; + }), + + process: accueilAdminProcedure + .input(z.object({ + id: z.number(), + status: z.enum(['en_cours_traitement', 'information_complementaire', 'validee', 'refusee']), + commentaireAdmin: z.string().optional(), + montantAccorde: z.number().optional(), + recapServiceId: z.number().optional(), + recapRecipients: z.array(z.string().email()).optional(), + recapServiceLabel: z.string().optional(), + supervisionServiceId: z.number().optional(), + supervisionRecipients: z.array(z.string().email()).optional(), + supervisionServiceLabel: z.string().optional(), + // Cadre DSU pour les demandes de salle et de matériel événementiel + dsuData: z.object({ + dateReception: z.string().optional(), + avisDSU: z.enum(['favorable', 'defavorable', 'favorable_avec_reserves']).optional(), + conditionsParticulieres: z.string().optional(), + cautionRequise: z.boolean().optional(), + montantCaution: z.string().optional(), + assuranceRequise: z.boolean().optional(), + horairesImposes: z.string().optional(), + responsableDSU: z.string().optional(), + dateDecision: z.string().optional(), + observationsDSU: z.string().optional(), + salleReservation: z.object({ + usageType: z.enum(["conventionne", "occasionnel"]).optional(), + frequency: z.enum(["demi_journee", "journee", "mensuel"]).optional(), + }).optional(), + materielEvent: z.object({ + itemsAccordes: z.object({ + tente3x3: z.boolean().optional(), + chapiteau5x5: z.boolean().optional(), + podium: z.boolean().optional(), + autres: z.boolean().optional(), + }).optional(), + quantitesAccordees: z.object({ + tente3x3: z.string().optional(), + chapiteau5x5: z.string().optional(), + podium: z.string().optional(), + autres: z.string().optional(), + }).optional(), + autresPrecisions: z.string().optional(), + financialDecision: z.object({ + financialMode: z.enum(["gratuite", "gratuite_avec_caution", "location_payante"]).optional(), + depositRequired: z.boolean().optional(), + depositAmountCents: z.number().int().min(0).optional(), + rentalAmountCents: z.number().int().min(0).optional(), + pricingNotes: z.string().optional(), + contractStatus: z.enum(["a_generer", "generee", "signee", "refusee", "annulee"]).optional(), + }).optional(), + }).optional(), + }).optional(), + generateContract: z.boolean().optional(), + })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + if (ctx.user.role === "accueil" && request.type !== "demande_salle") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Le rôle accueil ne peut traiter que les demandes de salle", + }); + } + if (ctx.user.role === "logistique_controle" && request.type !== "demande_materiel_evenementiel") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Le rôle matériel CCDS - logistique et contrôle ne peut traiter que les demandes de matériel", + }); + } + if (request.type === "demande_materiel_evenementiel" && !canManageLogistics(ctx.user)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Les demandes de matériel sont gérées par le service logistique et contrôle", + }); + } + if (request.type === "demande_salle") { + const workflow = getSalleWorkflowData(request.formData); + if (workflow.quoteStatus === "en_attente_association") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cette demande est en attente de réponse de l'association. Elle ne peut plus être traitée tant que le devis n'est pas accepté.", + }); + } + } + + const selectedRecapService = input.recapServiceId + ? await db.getOperationalRecapServiceById(input.recapServiceId) + : undefined; + const selectedSupervisionService = input.supervisionServiceId + ? await db.getOperationalRecapServiceById(input.supervisionServiceId) + : undefined; + const resolvedServiceRecipients = parseStoredRecipientEmails(selectedRecapService?.recipientEmails); + const resolvedRecipients = mergeRecipients(resolvedServiceRecipients, parseRecipients(input.recapRecipients)); + const resolvedServiceLabel = input.recapServiceLabel?.trim() || selectedRecapService?.label || undefined; + const resolvedSupervisionServiceRecipients = parseStoredRecipientEmails(selectedSupervisionService?.recipientEmails); + const resolvedSupervisionRecipients = mergeRecipients( + resolvedSupervisionServiceRecipients, + parseRecipients(input.supervisionRecipients) + ); + const resolvedSupervisionServiceLabel = + input.supervisionServiceLabel?.trim() || selectedSupervisionService?.label || undefined; + + if ((request.type === 'demande_salle' || request.type === 'demande_materiel_evenementiel') + && !input.dsuData?.responsableDSU?.trim()) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Le responsable DSU est obligatoire pour traiter cette demande', + }); + } + + if ((request.type === 'demande_salle' || request.type === 'demande_materiel_evenementiel') && input.status === 'validee') { + const financialDecision = input.dsuData?.materielEvent?.financialDecision; + if (!financialDecision?.financialMode) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Le mode financier est obligatoire pour valider une ${request.type === 'demande_salle' ? 'demande de salle' : 'demande de matériel'}`, + }); + } + } + + if (request.type === 'demande_materiel_evenementiel' && input.status === 'validee') { + const grantedItems = input.dsuData?.materielEvent?.itemsAccordes || {}; + const grantedQuantities = input.dsuData?.materielEvent?.quantitesAccordees || {}; + const invalidGrantedItem = materialEventItems.find((item) => { + if (!grantedItems[item.key]) return false; + return parseMaterialEventQuantity(grantedQuantities[item.key]) <= 0; + }); + + if (invalidGrantedItem) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `La quantité accordée est obligatoire pour ${invalidGrantedItem.label}`, + }); + } + } + + const canSendRecapEmails = await canSendOperationalEmails(); + const existingFinancialDecision = parseMaterialFinancialDecision(request.formData); + const parsedRequestFormData = parseRequestFormData(request.formData); + const resolvedSalleUsageType = request.type === 'demande_salle' + ? (input.dsuData?.salleReservation?.usageType || parsedRequestFormData.typeUsage || 'conventionne') + : undefined; + const resolvedSalleFrequency = request.type === 'demande_salle' + ? (input.dsuData?.salleReservation?.frequency || parsedRequestFormData.frequence || 'journee') + : undefined; + const computedSallePricing = request.type === 'demande_salle' + ? computeSallePricing({ + sallesIds: Array.isArray(parsedRequestFormData.sallesIds) + ? parsedRequestFormData.sallesIds.filter((value: unknown): value is string => typeof value === 'string' && value.trim().length > 0) + : [], + usageType: resolvedSalleUsageType as "conventionne" | "occasionnel", + frequency: resolvedSalleFrequency as "demi_journee" | "journee" | "mensuel", + dateReservation: parsedRequestFormData.dateReservation, + dateFinReservation: parsedRequestFormData.dateFinReservation, + }) + : null; + + if ( + request.type === 'demande_salle' + && input.dsuData?.materielEvent?.financialDecision?.financialMode === 'location_payante' + && (!computedSallePricing || computedSallePricing.unsupportedSalles.length > 0) + ) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: computedSallePricing?.unsupportedSalles?.length + ? `Tarification automatique indisponible pour : ${computedSallePricing.unsupportedSalles.map((item: { salleNom: string }) => item.salleNom).join(', ')}` + : 'La tarification automatique est indisponible pour cette demande de salle', + }); + } + + // If DSU data is provided, merge it into the existing formData + let updatedFormData: string | undefined; + if (input.dsuData && (request.type === 'demande_salle' || request.type === 'demande_materiel_evenementiel')) { + try { + const existingFormData = request.formData ? JSON.parse(request.formData) : {}; + if (request.type === 'demande_salle') { + existingFormData.typeUsage = resolvedSalleUsageType; + existingFormData.frequence = resolvedSalleFrequency; + } + const nextFinancialDecision = (request.type === 'demande_salle' || request.type === 'demande_materiel_evenementiel') + ? (() => { + const inputFinancial = input.dsuData?.materielEvent?.financialDecision; + if (input.status === 'refusee') { + return { + ...(existingFinancialDecision || { + financialMode: 'gratuite' as const, + depositRequired: false, + depositAmountCents: 0, + rentalAmountCents: 0, + pricingNotes: '', + }), + contractStatus: 'refusee' as const, + }; + } + if (!inputFinancial?.financialMode) { + return existingFinancialDecision; + } + const depositRequired = Boolean(inputFinancial.depositRequired); + const depositAmountCents = depositRequired ? Math.max(0, inputFinancial.depositAmountCents || 0) : 0; + const rentalAmountCents = inputFinancial.financialMode === 'location_payante' + ? request.type === 'demande_salle' && computedSallePricing + ? computedSallePricing.totalAmountCents + : Math.max(0, inputFinancial.rentalAmountCents || 0) + : 0; + return { + financialMode: inputFinancial.financialMode, + depositRequired, + depositAmountCents, + rentalAmountCents, + pricingNotes: inputFinancial.pricingNotes || '', + contractStatus: inputFinancial.contractStatus || (input.status === 'validee' ? 'a_generer' : 'annulee'), + }; + })() + : null; + + existingFormData.cadreDSU = { + ...input.dsuData, + ...(request.type === 'demande_salle' ? { + salleReservation: { + usageType: resolvedSalleUsageType, + frequency: resolvedSalleFrequency, + }, + } : {}), + responsableDSU: input.dsuData.responsableDSU?.trim(), + cautionRequise: nextFinancialDecision ? nextFinancialDecision.depositRequired : input.dsuData.cautionRequise, + montantCaution: nextFinancialDecision?.depositRequired + ? String((nextFinancialDecision.depositAmountCents || 0) / 100) + : input.dsuData.montantCaution, + rempliPar: ctx.user.id, + dateRemplissage: new Date().toISOString(), + ...((request.type === 'demande_salle' || request.type === 'demande_materiel_evenementiel') ? { + materielEvent: { + ...input.dsuData.materielEvent, + financialDecision: nextFinancialDecision || undefined, + }, + } : {}), + }; + if ( + resolvedRecipients.length + || resolvedServiceLabel + || resolvedSupervisionRecipients.length + || resolvedSupervisionServiceLabel + ) { + existingFormData.notificationTrace = { + serviceId: selectedRecapService?.id, + serviceLabel: resolvedServiceLabel, + recipients: resolvedRecipients, + supervisionServiceId: selectedSupervisionService?.id, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + supervisionRecipients: resolvedSupervisionRecipients, + processedBy: ctx.user.name || `Admin #${ctx.user.id}`, + processedAt: new Date().toISOString(), + channel: canSendRecapEmails ? 'smtp' : 'notification_only', + }; + } + updatedFormData = JSON.stringify(existingFormData); + if (request.type === 'demande_salle' && nextFinancialDecision) { + const texts = buildDefaultSalleWorkflowTexts(updatedFormData); + updatedFormData = mergeSalleWorkflowData(updatedFormData, { + usageType: resolvedSalleUsageType, + frequency: resolvedSalleFrequency, + pricing: computedSallePricing || undefined, + conditionsFinancieresText: CONDITIONS_FINANCIERES_SALLE_TEXT, + decisionAdministrativeText: texts.decisionAdministrativeText, + quoteStatus: existingFormData.salleWorkflow?.quoteStatus || 'a_preparer', + directorStatus: existingFormData.salleWorkflow?.directorStatus || 'a_transmettre', + }); + } + } catch (e) { + console.error('Failed to merge DSU data:', e); + } + } else if ( + resolvedRecipients.length + || resolvedServiceLabel + || resolvedSupervisionRecipients.length + || resolvedSupervisionServiceLabel + ) { + try { + const existingFormData = request.formData ? JSON.parse(request.formData) : {}; + existingFormData.notificationTrace = { + serviceId: selectedRecapService?.id, + serviceLabel: resolvedServiceLabel, + recipients: resolvedRecipients, + supervisionServiceId: selectedSupervisionService?.id, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + supervisionRecipients: resolvedSupervisionRecipients, + processedBy: ctx.user.name || `Admin #${ctx.user.id}`, + processedAt: new Date().toISOString(), + channel: canSendRecapEmails ? 'smtp' : 'notification_only', + }; + updatedFormData = JSON.stringify(existingFormData); + } catch (e) { + console.error('Failed to merge notification trace:', e); + } + } + + const association = request.associationId ? await db.getAssociationById(request.associationId) : null; + let effectiveStatus = input.status; + if (request.type === 'demande_salle' && input.status === 'validee') { + effectiveStatus = 'en_cours_traitement'; + } + + await db.updateRequest(input.id, { + status: effectiveStatus, + commentaireAdmin: input.commentaireAdmin, + montantAccorde: input.montantAccorde, + traitePar: ctx.user.id, + dateTraitement: input.status === 'refusee' ? new Date() : undefined, + ...(updatedFormData ? { formData: updatedFormData } : {}), + }); + + if (request.type === 'demande_salle' && input.status === 'validee') { + const refreshedRequest = await db.getRequestById(input.id); + if (!refreshedRequest) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande de salle introuvable après traitement' }); + } + const quoteWorkflow = await sendSalleQuoteWorkflow({ + request: refreshedRequest, + association: association || undefined, + req: ctx.req, + user: ctx.user, + }); + updatedFormData = quoteWorkflow.updatedFormData; + await db.updateRequest(input.id, { + status: effectiveStatus, + formData: updatedFormData, + traitePar: ctx.user.id, + }); + } + + let generatedContract: { + contractGeneratedAt: Date; + contractPdfKey: string; + contractPdfUrl: string; + contractPdfName: string; + } | null = null; + if ( + (request.type === 'demande_materiel_evenementiel' || request.type === 'demande_salle') + && input.status === 'validee' + && input.generateContract + && updatedFormData + && request.type !== 'demande_salle' + ) { + const refreshedRequest = await db.getRequestById(input.id); + const financialDecision = parseMaterialFinancialDecision(updatedFormData); + + if (refreshedRequest && financialDecision) { + generatedContract = await generateAndStoreMaterialConventionForRequest({ + request: { + ...refreshedRequest, + formData: updatedFormData, + }, + association: association || undefined, + decision: { + financialMode: financialDecision.financialMode, + depositRequired: financialDecision.depositRequired, + depositAmountCents: financialDecision.depositAmountCents, + rentalAmountCents: financialDecision.rentalAmountCents, + pricingNotes: financialDecision.pricingNotes, + }, + validatedByUserId: ctx.user.id, + }); + + updatedFormData = mergeMaterialFinancialDecision({ + rawFormData: updatedFormData, + decision: { + financialMode: financialDecision.financialMode, + depositRequired: financialDecision.depositRequired, + depositAmountCents: financialDecision.depositAmountCents, + rentalAmountCents: financialDecision.rentalAmountCents, + pricingNotes: financialDecision.pricingNotes, + contractStatus: 'generee', + contractPdfUrl: generatedContract.contractPdfUrl, + contractPdfName: generatedContract.contractPdfName, + contractGeneratedAt: generatedContract.contractGeneratedAt, + contractValidatedByUserId: ctx.user.id, + }, + }); + + await db.updateRequest(input.id, { + formData: updatedFormData, + }); + } + } + + const actionType = input.status === 'validee' ? 'validation' : input.status === 'refusee' ? 'refus' : 'changement_statut'; + await db.createRequestHistory({ + requestId: input.id, + action: actionType, + ancienStatut: request.status, + nouveauStatut: effectiveStatus, + userId: ctx.user.id, + commentaire: request.type === 'demande_salle' && input.status === 'validee' + ? (input.commentaireAdmin + ? `${input.commentaireAdmin}\n\nDevis et décision administrative prioritaire envoyés à l'association pour validation.` + : "Devis et décision administrative prioritaire envoyés à l'association pour validation.") + : input.commentaireAdmin, + }); + + await logAdminAction(ctx.user.id, actionType, 'request', input.id, { + status: effectiveStatus, + montantAccorde: input.montantAccorde, + dsuData: input.dsuData, + generateContract: Boolean(input.generateContract), + contractPdfName: generatedContract?.contractPdfName, + recapServiceId: selectedRecapService?.id, + recapRecipients: resolvedRecipients, + recapServiceLabel: resolvedServiceLabel, + supervisionServiceId: selectedSupervisionService?.id, + supervisionRecipients: resolvedSupervisionRecipients, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + }); + + // Envoyer le récapitulatif complet par email à tous les administrateurs + if ((input.status === 'validee' || input.status === 'refusee') && request.type !== 'demande_salle') { + try { + // Récupérer les documents joints + const allDocs = request.associationId ? await db.getDocumentsByAssociationId(request.associationId) : []; + let requestDocs: { name: string; type: string }[] = []; + if (request.formData) { + try { + const fd = JSON.parse(request.formData); + if (fd.documentsJoints && Array.isArray(fd.documentsJoints)) { + requestDocs = fd.documentsJoints.map((docId: number) => { + const doc = allDocs.find((d: any) => d.id === docId); + return doc ? { name: doc.nom, type: doc.type || 'Document' } : { name: `Document #${docId}`, type: 'Document' }; + }); + } + } catch (e) { /* ignore */ } + } + + // Récupérer la demande mise à jour (avec les données DSU mises à jour) + const updatedRequest = await db.getRequestById(input.id); + const reqForRecap = updatedRequest || request; + let pdfAttachment: + | { + filename: string; + content: Buffer; + contentType?: string; + } + | undefined; + + if (input.status === 'validee') { + try { + const pdfDocument = await generateRequestPdfDocument({ + ...reqForRecap, + status: input.status, + commentaireAdmin: input.commentaireAdmin ?? reqForRecap.commentaireAdmin, + dateTraitement: new Date(), + }); + + pdfAttachment = { + filename: pdfDocument.fileName, + content: pdfDocument.buffer, + contentType: pdfDocument.contentType, + }; + } catch (pdfError) { + console.warn( + `PDF non joint au mail opérationnel pour la demande #${input.id}:`, + pdfError + ); + } + } + + const recap = generateRequestRecapEmail({ + request: { + id: reqForRecap.id, + titre: reqForRecap.titre, + type: reqForRecap.type, + status: effectiveStatus, + description: reqForRecap.description, + montantDemande: reqForRecap.montantDemande, + montantAccorde: input.montantAccorde ?? reqForRecap.montantAccorde, + commentaireAdmin: input.commentaireAdmin ?? reqForRecap.commentaireAdmin, + formData: reqForRecap.formData, + dateSubmission: reqForRecap.dateSubmission, + dateTraitement: new Date(), + createdAt: reqForRecap.createdAt, + }, + association: association ? { + nomAssociation: association.nomAssociation, + siret: association.siret, + adresse: association.adresse, + codePostal: association.codePostal, + ville: association.ville, + telephone: association.telephone, + emailContact: association.emailContact, + nomRepresentant: association.nomRepresentant, + } : null, + traitePar: ctx.user.name || `Admin #${ctx.user.id}`, + documents: requestDocs.length > 0 ? requestDocs : undefined, + serviceLabel: resolvedServiceLabel, + }); + + try { + await notifyOwner(recap); + } catch (notificationError) { + console.warn('Notification propriétaire non envoyée:', notificationError); + } + + const recipients = resolvedRecipients; + if (recipients.length > 0) { + try { + const result = await sendOperationalEmail({ + to: recipients, + subject: recap.title, + text: recap.content, + html: recap.html, + replyTo: ctx.user.email || undefined, + fromName: ctx.user.name ? `${ctx.user.name} via Portail Associations` : "Portail Associations", + attachments: pdfAttachment ? [pdfAttachment] : undefined, + }); + + if (!result.sent) { + console.warn('Envoi email opérationnel non effectué:', result.reason); + } + } catch (mailError) { + console.error('Erreur lors de l’envoi du mail opérationnel:', mailError); + } + } + } catch (e) { + console.error('Erreur lors de l\'envoi du récapitulatif par email:', e); + // Ne pas bloquer le traitement si l'envoi échoue + } + } + + if (request.type === 'demande_materiel_evenementiel' && input.status === 'validee') { + await scheduleMaterialReturnFollowup({ + request: { + ...request, + status: input.status, + formData: updatedFormData || request.formData, + }, + serviceLabel: resolvedServiceLabel, + recipientEmails: resolvedRecipients, + supervisionServiceLabel: resolvedSupervisionServiceLabel, + supervisionRecipientEmails: resolvedSupervisionRecipients, + }); + } + + return { success: true }; + }), + + reopen: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + if (request.status !== 'validee' && request.status !== 'refusee') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Seules les demandes traitées peuvent être rouvertes' }); + } + + const reopenedComment = request.commentaireAdmin + ? `${request.commentaireAdmin}\n\nDemande rouverte par l'administration pour modification.` + : 'Demande rouverte par l\'administration pour modification.'; + + await db.updateRequest(input.id, { + status: 'information_complementaire', + commentaireAdmin: reopenedComment, + dateTraitement: null, + traitePar: null, + }); + + await db.createRequestHistory({ + requestId: input.id, + action: 'changement_statut', + ancienStatut: request.status, + nouveauStatut: 'information_complementaire', + userId: ctx.user.id, + commentaire: 'Réouverture de la demande pour permettre une nouvelle modification par l’association', + }); + + await logAdminAction(ctx.user.id, 'reopen_request', 'request', input.id, { + ancienStatut: request.status, + nouveauStatut: 'information_complementaire', + }); + + return { success: true }; + }), + + getHistory: adminProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + return db.getRequestHistoryByRequestId(input.id); + }), + + // Calendrier : récupérer toutes les réservations accordées + getApprovedReservations: salleReadProcedure.query(async () => { + return db.getApprovedReservations(); + }), + + getMaterialCalendar: salleReadProcedure.query(async () => { + const logisticsSettings = await getLogisticsSettings(); + const inventory = logisticsSettings.inventory; + const materialRequests = (await db.searchRequests({ + type: "demande_materiel_evenementiel", + status: "validee", + limit: 500, + })).data; + + const events = await Promise.all( + materialRequests.map(async (request) => { + const association = await db.getAssociationById(request.associationId); + const followup = await db.getMaterialReturnFollowupByRequestId(request.id); + const event = getMaterialRequestEventData(request, association); + const boardState = computeMaterialReturnBoardState({ + requestStatus: request.status, + restitutionDate: event.restitutionDate, + followup, + graceDays: logisticsSettings.materialReturnGraceDays, + }); + + return { + ...event, + boardState, + boardStateLabel: materialReturnBoardStateLabels[boardState], + logisticServiceLabel: followup?.serviceLabel || "", + logisticRecipients: parseStoredRecipientEmails(followup?.recipientEmails), + followupStatus: followup?.status || null, + issueFlag: Boolean(followup?.issueFlag), + litigationStatus: followup?.litigationStatus || "none", + finalPdfUrl: followup?.finalPdfUrl || followup?.signedFileUrl || null, + uploadLink: followup?.uploadToken ? `/materiel/restitution/${followup.uploadToken}` : null, + }; + }) + ); + + const followups = await Promise.all(materialRequests.map((request) => db.getMaterialReturnFollowupByRequestId(request.id))); + const blockedTotals = sumBlockedInventory(followups); + + const conflicts = events.flatMap((event) => + event.requestedItems.flatMap((item) => { + const capacity = inventory[item.key]; + const blockedQuantity = blockedTotals[item.key] || 0; + const effectiveCapacity = capacity === null ? null : Math.max(capacity - blockedQuantity, 0); + const overlaps = events.filter((other) => + other.requestId !== event.requestId + && other.requestedItems.some((otherItem) => otherItem.key === item.key && otherItem.quantity > 0) + && dateRangesOverlap(event.useStartDate, event.useEndDate, other.useStartDate, other.useEndDate) + ); + + const stockConflicts = effectiveCapacity === null || item.quantity <= 0 + ? [] + : (() => { + const reservedQuantity = overlaps.reduce((sum, other) => { + const overlappingItem = other.requestedItems.find((entry) => entry.key === item.key); + return sum + (overlappingItem?.quantity || 0); + }, item.quantity); + + if (reservedQuantity <= effectiveCapacity) return []; + return [{ + requestId: event.requestId, + materialKey: item.key, + materialLabel: item.label, + reservedQuantity, + capacity: effectiveCapacity, + message: `${item.label} réservé au-delà du stock disponible (${reservedQuantity}/${effectiveCapacity})`, + }]; + })(); + + const litigeConflicts = overlaps + .filter((other) => other.litigationStatus === "pending") + .map((other) => ({ + requestId: event.requestId, + materialKey: item.key, + materialLabel: item.label, + reservedQuantity: item.quantity, + capacity: effectiveCapacity, + message: `Attention, ${item.label} est planifié pour ce dossier alors qu'un prêt précédent (#${other.requestId}) est déclaré en litige ou dégradation.`, + })); + + return [...stockConflicts, ...litigeConflicts]; + }) + ); + + return { + inventory: materialEventItems.map((item) => ({ + key: item.key, + label: item.label, + capacity: inventory[item.key], + replacementValue: logisticsSettings.replacementValues[item.key], + blocked: blockedTotals[item.key] || 0, + effectiveCapacity: inventory[item.key] === null ? null : Math.max((inventory[item.key] || 0) - (blockedTotals[item.key] || 0), 0), + })), + events, + conflicts, + }; + }), + + getLogisticsPolicy: protectedProcedure.query(async () => { + return getLogisticsSettings(); + }), + + getMaterialAvailability: protectedProcedure + .input(z.object({ + dateDebut: z.string().optional(), + dateFin: z.string().optional(), + })) + .query(async ({ input }) => { + const logisticsSettings = await getLogisticsSettings(); + const inventory = logisticsSettings.inventory; + const allMaterialRequests = (await db.searchRequests({ + type: "demande_materiel_evenementiel", + limit: 500, + })).data; + const materialRequests = allMaterialRequests.filter((request) => + ACTIVE_MATERIAL_AVAILABILITY_STATUSES.has(request.status) + ); + const followups = await Promise.all(materialRequests.map((request) => db.getMaterialReturnFollowupByRequestId(request.id))); + const blockedTotals = sumBlockedInventory(followups); + const dateDebut = normalizeDateString(input.dateDebut); + const dateFin = normalizeDateString(input.dateFin || input.dateDebut); + if (!dateDebut || !dateFin) { + return { + graceDays: logisticsSettings.materialReturnGraceDays, + items: materialEventItems.map((item) => ({ + key: item.key, + label: item.label, + capacity: inventory[item.key], + replacementValue: logisticsSettings.replacementValues[item.key], + reserved: 0, + available: inventory[item.key] === null ? null : Math.max((inventory[item.key] || 0) - (blockedTotals[item.key] || 0), 0), + blocked: (blockedTotals[item.key] || 0) > 0, + blockedQuantity: blockedTotals[item.key] || 0, + })), + }; + } + + const reservations = materialRequests.map((request) => getMaterialRequestEventData(request, null)); + const items = materialEventItems.map((item) => { + const capacity = inventory[item.key]; + const blockedQuantity = blockedTotals[item.key] || 0; + const effectiveCapacity = capacity === null ? null : Math.max(capacity - blockedQuantity, 0); + const reserved = reservations.reduce((sum, reservation) => { + if (!dateRangesOverlap(dateDebut, dateFin, reservation.useStartDate, reservation.useEndDate)) { + return sum; + } + const reservedItem = reservation.requestedItems.find((entry) => entry.key === item.key); + return sum + (reservedItem?.quantity || 0); + }, 0); + const available = effectiveCapacity === null ? null : Math.max(effectiveCapacity - reserved, 0); + return { + key: item.key, + label: item.label, + capacity, + replacementValue: logisticsSettings.replacementValues[item.key], + reserved, + available, + blocked: (blockedQuantity > 0) || (effectiveCapacity !== null ? (available ?? 0) <= 0 : false), + blockedQuantity, + }; + }); + + return { items, graceDays: logisticsSettings.materialReturnGraceDays }; + }), + + // Modifier l'attribution des salles d'une réservation + changeSalles: adminProcedure + .input(z.object({ + id: z.number(), + newSallesIds: z.array(z.string()), + newSallesNoms: z.array(z.string()), + raison: z.string().min(1, 'Veuillez indiquer la raison du changement'), + })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + if (request.type !== 'demande_salle') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cette demande n\'est pas une demande de salle' }); + } + + // Parse existing formData + let formData: any = {}; + try { + formData = request.formData ? JSON.parse(request.formData) : {}; + } catch { + formData = {}; + } + + // Save old salles for notification + const anciennesSalles = formData.sallesSelectionnees || []; + + // Update salles + formData.sallesSelectionnees = input.newSallesNoms; + formData.sallesIds = input.newSallesIds; + + // Add change history to formData + if (!formData.historiqueChangementsSalles) { + formData.historiqueChangementsSalles = []; + } + formData.historiqueChangementsSalles.push({ + date: new Date().toISOString(), + anciennesSalles, + nouvellesSalles: input.newSallesNoms, + raison: input.raison, + parAdmin: ctx.user.id, + }); + + // Update in DB + await db.updateRequestSalles(input.id, JSON.stringify(formData)); + + // Log in request history + await db.createRequestHistory({ + requestId: input.id, + action: 'changement_salle', + ancienStatut: request.status, + nouveauStatut: request.status, + userId: ctx.user.id, + commentaire: `Changement de salle(s) : ${anciennesSalles.join(', ')} → ${input.newSallesNoms.join(', ')}. Raison : ${input.raison}`, + }); + + // Audit log + await logAdminAction(ctx.user.id, 'changement_salle', 'request', input.id, { + anciennesSalles, + nouvellesSalles: input.newSallesNoms, + raison: input.raison, + }); + + // Create admin notification for record + await db.createAdminNotification({ + type: 'changement_salle', + titre: `Changement de salle : ${request.titre}`, + message: `Salles modifiées de ${anciennesSalles.join(', ')} vers ${input.newSallesNoms.join(', ')}. Raison : ${input.raison}`, + lien: `/dashboard/requests/${input.id}`, + }); + + // Notify the association owner via platform notification + try { + const association = await db.getAssociationById(request.associationId); + const notifTitle = `Changement de salle pour votre réservation`; + const notifContent = `Bonjour ${association?.nomAssociation || 'Cher adhérent'},\n\nVotre réservation "${request.titre}" a fait l'objet d'un changement de salle.\n\nAnciennes salles : ${anciennesSalles.join(', ')}\nNouvelles salles : ${input.newSallesNoms.join(', ')}\n\nRaison : ${input.raison}\n\nCordialement,\nLa Direction des Services aux Usagers`; + + await notifyOwner({ title: notifTitle, content: notifContent }); + } catch (e) { + console.error('Failed to send notification for salle change:', e); + } + + return { success: true, anciennesSalles, nouvellesSalles: input.newSallesNoms }; + }), + + cancel: protectedProcedure + .input(z.object({ + id: z.number(), + raison: z.string().optional(), + })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association || request.associationId !== association.id) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Vous ne pouvez annuler que vos propres demandes' }); + } + + if (request.status === 'validee' || request.status === 'refusee' || request.status === 'annulee') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cette demande ne peut plus être annulée' }); + } + + const ancienStatut = request.status; + + await db.updateRequest(input.id, { + status: 'annulee', + dateTraitement: new Date(), + }); + + await db.createRequestHistory({ + requestId: input.id, + action: 'annulation', + ancienStatut: ancienStatut, + nouveauStatut: 'annulee', + userId: ctx.user.id, + commentaire: input.raison || 'Annulation par l\'association', + }); + + // Notify admin + await db.createAdminNotification({ + type: 'annulation_demande', + titre: `Demande annulée : ${request.titre}`, + message: `L'association "${association.nomAssociation}" a annulé sa demande "${request.titre}".${input.raison ? ` Raison : ${input.raison}` : ''}`, + lien: `/dashboard/requests/${input.id}`, + }); + + // Notify owner + try { + await notifyOwner({ + title: `❌ Demande annulée : ${request.titre}`, + content: `L'association "${association.nomAssociation}" a annulé sa demande "${request.titre}".\n\nType : ${request.type}\nAncien statut : ${ancienStatut}${input.raison ? `\nRaison : ${input.raison}` : ''}`, + }); + } catch (e) { + console.error('Failed to notify owner about cancellation:', e); + } + + await logAdminAction(ctx.user.id, 'annulation_demande', 'request', input.id, { + titre: request.titre, + type: request.type, + ancienStatut, + raison: input.raison, + }); + + return { success: true }; + }), + + delete: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + const request = await db.getRequestById(input.id); + if (!request) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Demande non trouvée' }); + } + + const association = await db.getAssociationByUserId(ctx.user.id); + if (!association || request.associationId !== association.id) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Vous ne pouvez supprimer que vos propres demandes' }); + } + + if (request.status === 'validee' || request.status === 'refusee') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Une demande déjà traitée ne peut pas être supprimée' }); + } + + await db.deleteRequest(input.id); + + await logAdminAction(ctx.user.id, 'suppression_demande', 'request', input.id, { + titre: request.titre, + type: request.type, + status: request.status, + }); + + return { success: true }; + }), + }), + + // ============== REQUEST TEMPLATE ROUTES ============== + template: router({ + listAll: publicProcedure.query(async () => { + return db.getAllRequestTemplates(); + }), + + getById: publicProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + return db.getRequestTemplateById(input.id); + }), + + getByType: publicProcedure + .input(z.object({ type: z.string() })) + .query(async ({ input }) => { + return db.getRequestTemplateByType(input.type); + }), + + create: adminProcedure + .input(z.object({ + type: z.enum(['subvention_fonctionnement', 'subvention_projet', 'agrement_jeunesse_education', 'agrement_sport', 'autorisation_occupation', 'demande_salle', 'demande_materiel_evenementiel', 'autre']), + nom: z.string().min(1), + description: z.string().optional(), + formSchema: z.string(), + documentsRequis: z.array(z.string()).optional(), + serviceDestinataire: z.string().optional(), + emailDestinataire: z.string().email().optional(), + delaiTraitementJours: z.number().optional(), + })) + .mutation(async ({ ctx, input }) => { + const id = await db.createRequestTemplate({ + ...input, + documentsRequis: input.documentsRequis ? JSON.stringify(input.documentsRequis) : null, + }); + await logAdminAction(ctx.user.id, 'creation', 'template', id); + return { id }; + }), + + update: adminProcedure + .input(z.object({ + id: z.number(), + nom: z.string().optional(), + description: z.string().optional(), + formSchema: z.string().optional(), + documentsRequis: z.array(z.string()).optional(), + serviceDestinataire: z.string().optional(), + emailDestinataire: z.string().email().optional(), + delaiTraitementJours: z.number().optional(), + actif: z.boolean().optional(), + })) + .mutation(async ({ ctx, input }) => { + await db.updateRequestTemplate(input.id, { + ...input, + documentsRequis: input.documentsRequis ? JSON.stringify(input.documentsRequis) : undefined, + }); + await logAdminAction(ctx.user.id, 'modification', 'template', input.id); + return { success: true }; + }), + }), + + // ============== RESPONSE TEMPLATE ROUTES ============== + responseTemplate: router({ + listAll: adminProcedure.query(async () => { + return db.getAllResponseTemplates(); + }), + + getById: adminProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + return db.getResponseTemplateById(input.id); + }), + + create: adminProcedure + .input(z.object({ + nom: z.string().min(1), + type: z.enum(['validation', 'refus', 'information_complementaire', 'autre']), + sujet: z.string().optional(), + contenu: z.string().min(1), + })) + .mutation(async ({ ctx, input }) => { + const id = await db.createResponseTemplate(input); + await logAdminAction(ctx.user.id, 'creation', 'response_template', id); + return { id }; + }), + + update: adminProcedure + .input(z.object({ + id: z.number(), + nom: z.string().optional(), + type: z.enum(['validation', 'refus', 'information_complementaire', 'autre']).optional(), + sujet: z.string().optional(), + contenu: z.string().optional(), + })) + .mutation(async ({ ctx, input }) => { + await db.updateResponseTemplate(input.id, input); + await logAdminAction(ctx.user.id, 'modification', 'response_template', input.id); + return { success: true }; + }), + + delete: adminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + await db.deleteResponseTemplate(input.id); + await logAdminAction(ctx.user.id, 'suppression', 'response_template', input.id); + return { success: true }; + }), + }), + + operationalRecapService: router({ + listAll: logisticsProcedure.query(async () => { + // Répertoire utilisé pour l'assignation terrain et le suivi interne. + // On le réserve aux administrateurs logistiques et au super administrateur. + const services = await db.getAllOperationalRecapServices(); + return services.map(service => ({ + ...service, + usage: service.usage === "controle" ? "controle" : "terrain", + recipientEmails: parseStoredRecipientEmails(service.recipientEmails), + })); + }), + + getById: logisticsProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + const service = await db.getOperationalRecapServiceById(input.id); + return serializeOperationalRecapService(service); + }), + + create: logisticsProcedure + .input(z.object({ + label: z.string().min(1), + usage: z.enum(["terrain", "controle"]), + description: z.string().optional(), + recipientEmails: z.array(z.string().email()).min(1), + actif: z.boolean().optional(), + })) + .mutation(async ({ ctx, input }) => { + const id = await db.createOperationalRecapService({ + label: input.label.trim(), + usage: input.usage, + description: input.description?.trim() || null, + recipientEmails: JSON.stringify(parseRecipients(input.recipientEmails)), + actif: input.actif ?? true, + }); + await logAdminAction(ctx.user.id, 'creation', 'operational_recap_service', id, { + label: input.label.trim(), + usage: input.usage, + recipientEmails: parseRecipients(input.recipientEmails), + }); + return { id }; + }), + + update: logisticsProcedure + .input(z.object({ + id: z.number(), + label: z.string().min(1).optional(), + usage: z.enum(["terrain", "controle"]).optional(), + description: z.string().optional(), + recipientEmails: z.array(z.string().email()).optional(), + actif: z.boolean().optional(), + })) + .mutation(async ({ ctx, input }) => { + await db.updateOperationalRecapService(input.id, { + ...(input.label !== undefined ? { label: input.label.trim() } : {}), + ...(input.usage !== undefined ? { usage: input.usage } : {}), + ...(input.description !== undefined ? { description: input.description.trim() || null } : {}), + ...(input.recipientEmails !== undefined ? { recipientEmails: JSON.stringify(parseRecipients(input.recipientEmails)) } : {}), + ...(input.actif !== undefined ? { actif: input.actif } : {}), + }); + await logAdminAction(ctx.user.id, 'modification', 'operational_recap_service', input.id, { + label: input.label?.trim(), + usage: input.usage, + recipientEmails: input.recipientEmails ? parseRecipients(input.recipientEmails) : undefined, + actif: input.actif, + }); + return { success: true }; + }), + + delete: logisticsProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ ctx, input }) => { + await db.deleteOperationalRecapService(input.id); + await logAdminAction(ctx.user.id, 'suppression', 'operational_recap_service', input.id); + return { success: true }; + }), + }), + + // ============== ADMIN USER MANAGEMENT ============== + adminUsers: router({ + listAll: superAdminProcedure.query(async () => { + return db.getAdminUsers(); + }), + + create: superAdminProcedure + .input(z.object({ + name: z.string().trim().min(2, "Le nom est obligatoire"), + email: z.string().trim().email("Adresse email invalide"), + password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères").optional(), + role: z.enum(['accueil', 'service_terrain', 'logistique_controle', 'admin', 'directrice', 'super_admin']), + canManageLogistics: z.boolean().optional(), + })) + .mutation(async ({ ctx, input }) => { + const normalizedEmail = input.email.trim().toLowerCase(); + const existingUser = await db.getUserByEmail(normalizedEmail); + + if (existingUser) { + const updatePayload: Record = { + name: input.name.trim(), + email: normalizedEmail, + role: input.role, + mfaEnabled: isInternalRole(input.role) ? true : existingUser.mfaEnabled, + canManageLogistics: + input.role === "super_admin" + || input.role === "logistique_controle" + ? true + : input.role === "admin" + ? Boolean(input.canManageLogistics) + : false, + delegatedSalleSignerUserId: null, + isActive: true, + }; + + if (existingUser.loginMethod === "local_jwt" || existingUser.loginMethod === "bootstrap_jwt" || existingUser.passwordHash) { + updatePayload.openId = normalizedEmail; + } + + await db.updateUser(existingUser.id, updatePayload); + + await logAdminAction(ctx.user.id, 'creation', 'user', existingUser.id, { + email: normalizedEmail, + role: input.role, + canManageLogistics: + input.role === "super_admin" + || input.role === "logistique_controle" + ? true + : input.role === "admin" + ? Boolean(input.canManageLogistics) + : false, + reusedExistingAccount: true, + }); + + return { success: true, id: existingUser.id, reusedExistingAccount: true }; + } + + const generatedPassword = randomBytes(24).toString("base64url"); + const createdUser = await registerLocalUser({ + name: input.name, + email: normalizedEmail, + password: input.password?.trim() || generatedPassword, + }); + + await db.updateUser(createdUser.id, { + name: input.name.trim(), + email: normalizedEmail, + role: input.role, + mfaEnabled: isInternalRole(input.role), + canManageLogistics: + input.role === "super_admin" + || input.role === "logistique_controle" + ? true + : input.role === "admin" + ? Boolean(input.canManageLogistics) + : false, + delegatedSalleSignerUserId: null, + isActive: true, + loginMethod: 'local_jwt', + }); + + await logAdminAction(ctx.user.id, 'creation', 'user', createdUser.id, { + email: normalizedEmail, + role: input.role, + canManageLogistics: + input.role === "super_admin" + || input.role === "logistique_controle" + ? true + : input.role === "admin" + ? Boolean(input.canManageLogistics) + : false, + reusedExistingAccount: false, + }); + + return { success: true, id: createdUser.id, reusedExistingAccount: false }; + }), + + update: superAdminProcedure + .input(z.object({ + id: z.number(), + name: z.string().trim().min(2, "Le nom est obligatoire"), + email: z.string().trim().email("Adresse email invalide"), + role: z.enum(['user', 'accueil', 'service_terrain', 'logistique_controle', 'admin', 'directrice', 'super_admin']), + isActive: z.boolean(), + canManageLogistics: z.boolean(), + password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères").optional(), + })) + .mutation(async ({ ctx, input }) => { + const existingUser = await db.getUserById(input.id); + if (!existingUser) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Utilisateur introuvable' }); + } + + if (input.id === ctx.user.id && (!input.isActive || input.role !== 'super_admin')) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Tu ne peux pas retirer tes propres droits super administrateur ni désactiver ton compte.', + }); + } + + if (existingUser.role === 'super_admin' && (input.role !== 'super_admin' || !input.isActive)) { + const adminUsers = await db.getAdminUsers(); + const activeSuperAdmins = adminUsers.filter((user) => user.role === 'super_admin' && user.isActive); + if (activeSuperAdmins.length <= 1 && activeSuperAdmins.some((user) => user.id === input.id)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Le dernier super administrateur actif ne peut pas être rétrogradé ou désactivé.', + }); + } + } + + const normalizedEmail = input.email.trim().toLowerCase(); + const existingByEmail = await db.getUserByEmail(normalizedEmail); + if (existingByEmail && existingByEmail.id !== input.id) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'Cette adresse email est déjà utilisée par un autre compte.', + }); + } + + const updatePayload: Record = { + name: input.name.trim(), + email: normalizedEmail, + role: input.role, + mfaEnabled: isInternalRole(input.role) ? true : existingUser.mfaEnabled, + canManageLogistics: + input.role === "super_admin" + || input.role === "logistique_controle" + ? true + : input.role === "admin" + ? input.canManageLogistics + : false, + delegatedSalleSignerUserId: input.role === "directrice" ? existingUser.delegatedSalleSignerUserId ?? null : null, + isActive: input.isActive, + }; + + if (existingUser.loginMethod === 'local_jwt' || existingUser.loginMethod === 'bootstrap_jwt' || existingUser.passwordHash) { + updatePayload.openId = normalizedEmail; + } + + if (input.password?.trim()) { + updatePayload.passwordHash = await hashLocalPassword(input.password.trim()); + updatePayload.loginMethod = 'local_jwt'; + } + + await db.updateUser(input.id, updatePayload); + + await logAdminAction(ctx.user.id, 'modification', 'user', input.id, { + email: normalizedEmail, + role: input.role, + canManageLogistics: + input.role === "super_admin" + || input.role === "logistique_controle" + ? true + : input.role === "admin" + ? input.canManageLogistics + : false, + isActive: input.isActive, + passwordReset: Boolean(input.password?.trim()), + }); + + return { success: true }; + }), + + updateRole: superAdminProcedure + .input(z.object({ + id: z.number(), + role: z.enum(['user', 'accueil', 'service_terrain', 'logistique_controle', 'admin', 'directrice', 'super_admin']), + })) + .mutation(async ({ ctx, input }) => { + if (input.id === ctx.user.id && input.role !== 'super_admin') { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Tu ne peux pas retirer tes propres droits super administrateur.', + }); + } + const existingUser = await db.getUserById(input.id); + if (!existingUser) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Utilisateur introuvable' }); + } + if (existingUser.role === 'super_admin' && input.role !== 'super_admin') { + const adminUsers = await db.getAdminUsers(); + const activeSuperAdmins = adminUsers.filter((user) => user.role === 'super_admin' && user.isActive); + if (activeSuperAdmins.length <= 1 && activeSuperAdmins.some((user) => user.id === input.id)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Le dernier super administrateur actif ne peut pas être rétrogradé.', + }); + } + } + await db.updateUser(input.id, { + role: input.role, + mfaEnabled: isInternalRole(input.role) ? true : existingUser.mfaEnabled, + canManageLogistics: + input.role === "super_admin" + || input.role === "logistique_controle" + ? true + : input.role === "admin" + ? existingUser.canManageLogistics + : false, + delegatedSalleSignerUserId: input.role === "directrice" ? existingUser.delegatedSalleSignerUserId ?? null : null, + }); + await logAdminAction(ctx.user.id, 'changement_role', 'user', input.id, { + role: input.role, + canManageLogistics: + input.role === "super_admin" + || input.role === "logistique_controle" + ? true + : input.role === "admin" + ? existingUser.canManageLogistics + : false, + }); + return { success: true }; + }), + + toggleActive: superAdminProcedure + .input(z.object({ + id: z.number(), + isActive: z.boolean(), + })) + .mutation(async ({ ctx, input }) => { + if (input.id === ctx.user.id && !input.isActive) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Tu ne peux pas désactiver ton propre compte.', + }); + } + const existingUser = await db.getUserById(input.id); + if (!existingUser) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Utilisateur introuvable' }); + } + if (existingUser.role === 'super_admin' && !input.isActive) { + const adminUsers = await db.getAdminUsers(); + const activeSuperAdmins = adminUsers.filter((user) => user.role === 'super_admin' && user.isActive); + if (activeSuperAdmins.length <= 1 && activeSuperAdmins.some((user) => user.id === input.id)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Le dernier super administrateur actif ne peut pas être désactivé.', + }); + } + } + await db.updateUser(input.id, { isActive: input.isActive }); + await logAdminAction(ctx.user.id, input.isActive ? 'activation' : 'desactivation', 'user', input.id); + return { success: true }; + }), + + delete: superAdminProcedure + .input(z.object({ + id: z.number(), + })) + .mutation(async ({ ctx, input }) => { + if (input.id === ctx.user.id) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Tu ne peux pas supprimer ton propre compte.', + }); + } + + const existingUser = await db.getUserById(input.id); + if (!existingUser) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Utilisateur introuvable' }); + } + + if (existingUser.role === 'super_admin' && existingUser.isActive) { + const adminUsers = await db.getAdminUsers(); + const activeSuperAdmins = adminUsers.filter((user) => user.role === 'super_admin' && user.isActive); + if (activeSuperAdmins.length <= 1 && activeSuperAdmins.some((user) => user.id === input.id)) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Le dernier super administrateur actif ne peut pas être supprimé.', + }); + } + } + + await db.deleteUser(input.id); + await logAdminAction(ctx.user.id, 'suppression', 'user', input.id, { + email: existingUser.email, + role: existingUser.role, + ipAddress: extractClientIp(ctx.req), + }); + return { success: true }; + }), + }), + + // ============== AUDIT LOG ============== + auditLog: router({ + list: adminProcedure + .input(z.object({ + userId: z.number().optional(), + entityType: z.string().optional(), + entityId: z.number().optional(), + limit: z.number().optional(), + offset: z.number().optional(), + })) + .query(async ({ input }) => { + return db.getAuditLogs(input); + }), + }), + + compliance: router({ + getSnapshot: adminProcedure.query(async () => { + const [auditSummary, adminUsers, allUsers, operationalTourSettings, retentionReportRaw] = await Promise.all([ + db.getAuditLogs({ limit: 1 }), + db.getAdminUsers(), + db.getAllUsers(), + getOperationalTourSettings(), + db.getPortalSetting("retention.lastReport"), + ]); + + const activeAdmins = adminUsers.filter((user) => user.isActive); + const internalUsers = allUsers.filter((user) => isInternalRole(user.role)); + const activeInternalUsers = internalUsers.filter((user) => user.isActive); + const internalUsersWithTotp = activeInternalUsers.filter( + (user) => user.mfaMethod === "authenticator_app" && user.mfaTotpSecretEncrypted + ); + const superAdminUsers = activeInternalUsers.filter((user) => user.role === "super_admin"); + const superAdminUsersWithTotp = superAdminUsers.filter( + (user) => user.mfaMethod === "authenticator_app" && user.mfaTotpSecretEncrypted + ); + const retentionReport = retentionReportRaw ? JSON.parse(retentionReportRaw) as { + generatedAt?: string; + purgedUsers?: number; + pendingDeletionCount?: number; + purgedTotalCount?: number; + } : null; + const secondaryTarget = operationalTourSettings.targets.secondary.baseUrl.trim(); + const checks = { + technique: [ + buildComplianceItem( + "audit-log", + "Journalisation des actions sensibles", + auditSummary.total > 0 ? "implemented" : "partial", + auditSummary.total > 0 + ? "Le portail dispose d'un journal d'audit exploitable dans l'administration." + : "Le mecanisme existe mais aucun evenement n'a encore ete confirme dans l'historique.", + `Entrees constatees: ${auditSummary.total}.` + ), + buildComplianceItem( + "preprod", + "Separation production / preproduction", + secondaryTarget ? "implemented" : "partial", + secondaryTarget + ? "Une cible secondaire est configuree pour les controles." + : "La production est pilotee mais la cible secondaire doit encore etre renseignee." + ), + buildComplianceItem( + "password-hash", + "Hachage des mots de passe", + "partial", + "Le portail gere la migration de hachage et preferera Argon2id quand le runtime serveur le permet, avec repli securise sur scrypt pour l'environnement actuel.", + "Compatibilite legacy maintenue pour les anciens hachages scrypt." + ), + buildComplianceItem( + "mfa", + "Authentification multifacteur", + superAdminUsers.length === superAdminUsersWithTotp.length && activeInternalUsers.length > 0 + ? "implemented" + : activeInternalUsers.length > 0 + ? "partial" + : "missing", + "Le MFA est impose pour les roles internes. Authenticator est la methode cible, avec repli email encore tolere pour certains roles en phase transitoire.", + `Comptes internes actifs: ${activeInternalUsers.length}. Authenticator actif: ${internalUsersWithTotp.length}. Super administrateurs alignes TOTP: ${superAdminUsersWithTotp.length}/${superAdminUsers.length}.` + ), + buildComplianceItem( + "lockout", + "Blocage apres tentatives repeteees", + "implemented", + "Le portail verrouille temporairement un compte apres plusieurs echecs de connexion ou de code MFA." + ), + buildComplianceItem( + "backup-governance", + "Sauvegardes et restauration", + "partial", + "La politique de sauvegarde, la restauration et les objectifs RPO/RTO sont documentes. Les revues hors application et les tests trimestriels restent a tracer regulierement.", + "Strategie cible: 3-2-1, RPO 1 heure, RTO 4 heures." + ), + ], + juridique: [ + buildComplianceItem( + "privacy-pages", + "Pages legales de base publiees", + "implemented", + "La confidentialite, les mentions legales, les cookies et les CGU sont maintenant exposes dans le portail." + ), + buildComplianceItem( + "consent", + "Recueil de consentement explicite", + "implemented", + "Le portail journalise une validation explicite de confidentialite sur plusieurs parcours critiques." + ), + buildComplianceItem( + "retention", + "Politique de conservation formalisee", + "partial", + "Une politique de retention et une procedure de purge planifiee sont maintenant documentees. Le moteur de purge produit un rapport automatique minimal, mais la couverture complete par categorie reste a etendre." + , + retentionReport + ? `Dernier rapport retention: ${retentionReport.generatedAt || "n/a"} • purges du run: ${retentionReport.purgedUsers ?? 0} • comptes en attente: ${retentionReport.pendingDeletionCount ?? 0}.` + : "Aucun rapport automatique de retention n'a encore ete genere." + ), + buildComplianceItem( + "user-rights", + "Export et suppression en libre-service", + "implemented", + "L'espace utilisateur permet l'export JSON des donnees et une suppression planifiee avec purge differée a 30 jours." + ), + ], + organisation: [ + buildComplianceItem( + "roles", + "Gestion des roles internes", + activeAdmins.length > 0 ? "implemented" : "partial", + "Le portail segmente deja les acces par roles et habilitations internes.", + `Comptes internes actifs observes: ${activeAdmins.length}.` + ), + buildComplianceItem( + "incident-procedure", + "Procedure de gestion des incidents", + "partial", + "La procedure incidents est documentee avec classification, confinement, investigation, violation RGPD et revue post-incident. Elle doit encore etre adoptee et testee en exploitation." + ), + buildComplianceItem( + "register-processing", + "Registre des traitements", + "partial", + "Le portail dispose d'un socle de registre des traitements. Les traitements réels, responsables, durées, bases légales et justificatifs doivent encore être complétés et validés par la CCDS." + ), + buildComplianceItem( + "supplier-register", + "Registre des sous-traitants", + "partial", + "Le registre fournisseurs, le modele DPA et la politique de revue annuelle sont documentes. Les fournisseurs reels et justificatifs contractuels doivent encore etre completes." + ), + buildComplianceItem( + "mfa-policy", + "Politique MFA obligatoire par role", + "implemented", + "Une politique MFA interne explicite distingue les roles soumis a MFA obligatoire, la methode cible Authenticator et les phases de transition.", + "Super administrateur: Authenticator uniquement cible. Autres roles internes: MFA obligatoire avec bascule progressive vers TOTP." + ), + ], + }; + + const allChecks = [...checks.technique, ...checks.juridique, ...checks.organisation]; + const implementedCount = allChecks.filter((item) => item.status === "implemented").length; + const partialCount = allChecks.filter((item) => item.status === "partial").length; + const missingCount = allChecks.filter((item) => item.status === "missing").length; + + return { + generatedAt: new Date().toISOString(), + platform: { + totalUsers: allUsers.length, + activeInternalUsers: activeAdmins.length, + auditEntries: auditSummary.total, + secondaryConfigured: Boolean(secondaryTarget), + }, + mfaPolicy: { + targetMethod: "authenticator_app", + fallbackMethod: "email_otp", + internalRoles: activeInternalUsers.map((user) => user.role), + totpCoverage: { + activeInternalUsers: activeInternalUsers.length, + authenticatorEnabled: internalUsersWithTotp.length, + superAdmins: superAdminUsers.length, + superAdminsAligned: superAdminUsersWithTotp.length, + }, + phases: [ + "Phase 1 : MFA obligatoire pour tous les roles internes, Authenticator cible, email encore tolere hors super_admin.", + "Phase 2 : Authenticator obligatoire pour super_admin, admin et directrice.", + "Phase 3 : Authenticator obligatoire pour tous les roles internes.", + ], + }, + summary: { + implementedCount, + partialCount, + missingCount, + readinessLabel: + missingCount === 0 + ? "Socle avance mais encore a faire auditer" + : missingCount <= 3 + ? "Socle partiellement aligne" + : "Programme de mise en conformite a poursuivre", + }, + checks, + nextSteps: [ + "Initialiser Authenticator sur les comptes super_admin, admin et directrice avant enforcement complet en phase 2.", + "Renseigner les fournisseurs reels, signer les DPA et consigner la revue annuelle de chaque sous-traitant.", + "Etendre la retention automatisee aux autres categories documentaires et produire un rapport mensuel DPO.", + "Industrialiser les tests de restauration, les revues de sauvegarde et la revue post-incident hors application.", + ], + disclaimer: "Cette vue aide au pilotage interne. Elle ne remplace ni un audit juridique RGPD ni une certification ISO 27001.", + }; + }), + getOperations: adminProcedure.query(async () => { + const [operations, allUsers] = await Promise.all([ + getComplianceOperations(), + db.getAllUsers(), + ]); + + const legalHoldCount = allUsers.filter((user) => user.legalHold).length; + const pendingPurgeCount = allUsers.filter((user) => user.deletionRequestedAt && !user.purgedAt).length; + const purgedCount = allUsers.filter((user) => user.purgedAt).length; + const sortedBackupRecords = [...operations.backupRecords] + .filter((entry) => Boolean(entry.lastSuccessAt)) + .sort((a, b) => (b.lastSuccessAt || "").localeCompare(a.lastSuccessAt || "", "fr")); + const sortedRestoreTests = operations.restoreTests + .filter((entry) => entry.testedAt) + .sort((a, b) => (b.testedAt || "").localeCompare(a.testedAt || "", "fr")); + const latestBackupSuccess = sortedBackupRecords[0] || null; + const latestBackupFailure = [...operations.backupRecords] + .filter((entry) => entry.lastFailureAt) + .sort((a, b) => (b.lastFailureAt || "").localeCompare(a.lastFailureAt || "", "fr"))[0] || null; + const latestRestoreTest = sortedRestoreTests[0] || null; + const latestSuccessfulRestore = sortedRestoreTests.find((entry) => entry.result === "success") || null; + const latestBackupStatus = formatRecencyStatus(parseComplianceDate(latestBackupSuccess?.lastSuccessAt || null), 7); + const latestRestoreStatus = formatRecencyStatus(parseComplianceDate(latestRestoreTest?.testedAt || null), 90); + const proofCoverageCount = operations.backupRecords.filter((entry) => Boolean(entry.proofLabel || entry.proofUrl)).length; + const restoreProofCoverageCount = operations.restoreTests.filter((entry) => Boolean(entry.proofLabel)).length; + const confidenceStatus = + latestBackupStatus.status === "missing" || latestRestoreStatus.status === "missing" + ? "partial" + : latestBackupStatus.status === "warning" || latestRestoreStatus.status === "warning" + ? "warning" + : "ok"; + + return { + ...operations, + metrics: { + legalHoldCount, + pendingPurgeCount, + purgedCount, + }, + exploitation: { + latestBackupSuccess, + latestBackupFailure, + latestRestoreTest, + latestSuccessfulRestore, + latestBackupStatus, + latestRestoreStatus, + proofCoverageCount, + restoreProofCoverageCount, + confidenceStatus, + alerts: [ + operations.backupRecords.length === 0 + ? "Aucune preuve de sauvegarde n’est encore historisée dans le portail." + : null, + operations.restoreTests.length === 0 + ? "Aucun test de restauration n’est encore historisé dans le pilotage." + : null, + latestBackupStatus.status === "warning" && latestBackupStatus.ageDays !== null + ? `La dernière sauvegarde réussie visible date de ${latestBackupStatus.ageDays} jour(s).` + : null, + latestRestoreStatus.status === "warning" && latestRestoreStatus.ageDays !== null + ? `Le dernier test de restauration visible date de ${latestRestoreStatus.ageDays} jour(s).` + : null, + ].filter((value): value is string => Boolean(value)), + }, + }; + }), + bootstrapOperations: adminProcedure.mutation(async ({ ctx }) => { + const existing = await getComplianceOperations(); + const bootstrap = await buildComplianceBootstrapPayload(existing); + + const mergedSuppliers = mergeComplianceEntriesById(existing.suppliers, bootstrap.suppliers); + const mergedDpas = mergeComplianceEntriesById(existing.dpas, bootstrap.dpas); + const mergedEvidence = mergeComplianceEntriesById(existing.evidenceCenter, bootstrap.evidenceCenter); + + await Promise.all([ + db.setPortalSetting( + COMPLIANCE_SUPPLIERS_SETTING_KEY, + JSON.stringify(mergedSuppliers.entries.sort((a, b) => a.supplierName.localeCompare(b.supplierName, "fr"))), + "Registre fournisseurs et sous-traitants" + ), + db.setPortalSetting( + COMPLIANCE_DPA_SETTING_KEY, + JSON.stringify(mergedDpas.entries.sort((a, b) => a.supplierName.localeCompare(b.supplierName, "fr"))), + "Suivi des DPA et obligations sous-traitants" + ), + db.setPortalSetting( + COMPLIANCE_EVIDENCE_SETTING_KEY, + JSON.stringify(mergedEvidence.entries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt, "fr"))), + "Centre de conformité et preuves d'audit" + ), + ]); + + const summary: ComplianceBootstrapSummary = { + suppliersAdded: mergedSuppliers.addedCount, + dpasAdded: mergedDpas.addedCount, + evidenceAdded: mergedEvidence.addedCount, + notes: bootstrap.notes, + }; + + await logAdminAction(ctx.user.id, "modification", "compliance_bootstrap", undefined, { + ...summary, + ipAddress: extractClientIp(ctx.req), + }); + + return summary; + }), + upsertSupplier: adminProcedure + .input(complianceSupplierSchema.omit({ id: true }).extend({ id: z.string().trim().optional() })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + const nextEntry = complianceSupplierSchema.parse({ + ...input, + id: input.id?.trim() || nanoid(10), + }); + const nextEntries = [ + nextEntry, + ...operations.suppliers.filter((entry) => entry.id !== nextEntry.id), + ].sort((a, b) => a.supplierName.localeCompare(b.supplierName, "fr")); + await db.setPortalSetting( + COMPLIANCE_SUPPLIERS_SETTING_KEY, + JSON.stringify(nextEntries), + "Registre fournisseurs et sous-traitants" + ); + await logAdminAction(ctx.user.id, "modification", "compliance_supplier", undefined, { + supplierId: nextEntry.id, + supplierName: nextEntry.supplierName, + ipAddress: extractClientIp(ctx.req), + }); + return nextEntry; + }), + deleteSupplier: adminProcedure + .input(z.object({ id: z.string().trim().min(1) })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + await db.setPortalSetting( + COMPLIANCE_SUPPLIERS_SETTING_KEY, + JSON.stringify(operations.suppliers.filter((entry) => entry.id !== input.id)), + "Registre fournisseurs et sous-traitants" + ); + await logAdminAction(ctx.user.id, "suppression", "compliance_supplier", undefined, { + supplierId: input.id, + ipAddress: extractClientIp(ctx.req), + }); + return { success: true }; + }), + upsertDpa: adminProcedure + .input(complianceDpaSchema.omit({ id: true }).extend({ id: z.string().trim().optional() })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + const nextEntry = complianceDpaSchema.parse({ + ...input, + id: input.id?.trim() || nanoid(10), + }); + const nextEntries = [ + nextEntry, + ...operations.dpas.filter((entry) => entry.id !== nextEntry.id), + ].sort((a, b) => a.supplierName.localeCompare(b.supplierName, "fr")); + await db.setPortalSetting( + COMPLIANCE_DPA_SETTING_KEY, + JSON.stringify(nextEntries), + "Suivi des DPA et obligations sous-traitants" + ); + await logAdminAction(ctx.user.id, "modification", "compliance_dpa", undefined, { + dpaId: nextEntry.id, + supplierName: nextEntry.supplierName, + status: nextEntry.status, + ipAddress: extractClientIp(ctx.req), + }); + return nextEntry; + }), + deleteDpa: adminProcedure + .input(z.object({ id: z.string().trim().min(1) })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + await db.setPortalSetting( + COMPLIANCE_DPA_SETTING_KEY, + JSON.stringify(operations.dpas.filter((entry) => entry.id !== input.id)), + "Suivi des DPA et obligations sous-traitants" + ); + await logAdminAction(ctx.user.id, "suppression", "compliance_dpa", undefined, { + dpaId: input.id, + ipAddress: extractClientIp(ctx.req), + }); + return { success: true }; + }), + upsertBackupRecord: adminProcedure + .input(complianceBackupRecordSchema.omit({ id: true }).extend({ id: z.string().trim().optional() })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + const nextEntry = complianceBackupRecordSchema.parse({ + ...input, + id: input.id?.trim() || nanoid(10), + }); + const backupRecords = [ + nextEntry, + ...operations.backupRecords.filter((entry) => entry.id !== nextEntry.id), + ].sort((a, b) => a.scope.localeCompare(b.scope, "fr")); + await saveComplianceBackups({ + backupRecords, + restoreTests: operations.restoreTests, + }); + await logAdminAction(ctx.user.id, "modification", "compliance_backup", undefined, { + backupId: nextEntry.id, + scope: nextEntry.scope, + ipAddress: extractClientIp(ctx.req), + }); + return nextEntry; + }), + deleteBackupRecord: adminProcedure + .input(z.object({ id: z.string().trim().min(1) })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + await saveComplianceBackups({ + backupRecords: operations.backupRecords.filter((entry) => entry.id !== input.id), + restoreTests: operations.restoreTests, + }); + await logAdminAction(ctx.user.id, "suppression", "compliance_backup", undefined, { + backupId: input.id, + ipAddress: extractClientIp(ctx.req), + }); + return { success: true }; + }), + upsertRestoreTest: adminProcedure + .input(complianceRestoreTestSchema.omit({ id: true }).extend({ id: z.string().trim().optional() })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + const nextEntry = complianceRestoreTestSchema.parse({ + ...input, + id: input.id?.trim() || nanoid(10), + }); + const restoreTests = [ + nextEntry, + ...operations.restoreTests.filter((entry) => entry.id !== nextEntry.id), + ].sort((a, b) => (b.testedAt || "").localeCompare(a.testedAt || "", "fr")); + await saveComplianceBackups({ + backupRecords: operations.backupRecords, + restoreTests, + }); + await logAdminAction(ctx.user.id, "modification", "compliance_restore_test", undefined, { + restoreTestId: nextEntry.id, + result: nextEntry.result, + ipAddress: extractClientIp(ctx.req), + }); + return nextEntry; + }), + deleteRestoreTest: adminProcedure + .input(z.object({ id: z.string().trim().min(1) })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + await saveComplianceBackups({ + backupRecords: operations.backupRecords, + restoreTests: operations.restoreTests.filter((entry) => entry.id !== input.id), + }); + await logAdminAction(ctx.user.id, "suppression", "compliance_restore_test", undefined, { + restoreTestId: input.id, + ipAddress: extractClientIp(ctx.req), + }); + return { success: true }; + }), + upsertRetentionReport: adminProcedure + .input(complianceRetentionReportSchema.omit({ id: true }).extend({ id: z.string().trim().optional() })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + const nextEntry = complianceRetentionReportSchema.parse({ + ...input, + id: input.id?.trim() || nanoid(10), + }); + const nextEntries = [ + nextEntry, + ...operations.retentionReports.filter((entry) => entry.id !== nextEntry.id && entry.generatedAt !== nextEntry.generatedAt), + ].sort((a, b) => (b.generatedAt || "").localeCompare(a.generatedAt || "", "fr")); + await db.setPortalSetting( + COMPLIANCE_RETENTION_HISTORY_SETTING_KEY, + JSON.stringify(nextEntries), + "Historique des rapports de retention et purge" + ); + await logAdminAction(ctx.user.id, "modification", "compliance_retention_report", undefined, { + reportId: nextEntry.id, + generatedAt: nextEntry.generatedAt, + ipAddress: extractClientIp(ctx.req), + }); + return nextEntry; + }), + deleteRetentionReport: adminProcedure + .input(z.object({ id: z.string().trim().min(1) })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + await db.setPortalSetting( + COMPLIANCE_RETENTION_HISTORY_SETTING_KEY, + JSON.stringify(operations.retentionReports.filter((entry) => entry.id !== input.id)), + "Historique des rapports de retention et purge" + ); + await logAdminAction(ctx.user.id, "suppression", "compliance_retention_report", undefined, { + reportId: input.id, + ipAddress: extractClientIp(ctx.req), + }); + return { success: true }; + }), + upsertEvidence: adminProcedure + .input(complianceEvidenceSchema.omit({ id: true }).extend({ id: z.string().trim().optional() })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + const nextEntry = complianceEvidenceSchema.parse({ + ...input, + id: input.id?.trim() || nanoid(10), + }); + const nextEntries = [ + nextEntry, + ...operations.evidenceCenter.filter((entry) => entry.id !== nextEntry.id), + ].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt, "fr")); + await db.setPortalSetting( + COMPLIANCE_EVIDENCE_SETTING_KEY, + JSON.stringify(nextEntries), + "Centre de conformité et preuves d'audit" + ); + await logAdminAction(ctx.user.id, "modification", "compliance_evidence", undefined, { + evidenceId: nextEntry.id, + category: nextEntry.category, + title: nextEntry.title, + ipAddress: extractClientIp(ctx.req), + }); + return nextEntry; + }), + deleteEvidence: adminProcedure + .input(z.object({ id: z.string().trim().min(1) })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + await db.setPortalSetting( + COMPLIANCE_EVIDENCE_SETTING_KEY, + JSON.stringify(operations.evidenceCenter.filter((entry) => entry.id !== input.id)), + "Centre de conformité et preuves d'audit" + ); + await logAdminAction(ctx.user.id, "suppression", "compliance_evidence", undefined, { + evidenceId: input.id, + ipAddress: extractClientIp(ctx.req), + }); + return { success: true }; + }), + upsertIsoDeliverable: adminProcedure + .input(complianceIsoDeliverableSchema.omit({ id: true }).extend({ id: z.string().trim().optional() })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + const nextEntry = complianceIsoDeliverableSchema.parse({ + ...input, + id: input.id?.trim() || nanoid(10), + }); + const nextEntries = [ + nextEntry, + ...operations.isoDeliverables.filter((entry) => entry.id !== nextEntry.id), + ].sort((a, b) => a.reference.localeCompare(b.reference, "fr")); + await db.setPortalSetting( + COMPLIANCE_ISO_DELIVERABLES_SETTING_KEY, + JSON.stringify(nextEntries), + "Pilotage des livrables ISO 27001 et validation interne" + ); + await logAdminAction(ctx.user.id, "modification", "compliance_iso_deliverable", undefined, { + deliverableId: nextEntry.id, + reference: nextEntry.reference, + status: nextEntry.status, + ipAddress: extractClientIp(ctx.req), + }); + return nextEntry; + }), + setIsoDeliverableStatus: adminProcedure + .input(z.object({ + id: z.string().trim().min(1), + status: z.enum(["a_rediger", "en_cours", "a_relire", "a_valider", "valide", "a_reviser"]), + approverName: z.string().trim().max(160).optional().or(z.literal("")), + approverEmail: z.string().trim().max(320).optional().or(z.literal("")), + note: z.string().trim().max(1000).optional().or(z.literal("")), + })) + .mutation(async ({ ctx, input }) => { + const operations = await getComplianceOperations(); + const existing = operations.isoDeliverables.find((entry) => entry.id === input.id); + if (!existing) { + throw new TRPCError({ code: "NOT_FOUND", message: "Livrable ISO introuvable" }); + } + + const now = new Date().toISOString(); + const nextEntry = complianceIsoDeliverableSchema.parse({ + ...existing, + status: input.status, + approverName: input.approverName || existing.approverName, + approverEmail: input.approverEmail || existing.approverEmail, + lastReviewedAt: now, + validatedAt: input.status === "valide" ? now : input.status === "a_reviser" ? "" : existing.validatedAt, + notes: [existing.notes, input.note].filter(Boolean).join(existing.notes && input.note ? "\n\n" : ""), + }); + + const nextEntries = [ + nextEntry, + ...operations.isoDeliverables.filter((entry) => entry.id !== nextEntry.id), + ].sort((a, b) => a.reference.localeCompare(b.reference, "fr")); + + await db.setPortalSetting( + COMPLIANCE_ISO_DELIVERABLES_SETTING_KEY, + JSON.stringify(nextEntries), + "Pilotage des livrables ISO 27001 et validation interne" + ); + await logAdminAction(ctx.user.id, "modification", "compliance_iso_deliverable_status", undefined, { + deliverableId: nextEntry.id, + reference: nextEntry.reference, + status: nextEntry.status, + ipAddress: extractClientIp(ctx.req), + }); + return nextEntry; + }), + }), + + // ============== NOTIFICATIONS ============== + notifications: router({ + getAll: accueilAdminProcedure + .input(z.object({ unreadOnly: z.boolean().optional() })) + .query(async ({ ctx, input }) => { + return db.getAdminNotifications(ctx.user.id, input.unreadOnly); + }), + + getUnreadCount: accueilAdminProcedure.query(async ({ ctx }) => { + return db.getUnreadNotificationCount(ctx.user.id); + }), + + markAsRead: accueilAdminProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + await db.markNotificationAsRead(input.id); + return { success: true }; + }), + + markAllAsRead: accueilAdminProcedure.mutation(async ({ ctx }) => { + await db.markAllNotificationsAsRead(ctx.user.id); + return { success: true }; + }), + }), + + internalDirectory: router({ + get: salleReadProcedure.query(async () => { + const storedValue = await db.getPortalSetting(INTERNAL_DIRECTORY_SETTING_KEY); + if (!storedValue) { + return null; + } + + try { + return z.array(internalDirectoryDivisionSchema).parse(JSON.parse(storedValue)); + } catch (error) { + console.error("Failed to parse internal directory setting:", error); + return null; + } + }), + + set: superAdminProcedure + .input(z.object({ + divisions: z.array(internalDirectoryDivisionSchema).min(1).max(50), + })) + .mutation(async ({ ctx, input }) => { + await db.setPortalSetting( + INTERNAL_DIRECTORY_SETTING_KEY, + JSON.stringify(input.divisions), + "Annuaire interne CCDS editable depuis l'administration" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: INTERNAL_DIRECTORY_SETTING_KEY, + divisionCount: input.divisions.length, + ipAddress: extractClientIp(ctx.req), + }); + return { success: true }; + }), + }), + + // ============== PORTAL SETTINGS ============== + settings: router({ + getAppearance: publicProcedure.query(async () => { + const storedValue = await db.getPortalSetting(APPEARANCE_SETTING_KEY); + + if (!storedValue) { + return defaultPortalAppearance; + } + + try { + return sanitizePortalAppearance(JSON.parse(storedValue)); + } catch { + return defaultPortalAppearance; + } + }), + + getMail: adminProcedure.query(async () => { + return getAdminMailSettings(); + }), + + getAssociationMap: publicProcedure.query(async () => { + return getAssociationMapSettings(); + }), + + getUserOperatingTour: publicProcedure.query(async () => { + return getUserOperatingTourSettings(); + }), + + getLogistics: adminProcedure.query(async ({ ctx }) => { + assertLogisticsAccess(ctx.user); + return getLogisticsSettings(); + }), + + getLogisticsGroup: adminProcedure.query(async () => { + return getLogisticsGroupSettings(); + }), + + getSalleBilling: adminProcedure.query(async () => { + return getSalleBillingSettings(); + }), + + getHelloAsso: adminProcedure.query(async () => { + const settings = await getHelloAssoSettings(); + return serializeHelloAssoSettingsPublic(settings); + }), + + getAll: adminProcedure.query(async () => { + return db.getAllPortalSettings(); + }), + + get: adminProcedure + .input(z.object({ key: z.string() })) + .query(async ({ input }) => { + return db.getPortalSetting(input.key); + }), + + set: superAdminProcedure + .input(z.object({ + key: z.string(), + value: z.string(), + description: z.string().optional(), + })) + .mutation(async ({ ctx, input }) => { + await db.setPortalSetting(input.key, input.value, input.description); + await logAdminAction(ctx.user.id, 'modification', 'setting', undefined, { key: input.key }); + return { success: true }; + }), + + setAssociationMap: superAdminProcedure + .input(z.object({ + publicStyleUrl: z.string().trim().url(), + adminStyleUrl: z.string().trim().url(), + })) + .mutation(async ({ ctx, input }) => { + const settings = sanitizeAssociationMapSettings(input); + await db.setPortalSetting( + ASSOCIATION_MAP_SETTING_KEY, + JSON.stringify(settings), + "Affichage cartographique MapLibre / tuiles vectorielles de l'annuaire public" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: ASSOCIATION_MAP_SETTING_KEY, + publicStyleUrl: settings.publicStyleUrl, + adminStyleUrl: settings.adminStyleUrl, + ipAddress: extractClientIp(ctx.req), + }); + return settings; + }), + + setUserOperatingTour: superAdminProcedure + .input(userOperatingTourSettingsSchema) + .mutation(async ({ ctx, input }) => { + const settings = userOperatingTourSettingsSchema.parse(input); + await db.setPortalSetting( + USER_OPERATING_TOUR_SETTING_KEY, + JSON.stringify(settings), + "Activation globale du tour guide utilisateur" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: USER_OPERATING_TOUR_SETTING_KEY, + enabled: settings.enabled, + ipAddress: extractClientIp(ctx.req), + }); + return settings; + }), + + setHelloAsso: superAdminProcedure + .input(z.object({ + enabled: z.boolean(), + clientId: z.string().trim().min(1).max(200), + clientSecret: z.string().trim().max(400).optional().default(""), + })) + .mutation(async ({ ctx, input }) => { + const saved = await saveHelloAssoSettings(input); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: HELLOASSO_SETTINGS_KEY, + enabled: saved.enabled, + clientId: saved.clientId, + clientSecretUpdated: Boolean(input.clientSecret?.trim()), + ipAddress: extractClientIp(ctx.req), + }); + return serializeHelloAssoSettingsPublic(saved); + }), + + setMail: superAdminProcedure + .input(z.object({ + smtpProvider: z.enum(supportedMailProviders), + smtpHost: z.string().optional(), + smtpPort: z.string().optional(), + smtpUser: z.string().optional(), + smtpFrom: z.string().email(), + smtpSecure: z.boolean().optional(), + smtpRequireTls: z.boolean().optional(), + smtpPass: z.string().optional(), + })) + .mutation(async ({ ctx, input }) => { + const saved = await saveAdminMailSettings( + { + smtpProvider: input.smtpProvider, + smtpHost: input.smtpHost, + smtpPort: input.smtpPort, + smtpUser: input.smtpUser, + smtpFrom: input.smtpFrom, + smtpSecure: input.smtpSecure, + smtpRequireTls: input.smtpRequireTls, + smtpPass: input.smtpPass, + }, + { id: ctx.user.id, name: ctx.user.name } + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: "system.mail.settings", + smtpProvider: input.smtpProvider, + smtpHost: input.smtpHost, + smtpPort: input.smtpPort, + smtpFrom: input.smtpFrom, + smtpUser: input.smtpUser, + smtpSecure: input.smtpSecure, + smtpRequireTls: input.smtpRequireTls, + passwordUpdated: Boolean(input.smtpPass?.trim()), + ipAddress: extractClientIp(ctx.req), + }); + return saved; + }), + + setLogistics: adminProcedure + .input(z.object({ + materialReturnLeadDays: z.number().int().min(1).max(30), + materialReturnGraceDays: z.number().int().min(0).max(30), + inventory: z.record(z.string(), z.number().int().min(0).nullable()).optional().default({}), + replacementValues: z.record(z.string(), z.number().int().min(0).nullable()).optional().default({}), + })) + .mutation(async ({ ctx, input }) => { + assertLogisticsAccess(ctx.user); + const saved = await saveLogisticsSettings({ + materialReturnLeadDays: input.materialReturnLeadDays, + materialReturnGraceDays: input.materialReturnGraceDays, + inventory: input.inventory as Partial>, + replacementValues: input.replacementValues as Partial>, + }); + + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: "system.logistics.settings", + materialReturnLeadDays: saved.materialReturnLeadDays, + materialReturnGraceDays: saved.materialReturnGraceDays, + inventory: saved.inventory, + replacementValues: saved.replacementValues, + ipAddress: extractClientIp(ctx.req), + }); + + return saved; + }), + + setLogisticsGroup: superAdminProcedure + .input(z.object({ + label: z.string().trim().min(1).max(120), + memberUserIds: z.array(z.number().int().positive()), + })) + .mutation(async ({ ctx, input }) => { + const saved = await saveLogisticsGroupSettings(input); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: "system.logistics.group", + label: saved.label, + memberUserIds: saved.memberUserIds, + ipAddress: extractClientIp(ctx.req), + }); + return saved; + }), + + sendTestMail: superAdminProcedure + .input(z.object({ + to: z.string().email(), + })) + .mutation(async ({ ctx, input }) => { + const result = await sendOperationalEmail({ + to: [input.to], + subject: "Test de configuration mail - Portail Associations", + text: [ + "Bonjour,", + "", + "Ceci est un email de test envoyé depuis l’espace d’administration du Portail Associations.", + `Configuration testée par : ${ctx.user.name || ctx.user.email || `Admin #${ctx.user.id}`}`, + `Date : ${new Date().toLocaleString("fr-FR")}`, + "", + "Si tu reçois ce message, la configuration SMTP du portail fonctionne.", + ].join("\n"), + replyTo: ctx.user.email || undefined, + fromName: ctx.user.name ? `${ctx.user.name} via Portail Associations` : "Portail Associations", + }); + + if (!result.sent) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: result.reason || "Impossible d’envoyer l’email de test", + }); + } + + await logAdminAction(ctx.user.id, "test", "setting", undefined, { + key: "system.mail.settings", + to: input.to, + ipAddress: extractClientIp(ctx.req), + }); + + return { success: true }; + }), + + setAppearance: superAdminProcedure + .input(appearanceSettingsSchema) + .mutation(async ({ ctx, input }) => { + const appearance = sanitizePortalAppearance(input); + await db.setPortalSetting( + APPEARANCE_SETTING_KEY, + JSON.stringify(appearance), + "Configuration visuelle du portail" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: APPEARANCE_SETTING_KEY, + ipAddress: extractClientIp(ctx.req), + }); + return appearance; + }), + + uploadAppearanceAsset: superAdminProcedure + .input(z.object({ + field: z.enum(appearanceAssetFields), + fileData: z.string(), + fileName: z.string(), + mimeType: z.string(), + })) + .mutation(async ({ ctx, input }) => { + const normalizedMimeType = input.mimeType.toLowerCase(); + const isImage = normalizedMimeType.startsWith("image/"); + const isIco = normalizedMimeType === "image/x-icon" || normalizedMimeType === "image/vnd.microsoft.icon"; + if (!isImage && !isIco) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le fichier doit être une image compatible" }); + } + + const fileBuffer = Buffer.from(input.fileData, "base64"); + const storageName = `portal/appearance/${input.field}/${nanoid()}-${input.fileName}`; + const { url } = await storagePut(storageName, fileBuffer, input.mimeType); + + const storedValue = await db.getPortalSetting(APPEARANCE_SETTING_KEY); + let currentAppearance = defaultPortalAppearance; + if (storedValue) { + try { + currentAppearance = sanitizePortalAppearance(JSON.parse(storedValue)); + } catch { + currentAppearance = defaultPortalAppearance; + } + } + + const nextAppearance = sanitizePortalAppearance({ + ...currentAppearance, + [input.field]: url, + ...(input.field === "backgroundImageUrl" ? { backgroundEnabled: true } : {}), + }); + + await db.setPortalSetting( + APPEARANCE_SETTING_KEY, + JSON.stringify(nextAppearance), + "Configuration visuelle du portail" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: APPEARANCE_SETTING_KEY, + field: input.field, + ipAddress: extractClientIp(ctx.req), + }); + return nextAppearance; + }), + + uploadAppearanceTargetAsset: superAdminProcedure + .input(z.object({ + target: z.enum(appearanceBackgroundTargets), + fileData: z.string(), + fileName: z.string(), + mimeType: z.string(), + })) + .mutation(async ({ ctx, input }) => { + if (!input.mimeType.startsWith("image/")) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le fichier doit etre une image" }); + } + + const fileBuffer = Buffer.from(input.fileData, "base64"); + const storageName = `portal/appearance/background-targets/${input.target}/${nanoid()}-${input.fileName}`; + const { url } = await storagePut(storageName, fileBuffer, input.mimeType); + + const storedValue = await db.getPortalSetting(APPEARANCE_SETTING_KEY); + let currentAppearance = defaultPortalAppearance; + if (storedValue) { + try { + currentAppearance = sanitizePortalAppearance(JSON.parse(storedValue)); + } catch { + currentAppearance = defaultPortalAppearance; + } + } + + const nextAppearance = sanitizePortalAppearance({ + ...currentAppearance, + backgroundEnabled: true, + backgroundTargets: Array.from(new Set([...currentAppearance.backgroundTargets, input.target])), + backgroundTargetImageUrls: { + ...(currentAppearance.backgroundTargetImageUrls || {}), + [input.target]: url, + }, + }); + + await db.setPortalSetting( + APPEARANCE_SETTING_KEY, + JSON.stringify(nextAppearance), + "Configuration visuelle du portail" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: APPEARANCE_SETTING_KEY, + field: `backgroundTargetImageUrls.${input.target}`, + ipAddress: extractClientIp(ctx.req), + }); + + return nextAppearance; + }), + + uploadAppearanceDecorationAsset: superAdminProcedure + .input(z.object({ + decorationId: z.string().trim().min(1).max(80), + fileData: z.string(), + fileName: z.string(), + mimeType: z.string(), + decoration: z.object({ + id: z.string().trim().min(1).max(80), + imageUrl: z.string().optional(), + scope: z.enum(appearanceDecorationScopes), + layer: z.enum(appearanceDecorationLayers), + widthPercent: z.number().min(6).max(38), + topPercent: z.number().min(0).max(100), + leftPercent: z.number().min(0).max(100), + opacity: z.number().min(4).max(28), + blurRadius: z.number().min(0).max(12), + cardAnchor: z.enum(appearanceCardDecorationAnchors), + cardInsetPercent: z.number().min(0).max(20), + }).optional(), + })) + .mutation(async ({ ctx, input }) => { + if (!input.mimeType.startsWith("image/")) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le fichier doit être une image" }); + } + + const fileBuffer = Buffer.from(input.fileData, "base64"); + const storageName = `portal/appearance/decorations/${input.decorationId}/${nanoid()}-${input.fileName}`; + const { url } = await storagePut(storageName, fileBuffer, input.mimeType); + + const storedValue = await db.getPortalSetting(APPEARANCE_SETTING_KEY); + let currentAppearance = defaultPortalAppearance; + if (storedValue) { + try { + currentAppearance = sanitizePortalAppearance(JSON.parse(storedValue)); + } catch { + currentAppearance = defaultPortalAppearance; + } + } + + const currentDecorativeElements = currentAppearance.decorativeElements || []; + const hasExistingDecoration = currentDecorativeElements.some((entry) => entry.id === input.decorationId); + const nextDecorativeElements = hasExistingDecoration + ? currentDecorativeElements.map((entry) => + entry.id === input.decorationId ? { ...entry, imageUrl: url } : entry + ) + : input.decoration + ? [ + ...currentDecorativeElements, + { + ...input.decoration, + imageUrl: url, + }, + ] + : currentDecorativeElements; + + const nextAppearance = sanitizePortalAppearance({ + ...currentAppearance, + decorativeElements: nextDecorativeElements, + }); + + await db.setPortalSetting( + APPEARANCE_SETTING_KEY, + JSON.stringify(nextAppearance), + "Configuration visuelle du portail" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: APPEARANCE_SETTING_KEY, + field: `decorativeElements.${input.decorationId}.imageUrl`, + ipAddress: extractClientIp(ctx.req), + }); + + return nextAppearance; + }), + + uploadHeroImage: superAdminProcedure + .input(z.object({ + fileData: z.string(), + fileName: z.string(), + mimeType: z.string(), + })) + .mutation(async ({ ctx, input }) => { + if (!input.mimeType.startsWith("image/")) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le fichier doit être une image" }); + } + + const fileBuffer = Buffer.from(input.fileData, "base64"); + const storageName = `portal/appearance/hero/${nanoid()}-${input.fileName}`; + const { url } = await storagePut(storageName, fileBuffer, input.mimeType); + + const storedValue = await db.getPortalSetting(APPEARANCE_SETTING_KEY); + let currentAppearance = defaultPortalAppearance; + if (storedValue) { + try { + currentAppearance = sanitizePortalAppearance(JSON.parse(storedValue)); + } catch { + currentAppearance = defaultPortalAppearance; + } + } + const nextAppearance = sanitizePortalAppearance({ + ...currentAppearance, + heroImageUrl: url, + }); + + await db.setPortalSetting( + APPEARANCE_SETTING_KEY, + JSON.stringify(nextAppearance), + "Configuration visuelle du portail" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: APPEARANCE_SETTING_KEY, + field: "heroImageUrl", + ipAddress: extractClientIp(ctx.req), + }); + return nextAppearance; + }), + + uploadSalleRib: adminProcedure + .input(z.object({ + fileData: z.string(), + fileName: z.string(), + mimeType: z.string(), + })) + .mutation(async ({ ctx, input }) => { + const allowedMimeTypes = new Set([ + "application/pdf", + "image/png", + "image/jpeg", + "image/webp", + ]); + if (!allowedMimeTypes.has(input.mimeType)) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Le RIB doit être un PDF, PNG, JPG ou WebP" }); + } + + const fileBuffer = Buffer.from(input.fileData, "base64"); + const storageName = `portal/salle-billing/rib/${nanoid()}-${input.fileName}`; + const { url } = await storagePut(storageName, fileBuffer, input.mimeType); + const nextSettings = sanitizeSalleBillingSettings({ + ribDocumentUrl: url, + ribDocumentName: input.fileName, + ribMimeType: input.mimeType, + ribUploadedAt: new Date().toISOString(), + }); + + await db.setPortalSetting( + SALLE_BILLING_SETTING_KEY, + JSON.stringify(nextSettings), + "RIB MJS utilisé pour les factures salle" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: SALLE_BILLING_SETTING_KEY, + fileName: input.fileName, + mimeType: input.mimeType, + ipAddress: extractClientIp(ctx.req), + }); + return nextSettings; + }), + + clearSalleRib: adminProcedure + .mutation(async ({ ctx }) => { + const nextSettings = sanitizeSalleBillingSettings(null); + await db.setPortalSetting( + SALLE_BILLING_SETTING_KEY, + JSON.stringify(nextSettings), + "RIB MJS utilisé pour les factures salle" + ); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: SALLE_BILLING_SETTING_KEY, + cleared: true, + ipAddress: extractClientIp(ctx.req), + }); + return nextSettings; + }), + }), + + // ============== STATS ROUTES (ADMIN) ============== + stats: router({ + getDashboard: accueilAdminProcedure.query(async () => { + const stats = await db.getDashboardStats(); + if (!stats) { + return { + totalAssociations: 0, + activeAssociations: 0, + totalRequests: 0, + pendingRequests: 0, + validatedRequests: 0, + rejectedRequests: 0, + newAssociationsThisMonth: 0, + newRequestsThisMonth: 0, + requestsThisWeek: 0, + overdueRequests: 0, + acceptanceRate: 0, + requestsByStatus: {}, + }; + } + return stats; + }), + + getRequestsPerMonth: accueilAdminProcedure + .input(z.object({ months: z.number().optional() })) + .query(async ({ input }) => { + return db.getRequestsPerMonth(input.months || 12); + }), + + getAssociationsPerMonth: accueilAdminProcedure + .input(z.object({ months: z.number().optional() })) + .query(async ({ input }) => { + return db.getAssociationsPerMonth(input.months || 12); + }), + + getRequestsByType: accueilAdminProcedure.query(async () => { + return db.getRequestsByTypeStats(); + }), + + getAverageProcessingTime: accueilAdminProcedure.query(async () => { + return db.getAverageProcessingTime(); + }), + + getAutomationSettings: accueilAdminProcedure.query(async () => { + return getStatsAutomationSettings(); + }), + + getReportHistory: accueilAdminProcedure.query(async () => { + return getStatsReportHistory(); + }), + + updateAutomationSettings: adminProcedure + .input(z.object({ + enabled: z.boolean(), + recipientEmails: z.array(z.string().email()).default([]), + frequencies: z.object({ + weekly: z.boolean(), + month: z.boolean(), + quarter: z.boolean(), + semester: z.boolean(), + year: z.boolean(), + }), + })) + .mutation(async ({ ctx, input }) => { + const current = await getStatsAutomationSettings(); + const saved = await saveStatsAutomationSettings({ + ...current, + enabled: input.enabled, + recipientEmails: input.recipientEmails.map((entry) => normalizeEmail(entry)), + frequencies: input.frequencies, + }); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: STATS_AUTOMATION_SETTING_KEY, + enabled: saved.enabled, + frequencies: saved.frequencies, + recipientEmails: saved.recipientEmails, + ipAddress: extractClientIp(ctx.req), + }); + return saved; + }), + + generateReportNow: adminProcedure + .input(z.object({ + period: z.enum(["weekly", "month", "quarter", "semester", "year"]), + })) + .mutation(async ({ ctx, input }) => { + const analyticsPeriod = input.period === "weekly" ? "7d" : input.period; + const cycleKey = `manual-${input.period}-${new Date().toISOString()}`; + const result = await generateAndArchiveStatsReport({ + period: input.period, + analyticsPeriod, + generatedBy: "manual", + cycleKey, + now: new Date(), + }); + await db.createAdminNotification({ + type: "systeme", + titre: `Rapport statistiques généré`, + message: `Un rapport ${result.entry.periodLabel.toLowerCase()} a été généré manuellement depuis l’espace statistiques.`, + lien: "/admin?tab=analytics", + }); + await logAdminAction(ctx.user.id, "generation_rapport_statistiques", "setting", undefined, { + period: input.period, + cycleKey, + ipAddress: extractClientIp(ctx.req), + }); + return result.entry; + }), + + getReservationAnalytics: accueilAdminProcedure + .input(z.object({ + period: analyticsPeriodSchema.default("month"), + })) + .query(async ({ input }) => { + return computeReservationAnalytics(input.period); + }), + }), + + operationalTour: router({ + getSettings: adminProcedure.query(async () => { + const settings = await getOperationalTourSettings(); + return { + ...settings, + targets: { + production: { + ...settings.targets.production, + associationPassword: "", + accueilPassword: "", + adminPassword: "", + }, + secondary: { + ...settings.targets.secondary, + associationPassword: "", + accueilPassword: "", + adminPassword: "", + }, + }, + }; + }), + + saveSettings: superAdminProcedure + .input(z.object({ + defaultEnvironment: operationalTourEnvironmentSchema, + targets: z.object({ + production: z.object({ + baseUrl: z.string().trim(), + associationEmail: z.string().trim(), + associationPassword: z.string().trim(), + associationAuthMode: operationalTourAuthModeSchema, + accueilEmail: z.string().trim(), + accueilPassword: z.string().trim(), + accueilAuthMode: operationalTourAuthModeSchema, + adminEmail: z.string().trim(), + adminPassword: z.string().trim(), + adminAuthMode: operationalTourAuthModeSchema, + }), + secondary: z.object({ + baseUrl: z.string().trim(), + associationEmail: z.string().trim(), + associationPassword: z.string().trim(), + associationAuthMode: operationalTourAuthModeSchema, + accueilEmail: z.string().trim(), + accueilPassword: z.string().trim(), + accueilAuthMode: operationalTourAuthModeSchema, + adminEmail: z.string().trim(), + adminPassword: z.string().trim(), + adminAuthMode: operationalTourAuthModeSchema, + }), + }), + })) + .mutation(async ({ ctx, input }) => { + const current = await getOperationalTourSettings(); + const saved = await saveOperationalTourSettings({ + defaultEnvironment: input.defaultEnvironment, + targets: { + production: { + ...current.targets.production, + ...input.targets.production, + associationPassword: + input.targets.production.associationAuthMode === "oauth" + ? "" + : input.targets.production.associationPassword.trim() || current.targets.production.associationPassword, + accueilPassword: + input.targets.production.accueilAuthMode === "oauth" + ? "" + : input.targets.production.accueilPassword.trim() || current.targets.production.accueilPassword, + adminPassword: + input.targets.production.adminAuthMode === "oauth" + ? "" + : input.targets.production.adminPassword.trim() || current.targets.production.adminPassword, + }, + secondary: { + ...current.targets.secondary, + ...input.targets.secondary, + associationPassword: + input.targets.secondary.associationAuthMode === "oauth" + ? "" + : input.targets.secondary.associationPassword.trim() || current.targets.secondary.associationPassword, + accueilPassword: + input.targets.secondary.accueilAuthMode === "oauth" + ? "" + : input.targets.secondary.accueilPassword.trim() || current.targets.secondary.accueilPassword, + adminPassword: + input.targets.secondary.adminAuthMode === "oauth" + ? "" + : input.targets.secondary.adminPassword.trim() || current.targets.secondary.adminPassword, + }, + }, + }); + await logAdminAction(ctx.user.id, "modification", "setting", undefined, { + key: OPERATIONAL_TOUR_SETTINGS_KEY, + defaultEnvironment: saved.defaultEnvironment, + productionBaseUrl: saved.targets.production.baseUrl, + secondaryBaseUrl: saved.targets.secondary.baseUrl, + productionAssociationConfigured: Boolean(saved.targets.production.associationEmail && (saved.targets.production.associationAuthMode === "oauth" || saved.targets.production.associationPassword)), + secondaryAssociationConfigured: Boolean(saved.targets.secondary.associationEmail && (saved.targets.secondary.associationAuthMode === "oauth" || saved.targets.secondary.associationPassword)), + productionAccueilConfigured: Boolean(saved.targets.production.accueilEmail && (saved.targets.production.accueilAuthMode === "oauth" || saved.targets.production.accueilPassword)), + secondaryAccueilConfigured: Boolean(saved.targets.secondary.accueilEmail && (saved.targets.secondary.accueilAuthMode === "oauth" || saved.targets.secondary.accueilPassword)), + productionAdminConfigured: Boolean(saved.targets.production.adminEmail && (saved.targets.production.adminAuthMode === "oauth" || saved.targets.production.adminPassword)), + secondaryAdminConfigured: Boolean(saved.targets.secondary.adminEmail && (saved.targets.secondary.adminAuthMode === "oauth" || saved.targets.secondary.adminPassword)), + ipAddress: extractClientIp(ctx.req), + }); + return saved; + }), + + getHistory: adminProcedure.query(async () => { + return getOperationalTourHistory(); + }), + + getLatest: adminProcedure.query(async () => { + const history = await getOperationalTourHistory(); + return history[0] ?? null; + }), + + clearHistory: superAdminProcedure.mutation(async ({ ctx }) => { + const retained = await clearOperationalTourHistory(); + await logAdminAction(ctx.user.id, "nettoyage_historique_tour_operationnel", "setting", undefined, { + key: OPERATIONAL_TOUR_SETTINGS_KEY, + retainedReports: retained.map((report) => ({ + id: report.id, + environment: report.environment, + finishedAt: report.finishedAt, + status: report.status, + })), + ipAddress: extractClientIp(ctx.req), + }); + return retained; + }), + + run: adminProcedure + .input(z.object({ + environment: operationalTourEnvironmentSchema.optional(), + })) + .mutation(async ({ ctx, input }) => { + const settings = await getOperationalTourSettings(); + const environment = input.environment ?? settings.defaultEnvironment; + const report = await runOperationalTour({ + environment, + req: ctx.req, + currentUser: ctx.user, + }); + await logAdminAction(ctx.user.id, "tour_operationnel", "setting", undefined, { + key: OPERATIONAL_TOUR_SETTINGS_KEY, + environment, + status: report.status, + summary: report.summary, + ipAddress: extractClientIp(ctx.req), + }); + return report; + }), + }), +}); + +export type AppRouter = typeof appRouter; diff --git a/server/salleAdministrativeDecisionPdf.ts b/server/salleAdministrativeDecisionPdf.ts new file mode 100644 index 0000000..4a11871 --- /dev/null +++ b/server/salleAdministrativeDecisionPdf.ts @@ -0,0 +1,89 @@ +import PDFDocument from "pdfkit"; +import fs from "node:fs"; +import path from "node:path"; + +const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png"); + +function drawLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) { + if (!fs.existsSync(CCDS_LOGO_PATH)) return; + try { + doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: "center", valign: "center" }); + } catch (error) { + console.warn("[SalleAdministrativeDecisionPdf] Impossible de charger le logo CCDS:", error); + } +} + +export async function generateSalleAdministrativeDecisionPdf(input: { + requestId: number; + decisionText: string; + signedAt?: string | Date | null; + signedByLabel?: string | null; +}) { + const pdfBuffer = await new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: "A4", + margins: { top: 42, bottom: 42, left: 42, right: 42 }, + }); + const chunks: Buffer[] = []; + doc.on("data", (chunk: Buffer) => chunks.push(chunk)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const width = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + drawLogo(doc, doc.page.margins.left + (width - 90) / 2, y, 90, 90); + y += 102; + + doc.font("Helvetica-Bold").fontSize(18).fillColor("#0f2d63").text( + "DÉCISION ADMINISTRATIVE PRIORITAIRE", + doc.page.margins.left, + y, + { width, align: "center" } + ); + y += 22; + doc.font("Helvetica").fontSize(10).fillColor("#475569").text( + `Dossier salle #${input.requestId}`, + doc.page.margins.left, + y, + { width, align: "center" } + ); + y += 34; + + const bodyHeight = input.signedAt ? 360 : 420; + doc.roundedRect(doc.page.margins.left, y, width, bodyHeight, 10).stroke("#cbd5e1"); + doc.font("Helvetica").fontSize(11).fillColor("#1f2937").text( + input.decisionText, + doc.page.margins.left + 18, + y + 18, + { width: width - 36, lineGap: 4 } + ); + + if (input.signedAt) { + const signatureY = y + bodyHeight + 18; + doc.roundedRect(doc.page.margins.left, signatureY, width, 62, 8).fillAndStroke("#eef6ff", "#93c5fd"); + doc.font("Helvetica-Bold").fontSize(11).fillColor("#1d4ed8").text( + "Visa de la Direction", + doc.page.margins.left + 16, + signatureY + 12, + { width: width - 32 } + ); + doc.font("Helvetica").fontSize(10).fillColor("#1f2937").text( + `Validation électronique le ${new Date(input.signedAt).toLocaleString("fr-FR")}${input.signedByLabel ? ` par ${input.signedByLabel}` : ""}.`, + doc.page.margins.left + 16, + signatureY + 32, + { width: width - 32 } + ); + } + + doc.end(); + } catch (error) { + reject(error); + } + }); + + return { + buffer: pdfBuffer, + fileName: `decision-administrative-salle-${input.requestId}.pdf`, + }; +} diff --git a/server/salleConventionPdf.ts b/server/salleConventionPdf.ts new file mode 100644 index 0000000..63e4854 --- /dev/null +++ b/server/salleConventionPdf.ts @@ -0,0 +1,254 @@ +import PDFDocument from "pdfkit"; +import fs from "node:fs"; +import path from "node:path"; +import type { MaterialContractFinancialMode, MaterialContractStatus } from "./materialConventionPdf"; + +const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png"); + +type ContractRequestLike = { + id: number; + titre: string; + status: string; + formData?: string | null; +}; + +type ContractAssociationLike = { + nomAssociation?: string | null; + adresse?: string | null; + codePostal?: string | null; + ville?: string | null; + telephone?: string | null; + emailContact?: string | null; + nomRepresentant?: string | null; +} | null | undefined; + +type ContractDecision = { + financialMode: MaterialContractFinancialMode; + depositRequired: boolean; + depositAmountCents: number; + rentalAmountCents: number; + pricingNotes?: string | null; + contractStatus: MaterialContractStatus; + contractGeneratedAt?: string | Date | null; + contractValidatedByUserId?: number | null; +}; + +function drawLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) { + if (!fs.existsSync(CCDS_LOGO_PATH)) return; + try { + doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: "center", valign: "center" }); + } catch (error) { + console.warn("[SalleConventionPdf] Impossible de charger le logo CCDS:", error); + } +} + +function formatDateFr(date: string | Date | null | undefined) { + if (!date) return "-"; + try { + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(date); + } +} + +function formatCurrency(cents: number | null | undefined) { + return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format((cents || 0) / 100); +} + +function getFinancialModeLabel(mode: MaterialContractFinancialMode) { + switch (mode) { + case "gratuite": + return "Mise à disposition gratuite"; + case "gratuite_avec_caution": + return "Mise à disposition gratuite avec caution"; + case "location_payante": + return "Location payante"; + default: + return mode; + } +} + +function getScheduleSummary(formData: any) { + const dailySlots = Array.isArray(formData?.horairesParJour) + ? formData.horairesParJour.filter((slot: any) => slot?.date) + : []; + + if (Boolean(formData?.useDetailedSchedule) && dailySlots.length > 0) { + return dailySlots + .map((slot: any) => `${formatDateFr(slot.date)} : ${slot.heureDebut || "?"} - ${slot.heureFin || "?"}`) + .join(" | "); + } + + if (formData?.heureDebut || formData?.heureFin) { + return `${formData.heureDebut || "?"} - ${formData.heureFin || "?"}`; + } + + return "-"; +} + +export async function generateSalleConventionPdf(input: { + request: ContractRequestLike; + association?: ContractAssociationLike; + decision: ContractDecision; +}) { + const formData = (() => { + try { + return input.request.formData ? JSON.parse(input.request.formData) : {}; + } catch { + return {}; + } + })(); + + const sallesSelectionnees = Array.isArray(formData.sallesSelectionnees) && formData.sallesSelectionnees.length > 0 + ? formData.sallesSelectionnees.join(", ") + : "-"; + + const reservationStart = formData.dateReservation; + const reservationEnd = formData.dateFinReservation || formData.dateReservation; + const scheduleSummary = getScheduleSummary(formData); + + const pdfBuffer = await new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: "A4", + margins: { top: 38, bottom: 42, left: 42, right: 42 }, + info: { + Title: `Convention salle CCDS - dossier ${input.request.id}`, + Author: "Communauté de Communes Des Savanes", + Subject: "Convention de mise à disposition / location d'un local CCDS", + }, + }); + + const chunks: Buffer[] = []; + doc.on("data", (chunk: Buffer) => chunks.push(chunk)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + + doc.lineWidth(2).strokeColor("#efb100").moveTo(doc.page.margins.left, y).lineTo(doc.page.margins.left + pageWidth, y).stroke(); + y += 16; + drawLogo(doc, doc.page.margins.left + (pageWidth - 84) / 2, y, 84, 84); + y += 92; + + doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("COMMUNAUTÉ DE COMMUNES DES SAVANES", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 18; + doc.font("Helvetica-Bold").fontSize(17).fillColor("#0f172a").text( + "CONVENTION DE MISE À DISPOSITION / LOCATION", + doc.page.margins.left, + y, + { width: pageWidth, align: "center" } + ); + y += 20; + doc.font("Helvetica-Bold").fontSize(14).text("D'UN LOCAL - MAISON DE LA JEUNESSE DES SAVANES", doc.page.margins.left, y, { + width: pageWidth, + align: "center", + }); + y += 28; + + doc.roundedRect(doc.page.margins.left, y, pageWidth, 42, 8).fillAndStroke("#eef5ff", "#bfd2ef"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f2d63").text( + "Document généré à partir de la décision administrative CCDS", + doc.page.margins.left + 14, + y + 9, + { width: pageWidth - 28, align: "center" } + ); + doc.font("Helvetica").fontSize(9).fillColor("#334155").text( + `Dossier #${input.request.id} — statut contrat : ${input.decision.contractStatus}`, + doc.page.margins.left + 14, + y + 23, + { width: pageWidth - 28, align: "center" } + ); + y += 56; + + const infoRows: Array<[string, string]> = [ + ["Association", input.association?.nomAssociation || formData.nomAssociation || "-"], + ["Représentant", input.association?.nomRepresentant || formData.representantLegal || "-"], + ["Adresse", [input.association?.adresse, input.association?.codePostal, input.association?.ville].filter(Boolean).join(" ") || "-"], + ["Téléphone", input.association?.telephone || formData.telephoneAssociation || "-"], + ["Email", input.association?.emailContact || formData.emailAssociation || "-"], + ["Local réservé", sallesSelectionnees], + [ + "Période d'utilisation", + reservationStart || reservationEnd + ? `${formatDateFr(reservationStart)}${reservationEnd ? ` au ${formatDateFr(reservationEnd)}` : ""}` + : "-", + ], + ["Créneau / horaires", scheduleSummary], + ["Objet", formData.motifReservation || input.request.titre || "-"], + ]; + + for (const [label, value] of infoRows) { + doc.font("Helvetica-Bold").fontSize(9).fillColor("#334155").text(`${label} :`, doc.page.margins.left, y, { width: 140 }); + doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text(value, doc.page.margins.left + 145, y - 1, { + width: pageWidth - 145, + }); + y += 18; + } + + y += 10; + doc.roundedRect(doc.page.margins.left, y, pageWidth, 108, 8).fillAndStroke("#f8fafc", "#cbd5e1"); + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Conditions financières", doc.page.margins.left + 14, y + 12); + doc.font("Helvetica").fontSize(10).fillColor("#334155") + .text(`Régime retenu : ${getFinancialModeLabel(input.decision.financialMode)}`, doc.page.margins.left + 14, y + 34, { + width: pageWidth - 28, + }) + .text(`Montant de location : ${formatCurrency(input.decision.rentalAmountCents)}`, doc.page.margins.left + 14, y + 52, { + width: pageWidth - 28, + }) + .text( + `Caution : ${input.decision.depositRequired ? formatCurrency(input.decision.depositAmountCents) : "Aucune caution exigée"}`, + doc.page.margins.left + 14, + y + 70, + { width: pageWidth - 28 } + ); + y += 122; + + if (input.decision.pricingNotes?.trim()) { + doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Clauses / réserves spécifiques", doc.page.margins.left, y); + y += 16; + doc.roundedRect(doc.page.margins.left, y, pageWidth, 70, 8).stroke("#cbd5e1"); + doc.font("Helvetica").fontSize(10).fillColor("#334155").text(input.decision.pricingNotes.trim(), doc.page.margins.left + 12, y + 12, { + width: pageWidth - 24, + }); + y += 84; + } + + y += 8; + doc.font("Helvetica").fontSize(9).fillColor("#475569").text( + "Cette convention formalise les conditions administratives retenues par la CCDS pour la mise à disposition ou la location du local sollicité.", + doc.page.margins.left, + y, + { width: pageWidth } + ); + y += 36; + + const signatureWidth = (pageWidth - 16) / 2; + doc.roundedRect(doc.page.margins.left, y, signatureWidth, 84, 8).stroke("#cbd5e1"); + doc.roundedRect(doc.page.margins.left + signatureWidth + 16, y, signatureWidth, 84, 8).stroke("#cbd5e1"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a") + .text("Association emprunteuse", doc.page.margins.left + 12, y + 12, { width: signatureWidth - 24, align: "center" }) + .text("CCDS / DSU", doc.page.margins.left + signatureWidth + 28, y + 12, { width: signatureWidth - 24, align: "center" }); + doc.font("Helvetica").fontSize(9).fillColor("#64748b") + .text("Nom, qualité et signature", doc.page.margins.left + 12, y + 56, { width: signatureWidth - 24, align: "center" }) + .text("Visa administratif", doc.page.margins.left + signatureWidth + 28, y + 56, { width: signatureWidth - 24, align: "center" }); + + doc.end(); + } catch (error) { + reject(error); + } + }); + + return { + buffer: pdfBuffer, + fileName: `convention-salle-ccds-${input.request.id}.pdf`, + }; +} diff --git a/server/salleInvoicePdf.ts b/server/salleInvoicePdf.ts new file mode 100644 index 0000000..41a6e1b --- /dev/null +++ b/server/salleInvoicePdf.ts @@ -0,0 +1,150 @@ +import PDFDocument from "pdfkit"; +import fs from "node:fs"; +import path from "node:path"; +import type { SallePricingSummary } from "@shared/sallePricing"; + +const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png"); + +function formatCurrency(cents: number) { + return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(cents / 100); +} + +function formatDateFr(date: string | Date | null | undefined) { + if (!date) return "-"; + try { + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(date); + } +} + +function drawLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) { + if (!fs.existsSync(CCDS_LOGO_PATH)) return; + try { + doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: "center", valign: "center" }); + } catch (error) { + console.warn("[SalleInvoicePdf] Impossible de charger le logo CCDS:", error); + } +} + +export async function generateSalleInvoicePdf(input: { + requestId: number; + requestTitle: string; + associationName: string; + datesLabel: string; + pricing: SallePricingSummary; +}) { + const pdfBuffer = await new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: "A4", + margins: { top: 36, bottom: 42, left: 34, right: 34 }, + }); + const chunks: Buffer[] = []; + doc.on("data", (chunk: Buffer) => chunks.push(chunk)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const width = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + + drawLogo(doc, doc.page.margins.left, y, 180, 70); + doc.font("Helvetica-Bold").fontSize(18).fillColor("#244f7a").text( + `Facture N° ${String(input.requestId).padStart(3, "0")}/${new Date().getFullYear()}`, + doc.page.margins.left, + y + 34, + { width, align: "right" } + ); + y += 82; + + const infoRows: Array<[string, string]> = [ + ["Date", formatDateFr(new Date())], + ["Référence dossier", `Réservation salle #${input.requestId}`], + ["Émis par", "Maison de la Jeunesse des Savanes - CCDS"], + ["Association", input.associationName], + ["Période", input.datesLabel], + ]; + + const labelWidth = 160; + infoRows.forEach(([label, value]) => { + doc.rect(doc.page.margins.left, y, labelWidth, 36).stroke("#d9d9d9"); + doc.rect(doc.page.margins.left + labelWidth, y, width - labelWidth, 36).stroke("#d9d9d9"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#404040").text(label, doc.page.margins.left + 10, y + 12, { width: labelWidth - 20 }); + doc.font("Helvetica").fontSize(10).fillColor("#1f2937").text(value, doc.page.margins.left + labelWidth + 10, y + 12, { + width: width - labelWidth - 20, + }); + y += 36; + }); + + y += 26; + const col1 = width * 0.52; + const col2 = width * 0.13; + const col3 = width * 0.17; + const col4 = width - col1 - col2 - col3; + doc.rect(doc.page.margins.left, y, width, 40).fillAndStroke("#244f7a", "#244f7a"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#ffffff") + .text("DESCRIPTION", doc.page.margins.left + 14, y + 14, { width: col1 - 20 }) + .text("QTÉ", doc.page.margins.left + col1 + 10, y + 14, { width: col2 - 20, align: "center" }) + .text("PRIX UNITAIRE", doc.page.margins.left + col1 + col2 + 10, y + 14, { width: col3 - 20, align: "center" }) + .text("TOTAL", doc.page.margins.left + col1 + col2 + col3 + 10, y + 14, { width: col4 - 20, align: "center" }); + y += 40; + + input.pricing.lineItems.forEach((line) => { + const rowHeight = 88; + doc.rect(doc.page.margins.left, y, col1, rowHeight).stroke("#d9d9d9"); + doc.rect(doc.page.margins.left + col1, y, col2, rowHeight).stroke("#d9d9d9"); + doc.rect(doc.page.margins.left + col1 + col2, y, col3, rowHeight).stroke("#d9d9d9"); + doc.rect(doc.page.margins.left + col1 + col2 + col3, y, col4, rowHeight).stroke("#d9d9d9"); + + doc.font("Helvetica-Bold").fontSize(11).fillColor("#1f2937").text( + `Location ${line.salleNom}`, + doc.page.margins.left + 14, + y + 12, + { width: col1 - 24 } + ); + doc.font("Helvetica").fontSize(10).fillColor("#334155").text( + `${line.categoryLabel}\n${input.datesLabel}\n${input.requestTitle}`, + doc.page.margins.left + 14, + y + 32, + { width: col1 - 24 } + ); + doc.font("Helvetica-Bold").fontSize(12).fillColor("#1f2937") + .text(String(line.quantity), doc.page.margins.left + col1, y + 34, { width: col2, align: "center" }) + .text(formatCurrency(line.unitAmountCents), doc.page.margins.left + col1 + col2, y + 34, { width: col3, align: "center" }) + .text(formatCurrency(line.totalAmountCents), doc.page.margins.left + col1 + col2 + col3, y + 34, { width: col4, align: "center" }); + y += rowHeight; + }); + + doc.rect(doc.page.margins.left, y, col1 + col2 + col3, 44).stroke("#244f7a"); + doc.rect(doc.page.margins.left + col1 + col2 + col3, y, col4, 44).stroke("#244f7a"); + doc.font("Helvetica-Bold").fontSize(12).fillColor("#244f7a") + .text("TOTAL À RÉGLER", doc.page.margins.left, y + 14, { width: col1 + col2 + col3 - 16, align: "right" }) + .fillColor("#c2410c") + .text(formatCurrency(input.pricing.totalAmountCents), doc.page.margins.left + col1 + col2 + col3, y + 14, { width: col4, align: "center" }); + y += 60; + + doc.roundedRect(doc.page.margins.left, y, width, 110, 8).fillAndStroke("#f8fafc", "#cbd5e1"); + doc.font("Helvetica-Bold").fontSize(11).fillColor("#1f2937").text("Récapitulatif pour l’envoi final", doc.page.margins.left + 16, y + 14); + doc.font("Helvetica").fontSize(10).fillColor("#475569") + .text("Cette facture accompagne la validation finale de la réservation. Le règlement est attendu selon les modalités précisées par la Maison de la Jeunesse des Savanes.", doc.page.margins.left + 16, y + 36, { + width: width - 32, + }) + .text("RIB MJS : à joindre ou communiquer depuis l’accueil si nécessaire.", doc.page.margins.left + 16, y + 72, { + width: width - 32, + }); + + doc.end(); + } catch (error) { + reject(error); + } + }); + + return { + buffer: pdfBuffer, + fileName: `facture-salle-ccds-${input.requestId}.pdf`, + }; +} diff --git a/server/salleQuotePdf.ts b/server/salleQuotePdf.ts new file mode 100644 index 0000000..c7aff93 --- /dev/null +++ b/server/salleQuotePdf.ts @@ -0,0 +1,182 @@ +import PDFDocument from "pdfkit"; +import fs from "node:fs"; +import path from "node:path"; +import type { SallePricingSummary } from "@shared/sallePricing"; + +const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png"); + +function formatCurrency(cents: number) { + return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(cents / 100); +} + +function formatDateFr(date: string | Date | null | undefined) { + if (!date) return "-"; + try { + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(date); + } +} + +function drawLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) { + if (!fs.existsSync(CCDS_LOGO_PATH)) return; + try { + doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: "center", valign: "center" }); + } catch (error) { + console.warn("[SalleQuotePdf] Impossible de charger le logo CCDS:", error); + } +} + +function getScheduleSummary(formData: any) { + const dailySlots = Array.isArray(formData?.horairesParJour) + ? formData.horairesParJour.filter((slot: any) => slot?.date) + : []; + if (Boolean(formData?.useDetailedSchedule) && dailySlots.length > 0) { + const start = dailySlots[0]; + const end = dailySlots[dailySlots.length - 1]; + return `${formatDateFr(start.date)} au ${formatDateFr(end.date)}`; + } + return `${formData?.heureDebut || "?"} à ${formData?.heureFin || "?"}`; +} + +export async function generateSalleQuotePdf(input: { + requestId: number; + requestTitle: string; + formData: any; + association: { + nomAssociation?: string | null; + adresse?: string | null; + codePostal?: string | null; + ville?: string | null; + } | null | undefined; + pricing: SallePricingSummary; + signedAt?: string | Date | null; + signedByLabel?: string | null; +}) { + const pdfBuffer = await new Promise((resolve, reject) => { + try { + const doc = new PDFDocument({ + size: "A4", + margins: { top: 36, bottom: 42, left: 34, right: 34 }, + }); + const chunks: Buffer[] = []; + doc.on("data", (chunk: Buffer) => chunks.push(chunk)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const width = doc.page.width - doc.page.margins.left - doc.page.margins.right; + let y = doc.page.margins.top; + + drawLogo(doc, doc.page.margins.left, y, 180, 70); + doc.font("Helvetica-Bold").fontSize(18).fillColor("#244f7a").text( + `Devis N° ${String(input.requestId).padStart(3, "0")}/${new Date().getFullYear()}`, + doc.page.margins.left, + y + 34, + { width, align: "right" } + ); + y += 82; + + const clientAddress = [input.association?.adresse, input.association?.codePostal, input.association?.ville].filter(Boolean).join(" "); + const infoRows: Array<[string, string]> = [ + ["Date", formatDateFr(new Date())], + ["Référence", `${String(input.requestId).padStart(3, "0")}/${new Date().getFullYear()}`], + ["Émis par", "Communauté de Communes Des Savanes"], + ["Client", input.association?.nomAssociation || input.formData.nomAssociation || "-"], + ["Adresse", clientAddress || "-"], + ]; + + const labelWidth = 160; + infoRows.forEach(([label, value]) => { + doc.rect(doc.page.margins.left, y, labelWidth, 36).stroke("#d9d9d9"); + doc.rect(doc.page.margins.left + labelWidth, y, width - labelWidth, 36).stroke("#d9d9d9"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#404040").text(label, doc.page.margins.left + 10, y + 12, { width: labelWidth - 20 }); + doc.font("Helvetica").fontSize(10).fillColor("#1f2937").text(value, doc.page.margins.left + labelWidth + 10, y + 12, { + width: width - labelWidth - 20, + }); + y += 36; + }); + + y += 26; + const col1 = width * 0.54; + const col2 = width * 0.13; + const col3 = width * 0.15; + const col4 = width - col1 - col2 - col3; + doc.rect(doc.page.margins.left, y, width, 40).fillAndStroke("#244f7a", "#244f7a"); + doc.font("Helvetica-Bold").fontSize(10).fillColor("#ffffff") + .text("DESCRIPTION", doc.page.margins.left + 14, y + 14, { width: col1 - 20 }) + .text("QUANTITÉ", doc.page.margins.left + col1 + 10, y + 14, { width: col2 - 20, align: "center" }) + .text("PRIX UNITAIRE HT", doc.page.margins.left + col1 + col2 + 10, y + 8, { width: col3 - 20, align: "center" }) + .text("TOTAL TTC", doc.page.margins.left + col1 + col2 + col3 + 10, y + 14, { width: col4 - 20, align: "center" }); + y += 40; + + input.pricing.lineItems.forEach((line) => { + const rowHeight = 92; + doc.rect(doc.page.margins.left, y, col1, rowHeight).stroke("#d9d9d9"); + doc.rect(doc.page.margins.left + col1, y, col2, rowHeight).stroke("#d9d9d9"); + doc.rect(doc.page.margins.left + col1 + col2, y, col3, rowHeight).stroke("#d9d9d9"); + doc.rect(doc.page.margins.left + col1 + col2 + col3, y, col4, rowHeight).stroke("#d9d9d9"); + doc.font("Helvetica-Bold").fontSize(11).fillColor("#1f2937").text( + `location de la salle "${line.salleNom.replace("Salle ", "")}"`, + doc.page.margins.left + 14, + y + 12, + { width: col1 - 24 } + ); + doc.font("Helvetica").fontSize(10).fillColor("#334155").text( + `${line.categoryLabel}\n${input.formData.dateReservation ? `Période du ${formatDateFr(input.formData.dateReservation)}${input.formData.dateFinReservation && input.formData.dateFinReservation !== input.formData.dateReservation ? ` au ${formatDateFr(input.formData.dateFinReservation)}` : ""}` : ""}\n${getScheduleSummary(input.formData)}`, + doc.page.margins.left + 14, + y + 34, + { width: col1 - 24 } + ); + doc.font("Helvetica-Bold").fontSize(12).fillColor("#1f2937") + .text(String(line.quantity), doc.page.margins.left + col1, y + 38, { width: col2, align: "center" }) + .text(formatCurrency(line.unitAmountCents), doc.page.margins.left + col1 + col2, y + 38, { width: col3, align: "center" }) + .text(formatCurrency(line.totalAmountCents), doc.page.margins.left + col1 + col2 + col3, y + 38, { width: col4, align: "center" }); + y += rowHeight; + }); + + doc.rect(doc.page.margins.left, y, col1 + col2 + col3, 44).stroke("#244f7a"); + doc.rect(doc.page.margins.left + col1 + col2 + col3, y, col4, 44).stroke("#244f7a"); + doc.font("Helvetica-Bold").fontSize(12).fillColor("#244f7a") + .text("TOTAL TTC", doc.page.margins.left, y + 14, { width: col1 + col2 + col3 - 16, align: "right" }) + .fillColor("#c2410c") + .text(formatCurrency(input.pricing.totalAmountCents), doc.page.margins.left + col1 + col2 + col3, y + 14, { width: col4, align: "center" }); + y += 64; + + doc.font("Helvetica-Bold").fontSize(12).fillColor("#111827").text("Arrêter le présent devis à la somme de :", doc.page.margins.left, y); + y += 22; + doc.font("Helvetica").fontSize(11).fillColor("#374151").text(formatCurrency(input.pricing.totalAmountCents), doc.page.margins.left, y); + y += 48; + + doc.font("Helvetica-Bold").fontSize(10).fillColor("#244f7a").text("COMMUNAUTÉ DE COMMUNES DES SAVANES", doc.page.margins.left, y, { width, align: "center" }); + y += 16; + doc.font("Helvetica").fontSize(10).fillColor("#475569").text("GESTION ADMINISTRATIVE ET FACTURATION", doc.page.margins.left, y, { width, align: "center" }); + + if (input.signedAt) { + y += 26; + doc.roundedRect(doc.page.margins.left, y, width, 56, 8).fillAndStroke("#eef6ff", "#93c5fd"); + doc.font("Helvetica-Bold").fontSize(11).fillColor("#1d4ed8").text("Devis validé pour signature Direction", doc.page.margins.left + 16, y + 12, { + width: width - 32, + }); + doc.font("Helvetica").fontSize(10).fillColor("#1f2937").text( + `Validation électronique le ${formatDateFr(input.signedAt)}${input.signedByLabel ? ` par ${input.signedByLabel}` : ""}.`, + doc.page.margins.left + 16, + y + 30, + { width: width - 32 } + ); + } + + doc.end(); + } catch (error) { + reject(error); + } + }); + + return { + buffer: pdfBuffer, + fileName: `devis-salle-ccds-${input.requestId}.pdf`, + }; +} diff --git a/server/salleReservationEmails.ts b/server/salleReservationEmails.ts new file mode 100644 index 0000000..a0bdd7c --- /dev/null +++ b/server/salleReservationEmails.ts @@ -0,0 +1,137 @@ +import { CONDITIONS_FINANCIERES_SALLE_TEXT, SALLE_FREQUENCY_LABELS, SALLE_USAGE_TYPE_LABELS, type SallePricingSummary } from "@shared/sallePricing"; + +function escapeHtml(value: string) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function formatCurrency(cents: number) { + return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(cents / 100); +} + +export function generateSalleQuoteEmail(input: { + associationName: string; + requestTitle: string; + pricing: SallePricingSummary; + acceptUrl: string; + refuseUrl: string; + decisionText: string; +}) { + const total = formatCurrency(input.pricing.totalAmountCents); + const conditionLines = CONDITIONS_FINANCIERES_SALLE_TEXT.split("\n").filter(Boolean); + const decisionLines = input.decisionText.split("\n").filter(Boolean); + const isBilled = input.pricing.totalAmountCents > 0; + const subject = isBilled + ? `Validation du devis - ${input.requestTitle}` + : `Validation de la mise à disposition - ${input.requestTitle}`; + + const intro = isBilled + ? `Bonjour ${input.associationName},\n\nVous trouverez ci-joint le devis, les conditions financières et la décision administrative prioritaire relatives à votre demande de réservation.` + : `Bonjour ${input.associationName},\n\nVous trouverez ci-joint les conditions financières et la décision administrative prioritaire relatives à votre demande de réservation.`; + + const amountBlock = isBilled + ? `Type d'usage : ${SALLE_USAGE_TYPE_LABELS[input.pricing.usageType]} +Fréquence : ${SALLE_FREQUENCY_LABELS[input.pricing.frequency]} +Montant calculé automatiquement : ${total}` + : `Type d'usage : ${SALLE_USAGE_TYPE_LABELS[input.pricing.usageType]} +Fréquence : ${SALLE_FREQUENCY_LABELS[input.pricing.frequency]} +Mise à disposition sans facturation.`; + + const text = `${intro} + +${amountBlock} + +Pour poursuivre le traitement de votre dossier, merci d'utiliser l'un des liens suivants : +- J'accepte : ${input.acceptUrl} +- Je refuse : ${input.refuseUrl} + +Conditions financières : +${conditionLines.join("\n")} + +Décision administrative : +${decisionLines.join("\n")} + +Maison de la Jeunesse des Savanes - Mélissa ALVES`; + + const html = ` +
+

Bonjour ${escapeHtml(input.associationName)},

+

${escapeHtml(isBilled + ? "Vous trouverez ci-joint le devis, les conditions financières et la décision administrative prioritaire relatives à votre demande de réservation." + : "Vous trouverez ci-joint les conditions financières et la décision administrative prioritaire relatives à votre demande de réservation.")}

+
+

Type d'usage : ${escapeHtml(SALLE_USAGE_TYPE_LABELS[input.pricing.usageType])}

+

Fréquence : ${escapeHtml(SALLE_FREQUENCY_LABELS[input.pricing.frequency])}

+

${escapeHtml(isBilled ? "Montant calculé automatiquement :" : "Conditions financières :")} ${escapeHtml(isBilled ? total : "Mise à disposition sans facturation")}

+
+

Merci de nous confirmer votre position :

+ +

Si les boutons ne fonctionnent pas, utilisez directement ces liens :
${escapeHtml(input.acceptUrl)}
${escapeHtml(input.refuseUrl)}

+
+

Maison de la Jeunesse des Savanes - Mélissa ALVES

+
+ `; + + return { subject, text, html }; +} + +export function generateSalleFinalEmail(input: { + associationName: string; + datesLabel: string; + pricingTotalCents: number; + billed: boolean; +}) { + const subject = input.billed + ? `Réservation validée - location de salle` + : `Réservation validée - mise à disposition de salle`; + + const text = input.billed + ? `Bonjour ${input.associationName}, + +Votre demande de réservation d’un espace pour le ${input.datesLabel} a reçu un avis favorable. +Nous vous confirmons que votre événement se tiendra au sein de la Maison de la Jeunesse des Savanes « Mélissa ALVES ». + +Le montant de la location s’élève à ${formatCurrency(input.pricingTotalCents)} conformément à la tarification en vigueur et au devis validé. + +Nous vous rappelons que vous êtes seule responsable de tout ce qui se passera dans les locaux, à l’intérieur comme à l’extérieur, concernant les participants et le public durant la période de l’événement. + +Merci de bien vouloir nous transmettre une attestation de responsabilité civile couvrant cette journée, ainsi que le règlement du montant indiqué selon les modalités précisées dans le devis. + +Il est formellement interdit de manger à l’intérieur des espaces utilisés et nous vous demandons de veiller à ce que les lieux soient laissés aussi propres à la fin de l’événement qu’à votre arrivée. + +Vous trouverez en pièce jointe : +- le devis validé +- la décision administrative +- le formulaire signé +- la facture à régler + +Bien cordialement, +Maison de la Jeunesse des Savanes - Mélissa ALVES` + : `Bonjour ${input.associationName}, + +Votre demande de réservation d’un espace pour le ${input.datesLabel} a reçu un avis favorable. +Nous vous confirmons que votre événement se tiendra au sein de la Maison de la Jeunesse des Savanes « Mélissa ALVES ». + +Nous vous rappelons que vous êtes seule responsable de tout ce qui se passera dans les locaux, à l’intérieur comme à l’extérieur, concernant les participants et le public durant la période de l’événement. +Merci de bien vouloir nous transmettre une attestation de responsabilité civile couvrant cette journée. +Il est formellement interdit de manger à l’intérieur des espaces utilisés. +Nous vous demandons également de veiller à laisser les lieux propres et en bon état à la fin de votre événement. + +Vous trouverez en pièce jointe le formulaire validé et signé. + +Bien cordialement, +Maison de la Jeunesse des Savanes - Mélissa ALVES`; + + return { + subject, + text, + html: `
${escapeHtml(text)}
`, + }; +} diff --git a/server/salleReservationWorkflow.ts b/server/salleReservationWorkflow.ts new file mode 100644 index 0000000..b990e01 --- /dev/null +++ b/server/salleReservationWorkflow.ts @@ -0,0 +1,123 @@ +import { + buildDecisionAdministrativeText, + computeSallePricing, + CONDITIONS_FINANCIERES_SALLE_TEXT, + type SallePricingSummary, + type SalleReservationFrequency, + type SalleUsageType, + type SalleWorkflowData, + type SalleWorkflowDirectorStatus, + type SalleWorkflowQuoteStatus, +} from "@shared/sallePricing"; + +function formatDateFr(date: string | Date | null | undefined) { + if (!date) return "-"; + try { + return new Date(date).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(date); + } +} + +function getScheduleSummary(formData: any) { + const dailySlots = Array.isArray(formData?.horairesParJour) + ? formData.horairesParJour.filter((slot: any) => slot?.date) + : []; + + if (Boolean(formData?.useDetailedSchedule) && dailySlots.length > 0) { + return dailySlots + .map((slot: any) => `${formatDateFr(slot.date)} : ${slot.heureDebut || "?"} - ${slot.heureFin || "?"}`) + .join(" | "); + } + + if (formData?.heureDebut || formData?.heureFin) { + return `${formData.heureDebut || "?"} - ${formData.heureFin || "?"}`; + } + + return "-"; +} + +export function parseRequestFormData(rawFormData?: string | null) { + if (!rawFormData) return {}; + try { + return JSON.parse(rawFormData); + } catch { + return {}; + } +} + +export function getSalleWorkflowData(rawFormData?: string | null): SalleWorkflowData { + const formData = parseRequestFormData(rawFormData); + return formData?.salleWorkflow && typeof formData.salleWorkflow === "object" ? formData.salleWorkflow : {}; +} + +export function computeSalleWorkflowPricing(rawFormData?: string | null): SallePricingSummary | null { + const formData = parseRequestFormData(rawFormData); + const usageType = formData?.typeUsage as SalleUsageType | undefined; + const frequency = formData?.frequence as SalleReservationFrequency | undefined; + const sallesIds = Array.isArray(formData?.sallesIds) ? formData.sallesIds : []; + if (!usageType || !frequency || sallesIds.length === 0) { + return null; + } + + return computeSallePricing({ + sallesIds, + usageType, + frequency, + dateReservation: formData.dateReservation, + dateFinReservation: formData.dateFinReservation, + }); +} + +export function mergeSalleWorkflowData(rawFormData: string | null | undefined, workflowPatch: Partial) { + const formData = parseRequestFormData(rawFormData); + const currentWorkflow = getSalleWorkflowData(rawFormData); + formData.salleWorkflow = { + ...currentWorkflow, + ...workflowPatch, + }; + return JSON.stringify(formData); +} + +export function buildDefaultSalleWorkflowTexts(rawFormData?: string | null) { + const formData = parseRequestFormData(rawFormData); + const salles = Array.isArray(formData?.sallesSelectionnees) && formData.sallesSelectionnees.length > 0 + ? formData.sallesSelectionnees.join(", ") + : "-"; + const dates = formData?.dateReservation + ? `${formatDateFr(formData.dateReservation)}${formData?.dateFinReservation && formData.dateFinReservation !== formData.dateReservation ? ` au ${formatDateFr(formData.dateFinReservation)}` : ""}` + : "-"; + const horaires = getScheduleSummary(formData); + const association = formData?.nomAssociation || "-"; + + return { + conditionsFinancieresText: CONDITIONS_FINANCIERES_SALLE_TEXT, + decisionAdministrativeText: buildDecisionAdministrativeText({ + espace: salles, + dates, + horaires, + association, + }), + }; +} + +export function updateSalleWorkflowQuoteStatus(rawFormData: string | null | undefined, status: SalleWorkflowQuoteStatus) { + return mergeSalleWorkflowData(rawFormData, { + quoteStatus: status, + quoteRespondedAt: status === "accepte" || status === "refuse" ? new Date().toISOString() : undefined, + }); +} + +export function updateSalleWorkflowDirectorStatus(rawFormData: string | null | undefined, status: SalleWorkflowDirectorStatus) { + return mergeSalleWorkflowData(rawFormData, { + directorStatus: status, + directorTransmissionAt: status === "en_attente_signature" ? new Date().toISOString() : undefined, + directorSignedAt: status === "signee" ? new Date().toISOString() : undefined, + directorReturnComment: status === "en_attente_signature" ? "" : undefined, + directorReturnedAt: status === "en_attente_signature" ? "" : undefined, + }); +} diff --git a/server/salleSignatureDelegation.ts b/server/salleSignatureDelegation.ts new file mode 100644 index 0000000..3696462 --- /dev/null +++ b/server/salleSignatureDelegation.ts @@ -0,0 +1,102 @@ +import * as db from "./db"; + +const SALLE_SIGNATURE_DELEGATION_SETTING_KEY = "system.salle_signature.delegations"; + +type DirectriceDelegationEntry = { + delegateUserIds: number[]; +}; + +type SalleSignatureDelegationSettings = { + directriceDelegations: Record; +}; + +function sanitizeUserIds(values: unknown): number[] { + if (!Array.isArray(values)) return []; + return Array.from( + new Set( + values + .map((value) => Number(value)) + .filter((value) => Number.isInteger(value) && value > 0) + ) + ); +} + +function sanitizeSettings(value: unknown): SalleSignatureDelegationSettings { + if (!value || typeof value !== "object") { + return { directriceDelegations: {} }; + } + + const record = (value as { directriceDelegations?: unknown }).directriceDelegations; + if (!record || typeof record !== "object") { + return { directriceDelegations: {} }; + } + + const entries = Object.entries(record as Record).reduce>( + (acc, [directriceId, entry]) => { + const userId = Number(directriceId); + if (!Number.isInteger(userId) || userId <= 0) return acc; + const delegateUserIds = sanitizeUserIds((entry as { delegateUserIds?: unknown })?.delegateUserIds); + acc[String(userId)] = { delegateUserIds }; + return acc; + }, + {} + ); + + return { directriceDelegations: entries }; +} + +export async function getSalleSignatureDelegationSettings(): Promise { + const raw = await db.getPortalSetting(SALLE_SIGNATURE_DELEGATION_SETTING_KEY); + if (!raw) { + return { directriceDelegations: {} }; + } + + try { + return sanitizeSettings(JSON.parse(raw)); + } catch { + return { directriceDelegations: {} }; + } +} + +export async function saveSalleSignatureDelegationSettings(settings: SalleSignatureDelegationSettings) { + await db.setPortalSetting( + SALLE_SIGNATURE_DELEGATION_SETTING_KEY, + JSON.stringify(sanitizeSettings(settings)), + "Délégations de signature des demandes de salle" + ); +} + +export async function getDelegatedSignerIdsForDirectrice(directriceUserId: number) { + const settings = await getSalleSignatureDelegationSettings(); + return settings.directriceDelegations[String(directriceUserId)]?.delegateUserIds || []; +} + +export async function setDelegatedSignerIdsForDirectrice(directriceUserId: number, delegateUserIds: number[]) { + const settings = await getSalleSignatureDelegationSettings(); + settings.directriceDelegations[String(directriceUserId)] = { + delegateUserIds: sanitizeUserIds(delegateUserIds), + }; + await saveSalleSignatureDelegationSettings(settings); +} + +export async function addDelegatedSignerForDirectrice(directriceUserId: number, delegateUserId: number) { + const current = await getDelegatedSignerIdsForDirectrice(directriceUserId); + const next = Array.from(new Set([...current, delegateUserId])); + await setDelegatedSignerIdsForDirectrice(directriceUserId, next); + return next; +} + +export async function removeDelegatedSignerForDirectrice(directriceUserId: number, delegateUserId: number) { + const current = await getDelegatedSignerIdsForDirectrice(directriceUserId); + const next = current.filter((entry) => entry !== delegateUserId); + await setDelegatedSignerIdsForDirectrice(directriceUserId, next); + return next; +} + +export async function getDirectriceIdsDelegatingToUser(userId: number) { + const settings = await getSalleSignatureDelegationSettings(); + return Object.entries(settings.directriceDelegations) + .filter(([, entry]) => sanitizeUserIds(entry.delegateUserIds).includes(userId)) + .map(([directriceId]) => Number(directriceId)) + .filter((value) => Number.isInteger(value) && value > 0); +} diff --git a/server/socialAuth.ts b/server/socialAuth.ts new file mode 100644 index 0000000..43836d7 --- /dev/null +++ b/server/socialAuth.ts @@ -0,0 +1,281 @@ +import { randomBytes } from "node:crypto"; +import type { Express, Request, Response } from "express"; +import { parse as parseCookieHeader } from "cookie"; +import { createSessionToken, loginOAuthUser, setSessionCookie } from "./_core/auth"; +import { getSessionCookieOptions } from "./_core/cookies"; +import { ENV } from "./_core/env"; + +const GOOGLE_STATE_COOKIE = "oauth_state_google"; +const FACEBOOK_STATE_COOKIE = "oauth_state_facebook"; +const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000; + +function parseCookies(cookieHeader: string | undefined) { + if (!cookieHeader) return new Map(); + return new Map(Object.entries(parseCookieHeader(cookieHeader))); +} + +function buildBaseUrl(req: Request) { + if (ENV.appBaseUrl) { + return ENV.appBaseUrl.replace(/\/$/, ""); + } + + const forwardedProto = req.headers["x-forwarded-proto"]; + const forwardedHost = req.headers["x-forwarded-host"]; + const proto = typeof forwardedProto === "string" + ? forwardedProto.split(",")[0]?.trim() + : req.protocol; + const host = typeof forwardedHost === "string" + ? forwardedHost.split(",")[0]?.trim() + : req.get("host"); + + return `${proto || "http"}://${host}`; +} + +function getCallbackUrl(req: Request, provider: "google" | "facebook") { + return `${buildBaseUrl(req)}/auth/${provider}/callback`; +} + +function redirectToLogin(res: Response, message: string) { + res.redirect(`/login?authError=${encodeURIComponent(message)}`); +} + +function setOAuthStateCookie(req: Request, res: Response, name: string, value: string) { + res.cookie(name, value, { + ...getSessionCookieOptions(req), + maxAge: OAUTH_STATE_MAX_AGE_MS, + }); +} + +function clearOAuthStateCookie(req: Request, res: Response, name: string) { + res.clearCookie(name, { + ...getSessionCookieOptions(req), + maxAge: -1, + }); +} + +function readAndValidateState(req: Request, res: Response, cookieName: string, receivedState: string | null) { + const cookies = parseCookies(req.headers.cookie); + const expectedState = cookies.get(cookieName) ?? ""; + clearOAuthStateCookie(req, res, cookieName); + return Boolean(receivedState && expectedState && receivedState === expectedState); +} + +function createOAuthState() { + return randomBytes(24).toString("hex"); +} + +function isGoogleConfigured() { + return Boolean(ENV.googleClientId && ENV.googleClientSecret); +} + +function isFacebookConfigured() { + return Boolean(ENV.facebookAppId && ENV.facebookAppSecret); +} + +async function finishLogin(req: Request, res: Response, userData: { + provider: "google" | "facebook"; + providerUserId: string; + email: string; + name?: string | null; +}) { + const user = await loginOAuthUser(userData); + const token = await createSessionToken(user); + setSessionCookie(req, res, token); + res.redirect(user.role === "service_terrain" ? "/terrain" : "/dashboard"); +} + +export function getOAuthProviderStatus() { + return { + google: isGoogleConfigured(), + facebook: isFacebookConfigured(), + }; +} + +export function registerSocialAuthRoutes(app: Express) { + app.get("/auth/google/start", (req, res) => { + if (!isGoogleConfigured()) { + redirectToLogin(res, "Connexion Google non configurée"); + return; + } + + const state = createOAuthState(); + setOAuthStateCookie(req, res, GOOGLE_STATE_COOKIE, state); + + const params = new URLSearchParams({ + client_id: ENV.googleClientId, + redirect_uri: getCallbackUrl(req, "google"), + response_type: "code", + scope: "openid email profile", + state, + access_type: "online", + include_granted_scopes: "true", + prompt: "select_account", + }); + + res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`); + }); + + app.get("/auth/google/callback", async (req, res) => { + try { + if (!isGoogleConfigured()) { + redirectToLogin(res, "Connexion Google non configurée"); + return; + } + + const code = typeof req.query.code === "string" ? req.query.code : ""; + const state = typeof req.query.state === "string" ? req.query.state : null; + + if (!code || !readAndValidateState(req, res, GOOGLE_STATE_COOKIE, state)) { + redirectToLogin(res, "Connexion Google invalide ou expirée"); + return; + } + + const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + code, + client_id: ENV.googleClientId, + client_secret: ENV.googleClientSecret, + redirect_uri: getCallbackUrl(req, "google"), + grant_type: "authorization_code", + }), + }); + + if (!tokenResponse.ok) { + redirectToLogin(res, "Google a refusé la connexion"); + return; + } + + const tokenData = await tokenResponse.json() as { access_token?: string }; + if (!tokenData.access_token) { + redirectToLogin(res, "Jeton Google manquant"); + return; + } + + const userInfoResponse = await fetch("https://openidconnect.googleapis.com/v1/userinfo", { + headers: { + authorization: `Bearer ${tokenData.access_token}`, + }, + }); + + if (!userInfoResponse.ok) { + redirectToLogin(res, "Impossible de récupérer le profil Google"); + return; + } + + const userInfo = await userInfoResponse.json() as { + sub?: string; + email?: string; + email_verified?: boolean; + name?: string; + }; + + if (!userInfo.sub || !userInfo.email || userInfo.email_verified === false) { + redirectToLogin(res, "Le compte Google doit fournir une adresse email vérifiée"); + return; + } + + await finishLogin(req, res, { + provider: "google", + providerUserId: userInfo.sub, + email: userInfo.email, + name: userInfo.name, + }); + } catch (error) { + console.error("[OAuth] Google callback failed:", error); + redirectToLogin(res, "Connexion Google impossible pour le moment"); + } + }); + + app.get("/auth/facebook/start", (req, res) => { + if (!isFacebookConfigured()) { + redirectToLogin(res, "Connexion Facebook non configurée"); + return; + } + + const state = createOAuthState(); + setOAuthStateCookie(req, res, FACEBOOK_STATE_COOKIE, state); + + const params = new URLSearchParams({ + client_id: ENV.facebookAppId, + redirect_uri: getCallbackUrl(req, "facebook"), + state, + scope: "email,public_profile", + }); + + res.redirect(`https://www.facebook.com/dialog/oauth?${params.toString()}`); + }); + + app.get("/auth/facebook/callback", async (req, res) => { + try { + if (!isFacebookConfigured()) { + redirectToLogin(res, "Connexion Facebook non configurée"); + return; + } + + const code = typeof req.query.code === "string" ? req.query.code : ""; + const state = typeof req.query.state === "string" ? req.query.state : null; + + if (!code || !readAndValidateState(req, res, FACEBOOK_STATE_COOKIE, state)) { + redirectToLogin(res, "Connexion Facebook invalide ou expirée"); + return; + } + + const tokenUrl = new URL("https://graph.facebook.com/oauth/access_token"); + tokenUrl.search = new URLSearchParams({ + client_id: ENV.facebookAppId, + client_secret: ENV.facebookAppSecret, + redirect_uri: getCallbackUrl(req, "facebook"), + code, + }).toString(); + + const tokenResponse = await fetch(tokenUrl); + if (!tokenResponse.ok) { + redirectToLogin(res, "Facebook a refusé la connexion"); + return; + } + + const tokenData = await tokenResponse.json() as { access_token?: string }; + if (!tokenData.access_token) { + redirectToLogin(res, "Jeton Facebook manquant"); + return; + } + + const profileUrl = new URL("https://graph.facebook.com/me"); + profileUrl.search = new URLSearchParams({ + fields: "id,name,email", + access_token: tokenData.access_token, + }).toString(); + + const profileResponse = await fetch(profileUrl); + if (!profileResponse.ok) { + redirectToLogin(res, "Impossible de récupérer le profil Facebook"); + return; + } + + const profile = await profileResponse.json() as { + id?: string; + name?: string; + email?: string; + }; + + if (!profile.id || !profile.email) { + redirectToLogin(res, "Le compte Facebook doit partager une adresse email"); + return; + } + + await finishLogin(req, res, { + provider: "facebook", + providerUserId: profile.id, + email: profile.email, + name: profile.name, + }); + } catch (error) { + console.error("[OAuth] Facebook callback failed:", error); + redirectToLogin(res, "Connexion Facebook impossible pour le moment"); + } + }); +} diff --git a/server/statsReportArtifacts.ts b/server/statsReportArtifacts.ts new file mode 100644 index 0000000..87a275f --- /dev/null +++ b/server/statsReportArtifacts.ts @@ -0,0 +1,231 @@ +import PDFDocument from "pdfkit"; +import * as XLSX from "xlsx"; + +type AnalyticsPayload = { + period: string; + periodLabel: string; + comparisonLabel: string; + startDate: string; + endDate: string; + summary: { + totalReservations: number; + salleReservations: number; + materialReservations: number; + occupancyRate: number; + activeAssociations: number; + totalMaterialUnits: number; + averageReservationsPerAssociation: number; + }; + associationUsage: Array<{ + associationName: string; + thematics: string[]; + totalReservations: number; + salleReservations: number; + materialReservations: number; + lastReservationAt: string; + }>; + roomOccupancy: Array<{ + roomName: string; + bookedDays: number; + occupancyRate: number; + }>; + materialUsage: Array<{ + label: string; + quantity: number; + requests: number; + }>; + history: Array<{ + type: string; + associationName: string; + title: string; + startDate: string; + endDate: string; + resources: string; + status: string; + createdAt: Date; + }>; +}; + +const requestTypeLabels: Record = { + demande_salle: "Demande salle", + demande_materiel_evenementiel: "Matériel événementiel", +}; + +const statusLabels: Record = { + brouillon: "Brouillon", + soumise: "Soumise", + en_cours_traitement: "En cours", + information_complementaire: "Info requise", + validee: "Validée", + refusee: "Refusée", + annulee: "Annulée", +}; + +function formatDateFr(value: string | Date | null | undefined) { + if (!value) return "-"; + try { + return new Date(value).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + } catch { + return String(value); + } +} + +function buildFileBaseName(data: AnalyticsPayload) { + return `rapport-statistiques-ccds-${data.period}-${data.endDate}`; +} + +export async function generateReservationAnalyticsPdf(data: AnalyticsPayload) { + const doc = new PDFDocument({ + size: "A4", + margin: 40, + info: { + Title: `Rapport statistiques CCDS - ${data.periodLabel}`, + Author: "Portail Associations CCDS", + }, + }); + + const chunks: Buffer[] = []; + doc.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); + + const done = new Promise((resolve) => { + doc.on("end", () => resolve(Buffer.concat(chunks))); + }); + + doc.fontSize(20).fillColor("#000091").text("Rapport statistiques CCDS"); + doc.moveDown(0.3); + doc.fontSize(11).fillColor("#475569").text(`${data.periodLabel} - ${formatDateFr(data.startDate)} au ${formatDateFr(data.endDate)}`); + doc.moveDown(1); + + doc.fontSize(14).fillColor("#0f172a").text("Synthèse"); + doc.moveDown(0.5); + [ + ["Réservations totales", String(data.summary.totalReservations)], + ["Réservations de salles", String(data.summary.salleReservations)], + ["Réservations de matériel", String(data.summary.materialReservations)], + ["Taux d'occupation des salles", `${data.summary.occupancyRate}%`], + ["Associations utilisatrices", String(data.summary.activeAssociations)], + ["Unités matérielles demandées", String(data.summary.totalMaterialUnits)], + ["Fréquence moyenne par association", String(data.summary.averageReservationsPerAssociation)], + ].forEach(([label, value]) => { + doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text(`${label} : `, { continued: true }); + doc.font("Helvetica").text(value); + }); + + doc.moveDown(1); + doc.font("Helvetica-Bold").fontSize(14).text("Top associations"); + doc.moveDown(0.5); + data.associationUsage.slice(0, 10).forEach((entry) => { + doc.fontSize(10).font("Helvetica-Bold").text(entry.associationName); + doc.font("Helvetica").text( + `Catégories: ${entry.thematics.join(" / ") || "Non renseignée"} • Total: ${entry.totalReservations} • Salles: ${entry.salleReservations} • Matériel: ${entry.materialReservations} • Dernière réservation: ${formatDateFr(entry.lastReservationAt)}` + ); + doc.moveDown(0.4); + }); + + doc.addPage(); + doc.font("Helvetica-Bold").fontSize(14).text("Occupation des salles"); + doc.moveDown(0.5); + data.roomOccupancy.forEach((entry) => { + doc.fontSize(10).font("Helvetica-Bold").text(entry.roomName, { continued: true }); + doc.font("Helvetica").text(` - ${entry.bookedDays} jour(s) réservé(s), ${entry.occupancyRate}% d'occupation`); + }); + + doc.moveDown(1); + doc.font("Helvetica-Bold").fontSize(14).text("Matériels les plus demandés"); + doc.moveDown(0.5); + data.materialUsage.forEach((entry) => { + doc.fontSize(10).font("Helvetica-Bold").text(entry.label, { continued: true }); + doc.font("Helvetica").text(` - Quantité: ${entry.quantity} • Dossiers: ${entry.requests}`); + }); + + doc.moveDown(1); + doc.font("Helvetica-Bold").fontSize(14).text("Historique récent"); + doc.moveDown(0.5); + data.history.slice(0, 20).forEach((entry) => { + const period = entry.startDate === entry.endDate + ? formatDateFr(entry.startDate) + : `${formatDateFr(entry.startDate)} au ${formatDateFr(entry.endDate)}`; + doc.fontSize(10).font("Helvetica-Bold").text(`${requestTypeLabels[entry.type] || entry.type} - ${entry.associationName}`); + doc.font("Helvetica").text(`${entry.title} • ${period} • ${entry.resources || "-"} • ${statusLabels[entry.status] || entry.status}`); + doc.moveDown(0.35); + }); + + doc.end(); + const buffer = await done; + return { + buffer, + fileName: `${buildFileBaseName(data)}.pdf`, + contentType: "application/pdf", + }; +} + +export function generateReservationAnalyticsExcel(data: AnalyticsPayload) { + const workbook = XLSX.utils.book_new(); + + const summaryRows = [ + ["Rapport statistiques CCDS", ""], + ["Période", `${data.periodLabel} (${data.startDate} au ${data.endDate})`], + ["Comparaison", data.comparisonLabel], + [], + ["Indicateur", "Valeur"], + ["Réservations totales", data.summary.totalReservations], + ["Réservations de salles", data.summary.salleReservations], + ["Réservations de matériel", data.summary.materialReservations], + ["Taux d'occupation des salles", `${data.summary.occupancyRate}%`], + ["Associations utilisatrices", data.summary.activeAssociations], + ["Unités matérielles demandées", data.summary.totalMaterialUnits], + ["Fréquence moyenne par association", data.summary.averageReservationsPerAssociation], + ]; + + const associationRows = [ + ["Association", "Catégories", "Total", "Salles", "Matériel", "Dernière réservation"], + ...data.associationUsage.map((item) => [ + item.associationName, + item.thematics.join(" / "), + item.totalReservations, + item.salleReservations, + item.materialReservations, + item.lastReservationAt, + ]), + ]; + + const roomRows = [ + ["Salle", "Jours réservés", "Taux d'occupation"], + ...data.roomOccupancy.map((item) => [item.roomName, item.bookedDays, `${item.occupancyRate}%`]), + ]; + + const materialRows = [ + ["Matériel", "Quantité", "Dossiers"], + ...data.materialUsage.map((item) => [item.label, item.quantity, item.requests]), + ]; + + const historyRows = [ + ["Type", "Association", "Dossier", "Début", "Fin", "Ressources", "Statut"], + ...data.history.map((item) => [ + requestTypeLabels[item.type] || item.type, + item.associationName, + item.title, + item.startDate, + item.endDate, + item.resources, + statusLabels[item.status] || item.status, + ]), + ]; + + XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(summaryRows), "Synthèse"); + XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(associationRows), "Associations"); + XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(roomRows), "Salles"); + XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(materialRows), "Matériel"); + XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(historyRows), "Historique"); + + const buffer = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" }) as Buffer; + return { + buffer, + fileName: `${buildFileBaseName(data)}.xlsx`, + contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }; +} diff --git a/server/storage.ts b/server/storage.ts new file mode 100644 index 0000000..2c5ca33 --- /dev/null +++ b/server/storage.ts @@ -0,0 +1,39 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +function normalizeKey(relKey: string): string { + return relKey + .replace(/^\/+/, "") + .split("/") + .filter(Boolean) + .map(segment => segment.replace(/[^a-zA-Z0-9._-]/g, "_")) + .join("/"); +} + +function getUploadRoot() { + return path.resolve(process.cwd(), "uploads"); +} + +function getPublicUrl(key: string) { + return `/uploads/${key}`; +} + +export async function storagePut( + relKey: string, + data: Buffer | Uint8Array | string, + contentType = "application/octet-stream" +): Promise<{ key: string; url: string }> { + const key = normalizeKey(relKey); + const filePath = path.join(getUploadRoot(), key); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, data); + return { key, url: getPublicUrl(key) }; +} + +export async function storageGet(relKey: string): Promise<{ key: string; url: string; }> { + const key = normalizeKey(relKey); + return { + key, + url: getPublicUrl(key), + }; +} diff --git a/server/templates/fiche_etat_des_lieux_officielle.pdf b/server/templates/fiche_etat_des_lieux_officielle.pdf new file mode 100644 index 0000000000000000000000000000000000000000..959318757c1362d323bc3ec042d23ab9a5d9ed00 GIT binary patch literal 103411 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⪙lhgGwecD6d{|gHhDJCgEnE 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 QJ1Zv(DixK4k|gT?04sE#M*si- literal 0 HcmV?d00001 diff --git a/shared/_core/errors.ts b/shared/_core/errors.ts new file mode 100644 index 0000000..15fa7ed --- /dev/null +++ b/shared/_core/errors.ts @@ -0,0 +1,19 @@ +/** + * Base HTTP error class with status code. + * Throw this from route handlers to send specific HTTP errors. + */ +export class HttpError extends Error { + constructor( + public statusCode: number, + message: string + ) { + super(message); + this.name = "HttpError"; + } +} + +// Convenience constructors +export const BadRequestError = (msg: string) => new HttpError(400, msg); +export const UnauthorizedError = (msg: string) => new HttpError(401, msg); +export const ForbiddenError = (msg: string) => new HttpError(403, msg); +export const NotFoundError = (msg: string) => new HttpError(404, msg); diff --git a/shared/appearance.ts b/shared/appearance.ts new file mode 100644 index 0000000..7026120 --- /dev/null +++ b/shared/appearance.ts @@ -0,0 +1,764 @@ +export const appearanceThemeModes = ["light", "dark", "system"] as const; +export const appearanceFontFamilies = ["marianne", "system", "rounded", "serif"] as const; +export const appearanceCardStyles = ["soft", "outlined", "elevated"] as const; +export const appearanceTableStyles = ["comfortable", "compact", "striped"] as const; +export const appearanceDisplayDensities = ["comfortable", "compact", "detailed"] as const; +export const appearanceSidebarStyles = ["soft", "contrast", "floating"] as const; +export const appearanceButtonStyles = ["rounded", "pill", "square"] as const; +export const appearanceInputStyles = ["outline", "soft", "filled"] as const; +export const appearanceBackgroundModes = ["cover", "repeat", "fixed"] as const; +export const appearanceBackgroundPresentationModes = ["full", "edge_frame"] as const; +export const appearanceDecorationScopes = [ + "all", + "home", + "public", + "public_directory", + "public_map", + "dashboard", + "dashboard_home", + "dashboard_profile", + "dashboard_documents", + "dashboard_requests", + "dashboard_new_request", + "dashboard_reservation_salle", + "dashboard_material", + "dashboard_directory", + "admin", + "admin_pilotage", + "admin_logistics", + "admin_directory", + "admin_compliance", +] as const; +export const appearanceDecorationLayers = ["background", "card"] as const; +export const appearanceCardDecorationAnchors = ["top_left", "top_right", "bottom_left", "bottom_right"] as const; +export const appearanceBackgroundPositions = [ + "center", + "top", + "bottom", + "left", + "right", + "top_left", + "top_right", + "bottom_left", + "bottom_right", +] as const; +export const appearanceBackgroundTargets = [ + "home", + "public", + "public_directory", + "public_map", + "dashboard", + "dashboard_home", + "dashboard_profile", + "dashboard_documents", + "dashboard_requests", + "dashboard_new_request", + "dashboard_reservation_salle", + "dashboard_material", + "dashboard_directory", + "admin", + "admin_pilotage", + "admin_logistics", + "admin_directory", + "admin_compliance", +] as const; +export const appearanceAssetFields = ["logoUrl", "faviconUrl", "heroImageUrl", "backgroundImageUrl"] as const; + +export type AppearanceThemeMode = (typeof appearanceThemeModes)[number]; +export type AppearanceFontFamily = (typeof appearanceFontFamilies)[number]; +export type AppearanceCardStyle = (typeof appearanceCardStyles)[number]; +export type AppearanceTableStyle = (typeof appearanceTableStyles)[number]; +export type AppearanceDisplayDensity = (typeof appearanceDisplayDensities)[number]; +export type AppearanceSidebarStyle = (typeof appearanceSidebarStyles)[number]; +export type AppearanceButtonStyle = (typeof appearanceButtonStyles)[number]; +export type AppearanceInputStyle = (typeof appearanceInputStyles)[number]; +export type AppearanceBackgroundMode = (typeof appearanceBackgroundModes)[number]; +export type AppearanceBackgroundPresentationMode = (typeof appearanceBackgroundPresentationModes)[number]; +export type AppearanceDecorationScope = (typeof appearanceDecorationScopes)[number]; +export type AppearanceDecorationLayer = (typeof appearanceDecorationLayers)[number]; +export type AppearanceCardDecorationAnchor = (typeof appearanceCardDecorationAnchors)[number]; +export type AppearanceBackgroundPosition = (typeof appearanceBackgroundPositions)[number]; +export type AppearanceBackgroundTarget = (typeof appearanceBackgroundTargets)[number]; +export type AppearanceAssetField = (typeof appearanceAssetFields)[number]; + +export interface PortalAppearanceDecoration { + id: string; + imageUrl: string; + scope: AppearanceDecorationScope; + layer: AppearanceDecorationLayer; + widthPercent: number; + topPercent: number; + leftPercent: number; + opacity: number; + blurRadius: number; + cardAnchor: AppearanceCardDecorationAnchor; + cardInsetPercent: number; +} + +export interface PortalAppearance { + themeMode: AppearanceThemeMode; + activeSkinId: string; + portalTitle: string; + portalTagline: string; + primaryColor: string; + secondaryColor: string; + accentColor: string; + buttonColor: string; + alertColor: string; + fontFamily: AppearanceFontFamily; + headingFontFamily: AppearanceFontFamily; + headingScalePercent: number; + bodyScalePercent: number; + logoUrl: string; + faviconUrl: string; + heroImageUrl: string; + backgroundImageUrl: string; + backgroundEnabled: boolean; + backgroundMode: AppearanceBackgroundMode; + backgroundPresentationMode: AppearanceBackgroundPresentationMode; + backgroundOverlayOpacity: number; + backgroundBlurRadius: number; + backgroundTargets: AppearanceBackgroundTarget[]; + backgroundTargetImageUrls: Partial>; + backgroundTargetOverlayOpacities: Partial>; + backgroundTargetBlurRadii: Partial>; + backgroundTargetPositions: Partial>; + backgroundTargetPresentationModes: Partial>; + heroImagePositionX: number; + heroImagePositionY: number; + heroOverlayOpacity: number; + heroTitlePositionX: number; + heroTitlePositionY: number; + heroTitleWidthPercent: number; + heroTaglinePositionX: number; + heroTaglinePositionY: number; + heroTaglineWidthPercent: number; + decorativeElements: PortalAppearanceDecoration[]; + cardStyle: AppearanceCardStyle; + tableStyle: AppearanceTableStyle; + displayDensity: AppearanceDisplayDensity; + sidebarStyle: AppearanceSidebarStyle; + buttonStyle: AppearanceButtonStyle; + inputStyle: AppearanceInputStyle; +} + +export interface AppearanceSkinPreset { + id: string; + label: string; + description: string; + category: string; + usage: string[]; + value: Partial; +} + +export const APPEARANCE_SETTING_KEY = "appearance"; + +export const defaultPortalAppearance: PortalAppearance = { + themeMode: "light", + activeSkinId: "guyane-institutionnelle", + portalTitle: "Portail Associations 973", + portalTagline: "Démarches, annuaire et services du tissu associatif guyanais", + primaryColor: "#2e7d32", + secondaryColor: "#1565c0", + accentColor: "#f9a825", + buttonColor: "#1565c0", + alertColor: "#dc2626", + fontFamily: "marianne", + headingFontFamily: "marianne", + headingScalePercent: 100, + bodyScalePercent: 100, + logoUrl: "", + faviconUrl: "", + heroImageUrl: "", + backgroundImageUrl: "", + backgroundEnabled: false, + backgroundMode: "cover", + backgroundPresentationMode: "full", + backgroundOverlayOpacity: 72, + backgroundBlurRadius: 0, + backgroundTargets: ["home", "public", "dashboard", "admin"], + backgroundTargetImageUrls: {}, + backgroundTargetOverlayOpacities: {}, + backgroundTargetBlurRadii: {}, + backgroundTargetPositions: {}, + backgroundTargetPresentationModes: {}, + heroImagePositionX: 50, + heroImagePositionY: 50, + heroOverlayOpacity: 74, + heroTitlePositionX: 0, + heroTitlePositionY: 0, + heroTitleWidthPercent: 50, + heroTaglinePositionX: 0, + heroTaglinePositionY: 0, + heroTaglineWidthPercent: 80, + decorativeElements: [], + cardStyle: "soft", + tableStyle: "comfortable", + displayDensity: "comfortable", + sidebarStyle: "soft", + buttonStyle: "rounded", + inputStyle: "outline", +}; + +export const appearanceSkinPresets: AppearanceSkinPreset[] = [ + { + id: "guyane-institutionnelle", + label: "Guyane institutionnelle", + description: "Palette ancrée dans le territoire, pensée pour les collectivités, services publics et fédérations.", + category: "Institutionnel", + usage: ["collectivités", "mairies", "fédérations", "services publics"], + value: { + activeSkinId: "guyane-institutionnelle", + portalTitle: "Portail Associations 973", + portalTagline: "L’espace territorial des démarches associatives en Guyane", + primaryColor: "#2e7d32", + secondaryColor: "#1565c0", + accentColor: "#f9a825", + buttonColor: "#1565c0", + alertColor: "#c62828", + fontFamily: "marianne", + headingFontFamily: "marianne", + cardStyle: "soft", + tableStyle: "comfortable", + displayDensity: "comfortable", + sidebarStyle: "soft", + buttonStyle: "rounded", + inputStyle: "outline", + backgroundEnabled: false, + backgroundMode: "cover", + backgroundPresentationMode: "full", + backgroundOverlayOpacity: 72, + backgroundBlurRadius: 0, + backgroundTargets: ["home", "public", "dashboard", "admin"], + backgroundTargetImageUrls: {}, + themeMode: "light", + }, + }, + { + id: "centre-spatial-guyanais", + label: "Centre spatial guyanais", + description: "Ambiance technologique et institutionnelle, inspirée du spatial, de l’innovation et des grands projets.", + category: "Spatial", + usage: ["innovation", "recherche", "éducation", "projets scientifiques"], + value: { + activeSkinId: "centre-spatial-guyanais", + portalTitle: "Portail Associations 973", + portalTagline: "Innovation, projets et coopérations du territoire guyanais", + primaryColor: "#0b3d91", + secondaryColor: "#1f5fbf", + accentColor: "#f6b317", + buttonColor: "#155eef", + alertColor: "#d92d20", + fontFamily: "system", + headingFontFamily: "rounded", + cardStyle: "elevated", + tableStyle: "striped", + displayDensity: "compact", + sidebarStyle: "contrast", + buttonStyle: "pill", + inputStyle: "soft", + backgroundEnabled: false, + backgroundMode: "cover", + backgroundPresentationMode: "full", + backgroundOverlayOpacity: 68, + backgroundBlurRadius: 0, + backgroundTargets: ["home", "public", "dashboard", "admin"], + backgroundTargetImageUrls: {}, + themeMode: "light", + }, + }, + { + id: "nature-amazonienne", + label: "Nature amazonienne", + description: "Univers plus organique, centré sur la biodiversité, l’environnement et les initiatives de terrain.", + category: "Territoire", + usage: ["environnement", "tourisme", "biodiversité", "éducation populaire"], + value: { + activeSkinId: "nature-amazonienne", + portalTitle: "Portail Associations 973", + portalTagline: "Les initiatives guyanaises entre nature, territoire et engagement", + primaryColor: "#1b5e20", + secondaryColor: "#4caf50", + accentColor: "#0277bd", + buttonColor: "#2e7d32", + alertColor: "#d84315", + fontFamily: "serif", + headingFontFamily: "serif", + cardStyle: "outlined", + tableStyle: "comfortable", + displayDensity: "detailed", + sidebarStyle: "floating", + buttonStyle: "rounded", + inputStyle: "filled", + backgroundEnabled: false, + backgroundMode: "cover", + backgroundPresentationMode: "full", + backgroundOverlayOpacity: 70, + backgroundBlurRadius: 0, + backgroundTargets: ["home", "public", "dashboard", "admin"], + backgroundTargetImageUrls: {}, + themeMode: "light", + }, + }, + { + id: "republicain", + label: "Républicain", + description: "Lecture claire, signal institutionnel fort, pour les espaces administratifs et les usages plus officiels.", + category: "Administration", + usage: ["préfecture", "administration", "services publics", "collectivités"], + value: { + activeSkinId: "republicain", + portalTitle: "Portail Associations 973", + portalTagline: "Un portail administratif lisible, fiable et immédiatement identifiable", + primaryColor: "#0055a4", + secondaryColor: "#ffffff", + accentColor: "#ef4135", + buttonColor: "#0055a4", + alertColor: "#b42318", + fontFamily: "marianne", + headingFontFamily: "marianne", + cardStyle: "outlined", + tableStyle: "compact", + displayDensity: "compact", + sidebarStyle: "contrast", + buttonStyle: "square", + inputStyle: "outline", + backgroundEnabled: false, + backgroundMode: "cover", + backgroundPresentationMode: "full", + backgroundOverlayOpacity: 78, + backgroundBlurRadius: 0, + backgroundTargets: ["home", "public", "dashboard", "admin"], + backgroundTargetImageUrls: {}, + themeMode: "light", + }, + }, + { + id: "institution-immersif", + label: "Institution immersif", + description: "Preset prêt à l’emploi pour un fond premium lisible, avec centre neutre et bords illustrés renforcés.", + category: "Immersif", + usage: ["dashboard", "portail institutionnel", "parcours connectés", "habillage premium"], + value: { + activeSkinId: "institution-immersif", + portalTitle: "Portail Associations 973", + portalTagline: "Un portail territorial clair, immersif et immédiatement lisible", + primaryColor: "#2e7d32", + secondaryColor: "#1565c0", + accentColor: "#f9a825", + buttonColor: "#1565c0", + alertColor: "#c62828", + fontFamily: "marianne", + headingFontFamily: "marianne", + cardStyle: "soft", + tableStyle: "comfortable", + displayDensity: "compact", + sidebarStyle: "soft", + buttonStyle: "rounded", + inputStyle: "outline", + backgroundEnabled: true, + backgroundMode: "cover", + backgroundPresentationMode: "edge_frame", + backgroundOverlayOpacity: 62, + backgroundBlurRadius: 0, + backgroundTargets: ["home", "public", "dashboard", "admin"], + backgroundTargetImageUrls: {}, + backgroundTargetOverlayOpacities: { + home: 58, + public: 64, + dashboard: 60, + admin: 68, + }, + backgroundTargetPositions: { + home: "center", + public: "center", + dashboard: "right", + admin: "right", + }, + backgroundTargetPresentationModes: { + home: "edge_frame", + public: "edge_frame", + dashboard: "edge_frame", + admin: "edge_frame", + }, + themeMode: "light", + }, + }, + { + id: "parcours-guide-immersif", + label: "Parcours guidé immersif", + description: "Met en avant les parcours, les formulaires et les cartes avec un habillage plus narratif et très lisible.", + category: "Immersif", + usage: ["nouvelle demande", "réservation", "matériel", "parcours guidés"], + value: { + activeSkinId: "parcours-guide-immersif", + portalTitle: "Portail Associations 973", + portalTagline: "Des démarches plus claires, étape par étape", + primaryColor: "#2f9e44", + secondaryColor: "#1d4ed8", + accentColor: "#f6c453", + buttonColor: "#2563eb", + alertColor: "#c2410c", + fontFamily: "marianne", + headingFontFamily: "rounded", + cardStyle: "soft", + tableStyle: "comfortable", + displayDensity: "compact", + sidebarStyle: "floating", + buttonStyle: "pill", + inputStyle: "soft", + backgroundEnabled: true, + backgroundMode: "cover", + backgroundPresentationMode: "edge_frame", + backgroundOverlayOpacity: 56, + backgroundBlurRadius: 1, + backgroundTargets: [ + "dashboard", + "dashboard_home", + "dashboard_new_request", + "dashboard_reservation_salle", + "dashboard_material", + ], + backgroundTargetImageUrls: {}, + backgroundTargetOverlayOpacities: { + dashboard: 58, + dashboard_home: 54, + dashboard_new_request: 52, + dashboard_reservation_salle: 56, + dashboard_material: 56, + }, + backgroundTargetPositions: { + dashboard: "right", + dashboard_home: "right", + dashboard_new_request: "right", + dashboard_reservation_salle: "bottom_right", + dashboard_material: "bottom_left", + }, + backgroundTargetPresentationModes: { + dashboard: "edge_frame", + dashboard_home: "edge_frame", + dashboard_new_request: "edge_frame", + dashboard_reservation_salle: "edge_frame", + dashboard_material: "edge_frame", + }, + themeMode: "light", + }, + }, + { + id: "pilotage-territorial", + label: "Pilotage territorial", + description: "Preset plus sobre et plus dense, pensé pour le bordereau, la logistique et les référentiels admin.", + category: "Immersif", + usage: ["bordereau", "logistique", "référentiels", "pilotage admin"], + value: { + activeSkinId: "pilotage-territorial", + portalTitle: "Portail Associations 973", + portalTagline: "Référentiels, logistique et pilotage du territoire", + primaryColor: "#256f5c", + secondaryColor: "#0f4c81", + accentColor: "#d97706", + buttonColor: "#0f4c81", + alertColor: "#b91c1c", + fontFamily: "marianne", + headingFontFamily: "marianne", + cardStyle: "outlined", + tableStyle: "compact", + displayDensity: "compact", + sidebarStyle: "soft", + buttonStyle: "rounded", + inputStyle: "outline", + backgroundEnabled: true, + backgroundMode: "cover", + backgroundPresentationMode: "edge_frame", + backgroundOverlayOpacity: 68, + backgroundBlurRadius: 0, + backgroundTargets: ["admin", "admin_pilotage", "admin_logistics", "admin_directory", "admin_compliance"], + backgroundTargetImageUrls: {}, + backgroundTargetOverlayOpacities: { + admin: 68, + admin_pilotage: 70, + admin_logistics: 66, + admin_directory: 64, + admin_compliance: 72, + }, + backgroundTargetPositions: { + admin: "right", + admin_pilotage: "center", + admin_logistics: "bottom_right", + admin_directory: "left", + admin_compliance: "top_right", + }, + backgroundTargetPresentationModes: { + admin: "edge_frame", + admin_pilotage: "edge_frame", + admin_logistics: "edge_frame", + admin_directory: "edge_frame", + admin_compliance: "edge_frame", + }, + themeMode: "light", + }, + }, +]; + +const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}){1,2}$/; + +function normalizeHexColor(value: string | undefined, fallback: string) { + const trimmed = value?.trim(); + if (!trimmed || !HEX_COLOR_REGEX.test(trimmed)) { + return fallback; + } + + if (trimmed.length === 4) { + return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`.toLowerCase(); + } + + return trimmed.toLowerCase(); +} + +function normalizeUrl(value: string | undefined) { + return value?.trim() || ""; +} + +function normalizeText(value: string | undefined, fallback: string, maxLength: number) { + const trimmed = value?.trim(); + if (!trimmed) { + return fallback; + } + return trimmed.slice(0, maxLength); +} + +function normalizePercent(value: number | undefined, fallback: number, min = 0, max = 100) { + if (typeof value !== "number" || Number.isNaN(value)) { + return fallback; + } + + return Math.min(max, Math.max(min, Math.round(value))); +} + +function normalizeOption(value: string | undefined, allowed: T, fallback: T[number]) { + if (value && allowed.includes(value)) { + return value as T[number]; + } + return fallback; +} + +function normalizeOptionsArray(value: string[] | undefined, allowed: T, fallback: readonly T[number][]) { + if (!Array.isArray(value)) { + return [...fallback] as T[number][]; + } + + const filtered = value.filter((entry): entry is T[number] => allowed.includes(entry as T[number])); + return filtered.length ? Array.from(new Set(filtered)) : [...fallback] as T[number][]; +} + +function normalizeTargetImageUrls( + value: Partial> | undefined, + allowed: readonly AppearanceBackgroundTarget[], +) { + const next: Partial> = {}; + if (!value || typeof value !== "object") { + return next; + } + + for (const target of allowed) { + const normalized = normalizeUrl(value[target]); + if (normalized) { + next[target] = normalized; + } + } + + return next; +} + +function normalizeTargetOverlayOpacities( + value: Partial> | undefined, + allowed: readonly AppearanceBackgroundTarget[], +) { + const next: Partial> = {}; + if (!value || typeof value !== "object") { + return next; + } + + for (const target of allowed) { + const raw = value[target]; + if (typeof raw === "number" && !Number.isNaN(raw)) { + next[target] = Math.min(100, Math.max(0, Math.round(raw))); + } + } + + return next; +} + +function normalizeTargetBlurRadii( + value: Partial> | undefined, + allowed: readonly AppearanceBackgroundTarget[], +) { + const next: Partial> = {}; + if (!value || typeof value !== "object") { + return next; + } + + for (const target of allowed) { + const raw = value[target]; + if (typeof raw === "number" && !Number.isNaN(raw)) { + next[target] = Math.min(24, Math.max(0, Math.round(raw))); + } + } + + return next; +} + +function normalizeTargetPositions( + value: Partial> | undefined, + allowed: readonly AppearanceBackgroundTarget[], +) { + const next: Partial> = {}; + if (!value || typeof value !== "object") { + return next; + } + + for (const target of allowed) { + const raw = value[target]; + if (raw && appearanceBackgroundPositions.includes(raw as AppearanceBackgroundPosition)) { + next[target] = raw as AppearanceBackgroundPosition; + } + } + + return next; +} + +function normalizeTargetPresentationModes( + value: Partial> | undefined, + allowed: readonly AppearanceBackgroundTarget[], +) { + const next: Partial> = {}; + if (!value || typeof value !== "object") { + return next; + } + + for (const target of allowed) { + const raw = value[target]; + if (raw && appearanceBackgroundPresentationModes.includes(raw as AppearanceBackgroundPresentationMode)) { + next[target] = raw as AppearanceBackgroundPresentationMode; + } + } + + return next; +} + +function normalizeDecorativeElements(value: unknown): PortalAppearanceDecoration[] { + if (!Array.isArray(value)) { + return []; + } + + return value + .map((entry, index) => { + const raw = entry && typeof entry === "object" ? entry as Partial : {}; + const imageUrl = normalizeUrl(raw.imageUrl); + if (!imageUrl) { + return null; + } + + return { + id: normalizeText(raw.id, `decor-${index + 1}`, 80), + imageUrl, + scope: normalizeOption(raw.scope, appearanceDecorationScopes, "all"), + layer: normalizeOption(raw.layer, appearanceDecorationLayers, "background"), + widthPercent: normalizePercent(raw.widthPercent, 18, 6, 38), + topPercent: normalizePercent(raw.topPercent, 20, 0, 100), + leftPercent: normalizePercent(raw.leftPercent, 10, 0, 100), + opacity: normalizePercent(raw.opacity, 12, 4, 28), + blurRadius: normalizePercent(raw.blurRadius, 2, 0, 12), + cardAnchor: normalizeOption(raw.cardAnchor, appearanceCardDecorationAnchors, "top_right"), + cardInsetPercent: normalizePercent(raw.cardInsetPercent, 6, 0, 20), + } satisfies PortalAppearanceDecoration; + }) + .filter((entry): entry is PortalAppearanceDecoration => Boolean(entry)) + .slice(0, 6); +} + +export function getAppearanceSkinPreset(presetId: string | undefined) { + return appearanceSkinPresets.find((preset) => preset.id === presetId) || null; +} + +export function sanitizePortalAppearance(value?: Partial | null): PortalAppearance { + const preset = getAppearanceSkinPreset(value?.activeSkinId) || getAppearanceSkinPreset(defaultPortalAppearance.activeSkinId); + const presetValue = preset?.value || {}; + + const base: PortalAppearance = { + ...defaultPortalAppearance, + ...presetValue, + }; + const normalizedBackgroundImageUrl = normalizeUrl(value?.backgroundImageUrl); + const normalizedBackgroundTargetImageUrls = normalizeTargetImageUrls(value?.backgroundTargetImageUrls, appearanceBackgroundTargets); + const normalizedBackgroundTargetOverlayOpacities = normalizeTargetOverlayOpacities( + value?.backgroundTargetOverlayOpacities, + appearanceBackgroundTargets + ); + const normalizedBackgroundTargetBlurRadii = normalizeTargetBlurRadii( + value?.backgroundTargetBlurRadii, + appearanceBackgroundTargets + ); + const normalizedBackgroundTargetPositions = normalizeTargetPositions( + value?.backgroundTargetPositions, + appearanceBackgroundTargets + ); + const normalizedBackgroundTargetPresentationModes = normalizeTargetPresentationModes( + value?.backgroundTargetPresentationModes, + appearanceBackgroundTargets + ); + const normalizedDecorativeElements = normalizeDecorativeElements(value?.decorativeElements); + const hasAnyBackgroundAsset = + Boolean(normalizedBackgroundImageUrl) || Object.values(normalizedBackgroundTargetImageUrls).some(Boolean); + + return { + themeMode: normalizeOption(value?.themeMode, appearanceThemeModes, base.themeMode), + activeSkinId: normalizeText(value?.activeSkinId, base.activeSkinId, 80), + portalTitle: normalizeText(value?.portalTitle, base.portalTitle, 120), + portalTagline: normalizeText(value?.portalTagline, base.portalTagline, 220), + primaryColor: normalizeHexColor(value?.primaryColor, base.primaryColor), + secondaryColor: normalizeHexColor(value?.secondaryColor, base.secondaryColor), + accentColor: normalizeHexColor(value?.accentColor, base.accentColor), + buttonColor: normalizeHexColor(value?.buttonColor, base.buttonColor), + alertColor: normalizeHexColor(value?.alertColor, base.alertColor), + fontFamily: normalizeOption(value?.fontFamily, appearanceFontFamilies, base.fontFamily), + headingFontFamily: normalizeOption(value?.headingFontFamily, appearanceFontFamilies, base.headingFontFamily), + headingScalePercent: normalizePercent(value?.headingScalePercent, base.headingScalePercent, 85, 130), + bodyScalePercent: normalizePercent(value?.bodyScalePercent, base.bodyScalePercent, 90, 115), + logoUrl: normalizeUrl(value?.logoUrl), + faviconUrl: normalizeUrl(value?.faviconUrl), + heroImageUrl: normalizeUrl(value?.heroImageUrl), + backgroundImageUrl: normalizedBackgroundImageUrl, + backgroundEnabled: Boolean(value?.backgroundEnabled) && hasAnyBackgroundAsset, + backgroundMode: normalizeOption(value?.backgroundMode, appearanceBackgroundModes, base.backgroundMode), + backgroundPresentationMode: normalizeOption( + value?.backgroundPresentationMode, + appearanceBackgroundPresentationModes, + base.backgroundPresentationMode + ), + backgroundOverlayOpacity: normalizePercent(value?.backgroundOverlayOpacity, base.backgroundOverlayOpacity, 0, 100), + backgroundBlurRadius: normalizePercent(value?.backgroundBlurRadius, base.backgroundBlurRadius, 0, 24), + backgroundTargets: normalizeOptionsArray(value?.backgroundTargets, appearanceBackgroundTargets, base.backgroundTargets), + backgroundTargetImageUrls: normalizedBackgroundTargetImageUrls, + backgroundTargetOverlayOpacities: normalizedBackgroundTargetOverlayOpacities, + backgroundTargetBlurRadii: normalizedBackgroundTargetBlurRadii, + backgroundTargetPositions: normalizedBackgroundTargetPositions, + backgroundTargetPresentationModes: normalizedBackgroundTargetPresentationModes, + heroImagePositionX: normalizePercent(value?.heroImagePositionX, base.heroImagePositionX), + heroImagePositionY: normalizePercent(value?.heroImagePositionY, base.heroImagePositionY), + heroOverlayOpacity: normalizePercent(value?.heroOverlayOpacity, base.heroOverlayOpacity), + heroTitlePositionX: normalizePercent(value?.heroTitlePositionX, base.heroTitlePositionX, -50, 50), + heroTitlePositionY: normalizePercent(value?.heroTitlePositionY, base.heroTitlePositionY, -50, 50), + heroTitleWidthPercent: normalizePercent(value?.heroTitleWidthPercent, base.heroTitleWidthPercent, 30, 100), + heroTaglinePositionX: normalizePercent(value?.heroTaglinePositionX, base.heroTaglinePositionX, -50, 50), + heroTaglinePositionY: normalizePercent(value?.heroTaglinePositionY, base.heroTaglinePositionY, -50, 50), + heroTaglineWidthPercent: normalizePercent(value?.heroTaglineWidthPercent, base.heroTaglineWidthPercent, 30, 100), + decorativeElements: normalizedDecorativeElements, + cardStyle: normalizeOption(value?.cardStyle, appearanceCardStyles, base.cardStyle), + tableStyle: normalizeOption(value?.tableStyle, appearanceTableStyles, base.tableStyle), + displayDensity: normalizeOption(value?.displayDensity, appearanceDisplayDensities, base.displayDensity), + sidebarStyle: normalizeOption(value?.sidebarStyle, appearanceSidebarStyles, base.sidebarStyle), + buttonStyle: normalizeOption(value?.buttonStyle, appearanceButtonStyles, base.buttonStyle), + inputStyle: normalizeOption(value?.inputStyle, appearanceInputStyles, base.inputStyle), + }; +} diff --git a/shared/associationCommunes.ts b/shared/associationCommunes.ts new file mode 100644 index 0000000..53024e9 --- /dev/null +++ b/shared/associationCommunes.ts @@ -0,0 +1,44 @@ +export const associationCommuneOptions = [ + { value: "all", label: "Toutes" }, + { value: "kourou", label: "Kourou" }, + { value: "sinnamary", label: "Sinnamary" }, + { value: "iracoubo", label: "Iracoubo" }, + { value: "saint_elie", label: "Saint-Élie" }, +] as const; + +export type AssociationCommuneFilter = (typeof associationCommuneOptions)[number]["value"]; + +const communeVariantMap: Record, string[]> = { + kourou: ["Kourou"], + sinnamary: ["Sinnamary"], + iracoubo: ["Iracoubo"], + saint_elie: ["Saint-Élie", "Saint Elie", "ST ELIE", "St Elie", "St-Élie", "Saint-Elie"], +}; + +export function getAssociationCommuneLabel(value: AssociationCommuneFilter) { + return associationCommuneOptions.find(option => option.value === value)?.label ?? "Toutes"; +} + +export function normalizeAssociationCommune(value: string | null | undefined): AssociationCommuneFilter { + const normalized = (value || "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[-_]/g, " ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); + + if (normalized.includes("saint elie") || normalized.includes("st elie")) return "saint_elie"; + if (normalized.includes("kourou")) return "kourou"; + if (normalized.includes("sinnamary")) return "sinnamary"; + if (normalized.includes("iracoubo")) return "iracoubo"; + return "all"; +} + +export function getAssociationCommuneVariants(value?: string | null) { + if (!value || value === "all") { + return []; + } + + return communeVariantMap[value as Exclude] || []; +} diff --git a/shared/associationGeo.ts b/shared/associationGeo.ts new file mode 100644 index 0000000..924e4dc --- /dev/null +++ b/shared/associationGeo.ts @@ -0,0 +1,29 @@ +export const associationGeoSources = [ + "manual", + "adresse_gouv", + "dataasso", + "commune_center", +] as const; + +export type AssociationGeoSource = (typeof associationGeoSources)[number]; + +export const associationGeoPrecisions = [ + "exact_address", + "commune_center", + "hidden", +] as const; + +export type AssociationGeoPrecision = (typeof associationGeoPrecisions)[number]; + +export const associationGeoSourceLabels: Record = { + manual: "Position définie manuellement", + adresse_gouv: "Adresse.data.gouv.fr", + dataasso: "DataAsso / référentiel association", + commune_center: "Centre de commune", +}; + +export const associationGeoPrecisionLabels: Record = { + exact_address: "Adresse exacte", + commune_center: "Centre de commune", + hidden: "Masquée du public", +}; diff --git a/shared/associationGovernance.ts b/shared/associationGovernance.ts new file mode 100644 index 0000000..7e3a579 --- /dev/null +++ b/shared/associationGovernance.ts @@ -0,0 +1,194 @@ +export const legalRepresentativeRoleValues = [ + "president", + "co_president", + "secretaire_general", + "directeur_gerant", +] as const; + +export type LegalRepresentativeRole = typeof legalRepresentativeRoleValues[number]; + +export const governanceMemberRoleValues = [ + "tresorier", + "tresorier_adjoint", + "secretaire", + "secretaire_adjoint", + "administrateur", + "charge_de_mission_referent", + "adherent_benevole", +] as const; + +export type GovernanceMemberRole = typeof governanceMemberRoleValues[number]; + +export const legalRepresentativeRoleLabels: Record = { + president: "Président", + co_president: "Co-président", + secretaire_general: "Secrétaire Général", + directeur_gerant: "Directeur / Gérant", +}; + +export const governanceMemberRoleLabels: Record = { + tresorier: "Trésorier", + tresorier_adjoint: "Trésorier adjoint", + secretaire: "Secrétaire", + secretaire_adjoint: "Secrétaire adjoint", + administrateur: "Membre du conseil d'administration / Administrateur", + charge_de_mission_referent: "Chargé de mission / Référent", + adherent_benevole: "Simple adhérent / Bénévole", +}; + +export const legalRepresentativeRoleOptions = legalRepresentativeRoleValues.map((value) => ({ + value, + label: legalRepresentativeRoleLabels[value], +})); + +export const governanceMemberRoleOptions = governanceMemberRoleValues.map((value) => ({ + value, + label: governanceMemberRoleLabels[value], +})); + +function normalizeRoleKey(value: string | null | undefined) { + return String(value || "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, ""); +} + +export function normalizeLegalRepresentativeRole(value: string | null | undefined): LegalRepresentativeRole { + const normalized = normalizeRoleKey(value); + switch (normalized) { + case "president": + case "presidente": + return "president"; + case "co_president": + case "copresident": + case "co_presidente": + return "co_president"; + case "secretaire_general": + return "secretaire_general"; + case "directeur": + case "gerant": + case "directeur_gerant": + return "directeur_gerant"; + default: + return "president"; + } +} + +export function normalizeGovernanceMemberRole(value: string | null | undefined): GovernanceMemberRole { + const normalized = normalizeRoleKey(value); + switch (normalized) { + case "tresorier": + return "tresorier"; + case "tresorier_adjoint": + return "tresorier_adjoint"; + case "secretaire": + return "secretaire"; + case "secretaire_adjoint": + return "secretaire_adjoint"; + case "administrateur": + case "membre_du_conseil_d_administration_administrateur": + return "administrateur"; + case "charge_de_mission": + case "referent": + case "charge_de_mission_referent": + return "charge_de_mission_referent"; + default: + return "adherent_benevole"; + } +} + +export type GovernancePerson = { + nom: string; + prenom: string; + email?: string | null; + telephone?: string | null; + fonction: string; +}; + +export type AssociationGovernance = { + representantLegal: GovernancePerson | null; + membres: GovernancePerson[]; +}; + +export function createEmptyGovernancePerson(fonction = ""): GovernancePerson { + return { + nom: "", + prenom: "", + email: "", + telephone: "", + fonction, + }; +} + +export function parseAssociationGovernance(value: string | null | undefined): AssociationGovernance { + if (!value) { + return { representantLegal: null, membres: [] }; + } + + try { + const parsed = JSON.parse(value) as Partial; + const normalizePerson = (person: any): GovernancePerson | null => { + if (!person || typeof person !== "object") return null; + return { + nom: typeof person.nom === "string" ? person.nom : "", + prenom: typeof person.prenom === "string" ? person.prenom : "", + email: typeof person.email === "string" ? person.email : "", + telephone: typeof person.telephone === "string" ? person.telephone : "", + fonction: typeof person.fonction === "string" ? person.fonction : "", + }; + }; + + return { + representantLegal: normalizePerson(parsed.representantLegal), + membres: Array.isArray(parsed.membres) + ? parsed.membres.map(normalizePerson).filter(Boolean) as GovernancePerson[] + : [], + }; + } catch { + return { representantLegal: null, membres: [] }; + } +} + +export function serializeAssociationGovernance( + governance: AssociationGovernance | null | undefined +) { + if (!governance?.representantLegal) return null; + return JSON.stringify(governance); +} + +export function buildDisplayName(person: GovernancePerson | null | undefined) { + if (!person) return ""; + return [person.prenom?.trim(), person.nom?.trim()].filter(Boolean).join(" ").trim(); +} + +export function sanitizeGovernance(governance: AssociationGovernance): AssociationGovernance { + const trimPerson = ( + person: GovernancePerson | null, + roleNormalizer?: (value: string | null | undefined) => string + ): GovernancePerson | null => { + if (!person) return null; + const normalized: GovernancePerson = { + nom: person.nom.trim(), + prenom: person.prenom.trim(), + email: person.email?.trim() || "", + telephone: person.telephone?.trim() || "", + fonction: roleNormalizer ? roleNormalizer(person.fonction) : person.fonction.trim(), + }; + + if (!normalized.nom && !normalized.prenom && !normalized.email && !normalized.telephone && !normalized.fonction) { + return null; + } + + return normalized; + }; + + return { + representantLegal: trimPerson(governance.representantLegal, normalizeLegalRepresentativeRole), + membres: governance.membres + .map((member) => trimPerson(member, normalizeGovernanceMemberRole)) + .filter((member): member is GovernancePerson => Boolean(member)), + }; +} diff --git a/shared/associationThematics.ts b/shared/associationThematics.ts new file mode 100644 index 0000000..ea39366 --- /dev/null +++ b/shared/associationThematics.ts @@ -0,0 +1,88 @@ +export const associationThematicValues = [ + "culture_loisirs", + "social_sante", + "education_formation", + "economie_territoire", + "environnement_patrimoine", + "institutions_divers", +] as const; + +export type AssociationThematic = typeof associationThematicValues[number]; + +export const associationThematicDefinitions: Record< + AssociationThematic, + { label: string; description: string } +> = { + culture_loisirs: { + label: "Culture & Loisirs", + description: "Arts, sport, chasse, pêche, activités civiques et religieuses.", + }, + social_sante: { + label: "Social & Santé", + description: "Caritatif, humanitaire, aide aux seniors, santé, services aux familles.", + }, + education_formation: { + label: "Éducation et formation", + description: "Écoles, formation continue, apprentissage.", + }, + economie_territoire: { + label: "Économie & Territoire", + description: "Emploi, insertion, logement, tourisme, défense d'intérêts économiques.", + }, + environnement_patrimoine: { + label: "Environnement et patrimoine", + description: "Écologie, cadre de vie, protection des monuments.", + }, + institutions_divers: { + label: "Institutions & Divers", + description: "Justice, sécurité civile, recherche, activités politiques.", + }, +}; + +export const associationThematicOptions = associationThematicValues.map((value) => ({ + value, + label: associationThematicDefinitions[value].label, + description: associationThematicDefinitions[value].description, +})); + +export function getAssociationThematicLabel(value: AssociationThematic | string | null | undefined) { + if (!value) return "Non renseignée"; + return associationThematicDefinitions[value as AssociationThematic]?.label ?? value; +} + +export function getAssociationThematicDescription(value: AssociationThematic | string | null | undefined) { + if (!value) return ""; + return associationThematicDefinitions[value as AssociationThematic]?.description ?? ""; +} + +export function parseAssociationThematics(value: string | null | undefined): AssociationThematic[] { + if (!value) return []; + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + return parsed.filter((item): item is AssociationThematic => + associationThematicValues.includes(item as AssociationThematic) + ); + } + } catch { + if (associationThematicValues.includes(value as AssociationThematic)) { + return [value as AssociationThematic]; + } + } + return []; +} + +export function serializeAssociationThematics(values: Array | null | undefined) { + const normalized = Array.from( + new Set( + (values || []).filter((item): item is AssociationThematic => + associationThematicValues.includes(item as AssociationThematic) + ) + ) + ); + return normalized.length > 0 ? JSON.stringify(normalized) : null; +} + +export function getAssociationThematicLabels(values: Array | null | undefined) { + return (values || []).map((value) => getAssociationThematicLabel(value)); +} diff --git a/shared/const.ts b/shared/const.ts new file mode 100644 index 0000000..112ffc5 --- /dev/null +++ b/shared/const.ts @@ -0,0 +1,5 @@ +export const COOKIE_NAME = "app_session_id"; +export const ONE_YEAR_MS = 1000 * 60 * 60 * 24 * 365; +export const AXIOS_TIMEOUT_MS = 30_000; +export const UNAUTHED_ERR_MSG = 'Please login (10001)'; +export const NOT_ADMIN_ERR_MSG = 'You do not have required permission (10002)'; diff --git a/shared/mailProviders.test.ts b/shared/mailProviders.test.ts new file mode 100644 index 0000000..6d6744c --- /dev/null +++ b/shared/mailProviders.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { resolveMailConfig } from "./mailProviders"; + +describe("resolveMailConfig", () => { + it("uses Gmail defaults when provider is gmail", () => { + const config = resolveMailConfig({ + smtpProvider: "gmail", + smtpFrom: "notifications@example.com", + smtpUser: "notifications@example.com", + smtpPass: "secret", + }); + + expect(config.provider).toBe("gmail"); + expect(config.host).toBe("smtp.gmail.com"); + expect(config.port).toBe(465); + expect(config.secure).toBe(true); + expect(config.ready).toBe(true); + }); + + it("uses OVH defaults when provider is ovh", () => { + const config = resolveMailConfig({ + smtpProvider: "ovh", + smtpFrom: "notifications@example.com", + }); + + expect(config.provider).toBe("ovh"); + expect(config.host).toBe("smtp.mail.ovh.net"); + expect(config.port).toBe(465); + expect(config.secure).toBe(true); + expect(config.requireTLS).toBe(false); + expect(config.ready).toBe(true); + }); + + it("supports custom SMTP host and port", () => { + const config = resolveMailConfig({ + smtpHost: "mail.example.org", + smtpPort: "2525", + smtpFrom: "notifications@example.org", + }); + + expect(config.provider).toBe("custom"); + expect(config.host).toBe("mail.example.org"); + expect(config.port).toBe(2525); + expect(config.ready).toBe(true); + }); + + it("reports missing SMTP_PASS when only SMTP_USER is present", () => { + const config = resolveMailConfig({ + smtpProvider: "outlook", + smtpFrom: "notifications@example.com", + smtpUser: "notifications@example.com", + }); + + expect(config.ready).toBe(false); + expect(config.missing).toContain("SMTP_PASS"); + }); +}); diff --git a/shared/mailProviders.ts b/shared/mailProviders.ts new file mode 100644 index 0000000..e009400 --- /dev/null +++ b/shared/mailProviders.ts @@ -0,0 +1,125 @@ +export const supportedMailProviders = ["custom", "ovh", "gmail", "outlook"] as const; + +export type SupportedMailProvider = typeof supportedMailProviders[number]; + +export type MailEnvInput = { + smtpProvider?: string; + smtpHost?: string; + smtpPort?: string; + smtpUser?: string; + smtpPass?: string; + smtpFrom?: string; + smtpSecure?: string; + smtpRequireTls?: string; +}; + +export type ResolvedMailConfig = { + provider: SupportedMailProvider; + host: string; + port: number | null; + secure: boolean; + requireTLS: boolean; + user: string; + pass: string; + from: string; + missing: string[]; + ready: boolean; +}; + +export const providerDefaults: Record< + Exclude, + { host: string; port: number; secure: boolean; requireTLS: boolean } +> = { + ovh: { + host: "smtp.mail.ovh.net", + port: 465, + secure: true, + requireTLS: false, + }, + gmail: { + host: "smtp.gmail.com", + port: 465, + secure: true, + requireTLS: false, + }, + outlook: { + host: "smtp.office365.com", + port: 587, + secure: false, + requireTLS: true, + }, +}; + +function normalizeProvider(value?: string): SupportedMailProvider { + const normalized = String(value || "") + .trim() + .toLowerCase(); + if (normalized === "ovh" || normalized === "gmail" || normalized === "outlook") { + return normalized; + } + return "custom"; +} + +function parseBoolean(value?: string, fallback = false): boolean { + const normalized = String(value || "") + .trim() + .toLowerCase(); + if (!normalized) { + return fallback; + } + return ["1", "true", "yes", "oui", "on"].includes(normalized); +} + +function parsePort(value?: string, fallback: number | null = null): number | null { + const trimmed = String(value || "").trim(); + if (!trimmed) { + return fallback; + } + const port = Number(trimmed); + if (!Number.isFinite(port) || port <= 0) { + return fallback; + } + return port; +} + +export function resolveMailConfig(input: MailEnvInput): ResolvedMailConfig { + const provider = normalizeProvider(input.smtpProvider); + const defaults = provider === "custom" ? null : providerDefaults[provider]; + const host = String(input.smtpHost || defaults?.host || "").trim(); + const port = parsePort(input.smtpPort, defaults?.port ?? null); + const secure = parseBoolean(input.smtpSecure, defaults?.secure ?? false); + const requireTLS = parseBoolean(input.smtpRequireTls, defaults?.requireTLS ?? false); + const user = String(input.smtpUser || "").trim(); + const pass = String(input.smtpPass || "").trim(); + const from = String(input.smtpFrom || "").trim(); + + const missing: string[] = []; + if (!host) { + missing.push("SMTP_HOST"); + } + if (!port) { + missing.push("SMTP_PORT"); + } + if (!from) { + missing.push("SMTP_FROM"); + } + if (user && !pass) { + missing.push("SMTP_PASS"); + } + if (pass && !user) { + missing.push("SMTP_USER"); + } + + return { + provider, + host, + port, + secure, + requireTLS, + user, + pass, + from, + missing, + ready: missing.length === 0, + }; +} diff --git a/shared/materialEvent.ts b/shared/materialEvent.ts new file mode 100644 index 0000000..49fb58f --- /dev/null +++ b/shared/materialEvent.ts @@ -0,0 +1,72 @@ +export const materialEventItems = [ + { key: "tente3x3", label: "Tente 3*3" }, + { key: "chapiteau5x5", label: "Chapiteau 5*5" }, + { key: "podium", label: "Podium" }, + { key: "autres", label: "Autres" }, +] as const; + +export const materialEventInventory: Record<(typeof materialEventItems)[number]["key"], number | null> = { + tente3x3: 12, + chapiteau5x5: 6, + podium: 2, + autres: null, +}; + +export const materialEventReplacementValues: Record<(typeof materialEventItems)[number]["key"], number | null> = { + tente3x3: 45000, + chapiteau5x5: 120000, + podium: 250000, + autres: null, +}; + +export type MaterialEventItemKey = (typeof materialEventItems)[number]["key"]; +export type MaterialEventSelection = Record; +export type MaterialEventQuantities = Record; +export type MaterialEventQuantityMap = Record; + +export const emptyMaterialEventSelection = (): MaterialEventSelection => ({ + tente3x3: false, + chapiteau5x5: false, + podium: false, + autres: false, +}); + +export const emptyMaterialEventQuantities = (): MaterialEventQuantities => ({ + tente3x3: "", + chapiteau5x5: "", + podium: "", + autres: "", +}); + +export function getMaterialEventLabel(key: MaterialEventItemKey | string) { + return materialEventItems.find((item) => item.key === key)?.label || String(key); +} + +export function parseMaterialEventQuantity(value: unknown) { + const parsed = Number.parseInt(String(value ?? "").trim(), 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; +} + +export function emptyMaterialEventQuantityMap(initialValue: number = 0): MaterialEventQuantityMap { + return { + tente3x3: initialValue, + chapiteau5x5: initialValue, + podium: initialValue, + autres: initialValue, + }; +} + +export function sanitizeMaterialEventQuantityMap(rawValue: unknown): MaterialEventQuantityMap { + const next = emptyMaterialEventQuantityMap(); + + if (!rawValue || typeof rawValue !== "object") { + return next; + } + + for (const item of materialEventItems) { + const value = (rawValue as Record)[item.key]; + next[item.key] = parseMaterialEventQuantity(value); + } + + return next; +} diff --git a/shared/privacyCompliance.ts b/shared/privacyCompliance.ts new file mode 100644 index 0000000..e9f8edb --- /dev/null +++ b/shared/privacyCompliance.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +export const DATA_PRIVACY_NOTICE_VERSION = "2026-06-11"; + +export const dataPrivacyConsentContextValues = [ + "register_account", + "save_profile", + "create_request", + "update_request", +] as const; + +export type DataPrivacyConsentContext = (typeof dataPrivacyConsentContextValues)[number]; + +export const dataPrivacyConsentContextLabels: Record = { + register_account: "Création de compte", + save_profile: "Enregistrement de la fiche association", + create_request: "Création d'une demande", + update_request: "Mise à jour d'une demande", +}; + +export const dataPrivacyConsentSchema = z.object({ + accepted: z.literal(true), + version: z.string().min(1), + context: z.enum(dataPrivacyConsentContextValues), +}); + +export type DataPrivacyConsent = z.infer; + +export function buildDataPrivacyConsent(context: DataPrivacyConsentContext): DataPrivacyConsent { + return { + accepted: true, + version: DATA_PRIVACY_NOTICE_VERSION, + context, + }; +} + diff --git a/shared/sallePricing.ts b/shared/sallePricing.ts new file mode 100644 index 0000000..da6c311 --- /dev/null +++ b/shared/sallePricing.ts @@ -0,0 +1,298 @@ +export type SalleUsageType = "conventionne" | "occasionnel"; +export type SalleReservationFrequency = "demi_journee" | "journee" | "mensuel"; +export type SalleWorkflowQuoteStatus = "a_preparer" | "en_attente_association" | "accepte" | "refuse"; +export type SalleWorkflowDirectorStatus = "a_transmettre" | "en_attente_signature" | "signee"; +export type SalleWorkflowPaymentStatus = "en_attente_paiement" | "paiement_partiel" | "paiement_recu" | "paiement_valide"; + +export type SallePricingLineItem = { + salleId: string; + salleNom: string; + categoryLabel: string; + usageType: SalleUsageType; + frequency: SalleReservationFrequency; + quantity: number; + unitAmountCents: number; + totalAmountCents: number; +}; + +export type SallePricingSummary = { + usageType: SalleUsageType; + frequency: SalleReservationFrequency; + quantity: number; + lineItems: SallePricingLineItem[]; + totalAmountCents: number; + unsupportedSalles: Array<{ + salleId: string; + salleNom: string; + }>; +}; + +export type SalleWorkflowData = { + usageType?: SalleUsageType; + frequency?: SalleReservationFrequency; + conditionsFinancieresText?: string; + decisionAdministrativeText?: string; + pricing?: SallePricingSummary; + quoteStatus?: SalleWorkflowQuoteStatus; + quoteSentAt?: string; + quoteRespondedAt?: string; + quotePdfUrl?: string; + quotePdfName?: string; + administrativeDecisionPdfUrl?: string; + administrativeDecisionPdfName?: string; + directorStatus?: SalleWorkflowDirectorStatus; + directorTransmissionAt?: string; + directorSignedAt?: string; + directorSignedByUserId?: number; + directorSignedByName?: string; + directorReturnComment?: string; + directorReturnedAt?: string; + signedQuotePdfUrl?: string; + signedQuotePdfName?: string; + signedDecisionPdfUrl?: string; + signedDecisionPdfName?: string; + signedRequestPdfUrl?: string; + signedRequestPdfName?: string; + invoicePdfUrl?: string; + invoicePdfName?: string; + invoiceGeneratedAt?: string; + paymentStatus?: SalleWorkflowPaymentStatus; + paymentDueDate?: string; + amountReceivedCents?: number; + paymentReceivedAt?: string; + paymentValidatedAt?: string; + paymentReference?: string; + paymentNotes?: string; + finalNotificationSentAt?: string; +}; + +type SalleTariffDefinition = { + salleId: string; + salleNom: string; + categoryLabel: string; + pricing: Partial>>>; +}; + +export const SALLE_USAGE_TYPE_LABELS: Record = { + conventionne: "Conventionné", + occasionnel: "Occasionnel", +}; + +export const SALLE_FREQUENCY_LABELS: Record = { + demi_journee: "Demi-journée", + journee: "Journée", + mensuel: "Mensuel", +}; + +export const SALLE_WORKFLOW_QUOTE_STATUS_LABELS: Record = { + a_preparer: "À préparer", + en_attente_association: "En attente de l'association", + accepte: "Accepté par l'association", + refuse: "Refusé par l'association", +}; + +export const SALLE_WORKFLOW_DIRECTOR_STATUS_LABELS: Record = { + a_transmettre: "À transmettre à la Directrice", + en_attente_signature: "En attente de signature", + signee: "Signée", +}; + +export const SALLE_WORKFLOW_PAYMENT_STATUS_LABELS: Record = { + en_attente_paiement: "En attente de paiement", + paiement_partiel: "Paiement partiel", + paiement_recu: "Paiement reçu", + paiement_valide: "Paiement validé", +}; + +const SALLE_TARIFFS: SalleTariffDefinition[] = [ + { + salleId: "bureau_11_17", + salleNom: "Bureau 11–17 m²", + categoryLabel: "Bureau 11–17 m²", + pricing: { + conventionne: { + journee: 1216, + mensuel: 18819, + }, + }, + }, + { + salleId: "salle_toucan", + salleNom: "Salle TOUCAN", + categoryLabel: "Salle de conférence - Formation", + pricing: { + conventionne: { + demi_journee: 7673, + journee: 15345, + }, + }, + }, + { + salleId: "salle_ibis", + salleNom: "Salle IBIS", + categoryLabel: "Espace formation", + pricing: { + conventionne: { + demi_journee: 8173, + journee: 15845, + mensuel: 25000, + }, + }, + }, + { + salleId: "salle_pelican", + salleNom: "Salle PELICAN", + categoryLabel: "Espace formation", + pricing: { + conventionne: { + demi_journee: 8173, + journee: 15845, + mensuel: 25000, + }, + }, + }, + { + salleId: "hall_amphitheatre", + salleNom: "Hall-Amphithéâtre", + categoryLabel: "Hall ouvert - 343,34 m²", + pricing: { + conventionne: { + demi_journee: 5000, + journee: 10000, + }, + }, + }, + { + salleId: "dojo", + salleNom: "DOJO", + categoryLabel: "Dojo", + pricing: { + conventionne: { + mensuel: 65000, + }, + occasionnel: { + mensuel: 25000, + }, + }, + }, +]; + +export const CONDITIONS_FINANCIERES_SALLE_TEXT = `CONDITIONS FINANCIERES - LOCATION DES ESPACES +La mise a disposition d'un espace au sein de la Maison de la Jeunesse des Savanes « Melissa ALVES » est soumise aux conditions suivantes : +1. Tarification +Les tarifs appliques sont ceux votes par le Conseil Communautaire, actualises au 1er septembre 2025. +Le montant est calcule automatiquement en fonction : +de l'espace choisi +de la duree (journee, demi-journee, mensuel) +du type d'utilisateur (conventionne / occasionnel) +2. Acompte +Un acompte peut etre demande selon la nature de l'evenement. +3. Annulation +Toute annulation doit etre signalee 48 heures a l'avance pour un eventuel remboursement de l'acompte. +4. Responsabilite +L'association est responsable : +des participants +du public +des dommages eventuels +du respect des regles de securite +Une attestation de responsabilite civile couvrant la periode doit etre fournie. +5. Entretien des locaux +Il est interdit de manger dans les salles. +Les espaces doivent etre rendus propres et en bon etat.`; + +export function buildDecisionAdministrativeText(input: { + espace: string; + dates: string; + horaires: string; + association: string; +}) { + return `DECISION ADMINISTRATIVE PRIORITAIRE +La presente decision atteste que la Maison de la Jeunesse des Savanes « Melissa ALVES » donne un avis favorable a la mise a disposition de l'espace suivant : +Espace : ${input.espace} +Dates : ${input.dates} +Horaires : ${input.horaires} +Association : ${input.association} +Cette decision est transmise avant validation finale et ne vaut pas autorisation definitive. +La mise a disposition devient effective uniquement apres : +validation du devis (si facturation) +signature de la Directrice +reception de l'attestation de responsabilite civile`; +} + +export function getSalleTariffDefinition(salleId: string) { + return SALLE_TARIFFS.find((entry) => entry.salleId === salleId) || null; +} + +export function listReservationDates(start: string, end: string) { + if (!start || !end) return [] as string[]; + const startDate = new Date(`${start}T12:00:00`); + const endDate = new Date(`${end}T12:00:00`); + if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()) || startDate > endDate) { + return [] as string[]; + } + + const values: string[] = []; + const cursor = new Date(startDate); + while (cursor <= endDate) { + values.push(cursor.toISOString().slice(0, 10)); + cursor.setDate(cursor.getDate() + 1); + } + return values; +} + +export function getReservationUnitQuantity(input: { + frequency: SalleReservationFrequency; + dateReservation?: string; + dateFinReservation?: string; +}) { + if (input.frequency === "mensuel") return 1; + const dates = listReservationDates(input.dateReservation || "", input.dateFinReservation || input.dateReservation || ""); + return Math.max(dates.length, 1); +} + +export function computeSallePricing(input: { + sallesIds: string[]; + usageType: SalleUsageType; + frequency: SalleReservationFrequency; + dateReservation?: string; + dateFinReservation?: string; +}) { + const quantity = getReservationUnitQuantity(input); + const summary: SallePricingSummary = { + usageType: input.usageType, + frequency: input.frequency, + quantity, + lineItems: [], + totalAmountCents: 0, + unsupportedSalles: [], + }; + + input.sallesIds.forEach((salleId) => { + const salle = getSalleTariffDefinition(salleId); + if (!salle) { + summary.unsupportedSalles.push({ salleId, salleNom: salleId }); + return; + } + + const unitAmountCents = salle.pricing[input.usageType]?.[input.frequency]; + if (typeof unitAmountCents !== "number") { + summary.unsupportedSalles.push({ salleId, salleNom: salle.salleNom }); + return; + } + + const lineItem: SallePricingLineItem = { + salleId, + salleNom: salle.salleNom, + categoryLabel: salle.categoryLabel, + usageType: input.usageType, + frequency: input.frequency, + quantity, + unitAmountCents, + totalAmountCents: unitAmountCents * quantity, + }; + summary.lineItems.push(lineItem); + summary.totalAmountCents += lineItem.totalAmountCents; + }); + + return summary; +} diff --git a/shared/socialLinks.ts b/shared/socialLinks.ts new file mode 100644 index 0000000..4a7f5b0 --- /dev/null +++ b/shared/socialLinks.ts @@ -0,0 +1,62 @@ +export function normalizeSocialUrl(value: string | null | undefined) { + const trimmed = (value || "").trim(); + if (!trimmed) return ""; + return trimmed; +} + +function parseSocialUrl(value: string | null | undefined) { + const trimmed = normalizeSocialUrl(value); + if (!trimmed) return null; + + try { + return new URL(trimmed); + } catch { + return null; + } +} + +function isAllowedSocialHostname(hostname: string, allowed: string[]) { + return allowed.some((domain) => hostname === domain || hostname === `www.${domain}`); +} + +export function isValidFacebookUrl(value: string | null | undefined) { + const url = parseSocialUrl(value); + if (!value || !String(value).trim()) return true; + if (!url) return false; + return url.protocol === "https:" && isAllowedSocialHostname(url.hostname.toLowerCase(), ["facebook.com", "fb.com"]); +} + +export function isValidInstagramUrl(value: string | null | undefined) { + const url = parseSocialUrl(value); + if (!value || !String(value).trim()) return true; + if (!url) return false; + return url.protocol === "https:" && isAllowedSocialHostname(url.hostname.toLowerCase(), ["instagram.com"]); +} + +export function getFacebookEmbedUrl(value: string | null | undefined) { + const trimmed = normalizeSocialUrl(value); + if (!isValidFacebookUrl(trimmed)) return null; + const url = new URL(trimmed); + const embedUrl = new URL("https://www.facebook.com/plugins/page.php"); + embedUrl.searchParams.set("href", url.toString()); + embedUrl.searchParams.set("tabs", "timeline"); + embedUrl.searchParams.set("width", "500"); + embedUrl.searchParams.set("height", "380"); + embedUrl.searchParams.set("small_header", "true"); + embedUrl.searchParams.set("adapt_container_width", "true"); + embedUrl.searchParams.set("hide_cover", "false"); + embedUrl.searchParams.set("show_facepile", "false"); + return embedUrl.toString(); +} + +export function getSocialHandle(value: string | null | undefined) { + const url = parseSocialUrl(value); + if (!url) return null; + + const handle = url.pathname + .split("/") + .map((part) => part.trim()) + .filter(Boolean)[0]; + + return handle ? `@${handle}` : null; +} diff --git a/shared/types.ts b/shared/types.ts new file mode 100644 index 0000000..47ca6e0 --- /dev/null +++ b/shared/types.ts @@ -0,0 +1,7 @@ +/** + * Unified type exports + * Import shared types from this single entry point. + */ + +export type * from "../drizzle/schema"; +export * from "./_core/errors"; diff --git a/tmp-documents-entry.png b/tmp-documents-entry.png new file mode 100644 index 0000000000000000000000000000000000000000..95889ba4a02a567d38269db01cfb669095c62a9e GIT binary patch literal 35372 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[M+xjf1ZA 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 UwCb-<>A(5f{s$E)==Z7r1u23qTL1t6 literal 0 HcmV?d00001 diff --git a/tmp-documents-header-mobile.png b/tmp-documents-header-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..aa209fa76e95ac409947b3f894f2b610a5d005ee GIT binary patch literal 34517 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 literal 0 HcmV?d00001 diff --git a/tmp-material-entry.png b/tmp-material-entry.png new file mode 100644 index 0000000000000000000000000000000000000000..06be1c554e2fcb81040dbfbde5c56c532164cadd GIT binary patch literal 15031 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 literal 0 HcmV?d00001 diff --git a/tmp-mes-demandes-mobile.png b/tmp-mes-demandes-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..29d5a50e84b82da119329b4ac678bcd2f2c9db45 GIT binary patch literal 156128 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%0DR z%l4M`XK-upp@mKPBOxG>SqH-=#Fm5D>k+Hru281$saf zR0|(1D2s$tf9^r^@g3;IoJB+cTiP6)N0}0)RX? z6CreT;6A1B#wG{sG#+;CTTlRZ6wudwJ?xmp`gPWat1>+%MdCHstA}pY1CSD5Dl2d> z*l7&`moF}vt&cVfK|FP5rey^jos4#-`>#YP&?1WKMMDsBg#1c0dB1!3_f$1~ieWm9 zN*ms8VPyK=ZJ+QlH3COudgzGqRZ^mW#&fv=&pFE>FZ($h{{&_Q@8xvSb4;Bcii3ch zJh7Y{nw+$eV+Gs(5|v}cT8_dg2UPYsxk& zH_?N<|1m=%KF*9>!}fBuaCYfVr`|Ao$Kcn%UCKMudK-7z2vD<5f_JTpAj(ujgOZ$e zuf-T;p!87pd79;uKHmu~ks?q@84XEEk`>=g>4$iMs4!l>(3wgN=ZmbB7r(=%-Ac0) z59rIRsRhQPDb{PW=ci`obf;tl%r%vaSS+@`Z120i)$B9W*EEFQFw3gqu(2(XaD*3F z#C&QX#6l0JecZzk1=3k*@>QJggj~i2R(@aIdA;vTMuwW?1%3nAnWzne94wX-o$@WuwH1KeCuoc$^M~PGKck-w<8lkk2ZCUesyJc zT<>#j-WLoU{%!!t90intWCEd7DiKAzeK3@cAuJYCR#}+}^?Ae?i*YxNjYR-PpguIl zGqubIqG7uCl}v>_V9*FDlhRL%f~45g^>e<+oI88vBK-Tk`Db2lU)Kw3%H|Dbc=dHTaSbldL@#|1rH&OGFVK z3quTQhE->bwJwt&vG?Tklq@x&f?%U4oN^A_jR~kPDJ&`WJ}M~dI$}fdtW}`Xq)XJK2AqzntVu7-N zX*EPltI9oUut%W*Ha@s>+6n+ZbSjgtOHpU!`z_)G>yk7^f|FyjBD2~cp3~77 zG15+1GH%b+K8-hc57rE8e=A+Ql@fwEVh5VydTUb;p;|atk2^*3zQGTU6X#&6jLdUj ztx456`G$DX$Vxdk_+643ur8VxToQnl!J*EdXn#1P10kMCuOtWUi>f}Qb8rrC=%rSq zg}`P(EpU2oVHl;OXpWCqy#hIPpqf}!c?J_!bOD$8X)OGqUtpytB9XBtGwOLLxRXTk z|4dAh3ALoDMlLC1+-we2R%YUAE^Rt{emfN1ojb&60VK;{M!gU?jH=5ic($cifIhs#~aJcboO(ujgrxv5)x5;cy*Qw&G zAXjX!=YR!m2-^m4BXfN0^hW=@tJN3&4Kb9zAY~N&G@*0tnZAw#T;#>cqppIVZYK#8 z>_UE_RFU_)1IDcug(4?~Ooeoy!z?pekQP9+TIODu8n-K2rQbJ9~3ATVd_l`liuhF4k(;@)0T6}U(@f8mA4Xx{zm#WyS zvem6>D&p0-&qp|H;+xxt85o0dNPt08OOBU%l_?HR+>MA$-10Ec`}hx$B4Sf~`K{#A zTfLa*e$9IA!GQOg8dKBJ2D=*wxw%~@aG9WmJ6ed+fuvp*4qhQbpGggfXJT2ABqDY%VGUoQPBhqZN%^|oyXAUKlAsd zZ`*%e-GFYq|HbsNSNU<4ICPftOayWH)%@w5_`Cn0{!%R;v9i-OtyaBfX6AAI!Yh3H zwsRqF{au;*(ec^JUfEw#v$-52V=r08+=Ev9mWYDX&-eZc`Y$E%f5hCp9wW#;y0zko z`JMZ+y=(kkY5reIdfs(BqQCORdd`AZoaIkn{uTZ&rTwwAt(|8n|Dk4dC(C~d|Mp)> zxrS4pKX3d9+QA&X3T`0#dJp(lc=f-e{&%*+%wL23cWTy<^}ST<`o~4;({=x6Z$k=y zmhH{|<87z?z-K{rQ_fo?a8VjHc*yar7`!>XwK@Lc6lCI$AV}!$&5nsI`X$o4aq=_8 zfa&mWht2Wr@V^S;%!%&-Af7wKJz)FYujE^!pJlho?~ab|0Rs1c&5W+|@oR&3<8U+A z%)1=(v8z5JqPGnv@8zT)0zi4YCB^B=B}}EVvO$(N8h5zlNHzgY&j!-!xOSNHS9^O- z%N8J`C*JQiRRy#>@_u~Ljf_w3b*>q0ZJ3x8)Ui9g>uc-sy#^<0coWf`G0QLs4reNL z{boVlnW?@>D;_VDAg&Kb)_J{QG8C9m&(KAe)g!`%%$B#ow75STmv$UFi@X*xj4yST zOi3iF%DVbgRgDY;82RN_36BDl#ptMQX!{hl8YNyb>cb^yg8P9T=`5<6QwBY_qw(vi z9I69|92zX6?W`+&9E>(@BAF=N^dcySLbo}v7*E#rhJP?01>2W(b&6*VFCY^jBko?Z z>p>?U2WRF?r zz8E35#S#nvB6|nAPzDRux|zh-nwE6EW83H%c`9^HZa0+VBc7pW?cg!$xw6uebr#}f z{pqV;EF&A4(NUwC!B2)7vqcZ2!i=|cC)4HwVn9~5++qDaph@PQ|{p)nM5~?l)_1-O%S+ZryI*O$FiVy5$$%n%-Jf)GfI+ zU(5zGPEJueWTG<^Cj1baXu2w=uh2UUv8zepdViUGMhMS8cZ z`yFH&{ND|8@F#>|SDlP{KCDKjBg@YcmI+YSk}@8lZov{P3X#SEBQ z-lG`f73hu9&^u~>XKF5MitBspLL(pOt*5IkXUw=!pOYjo4|W2}k8J(ae`EZ!^_OXy zpEpi~ZE0cVj1JM9t~A^z>rzhcOo5kI&EqTX)5Ic_H)k%vW2WFZyWBQnqAtDq>Cf&n zCX{L(Wh|1gupLc<-Cv?h1P_~rlKoqEI_412?}0VM-)fuTOs%Sfk;dT_X98IOz*)B+ z0o5~R(rhq%xoQoj$f;9JhE=hi+CTqj@-ccuCmCul*lYL8HW5S7$%e!4cXWB&V#hui zHPF0MMF}S^;c#<}{sAHXvOBNtega8M_jT!O-txo#X?3f}#*b4|^Tr3La_10LZtFNT zqqQ(BhI>GZo+Po-s{01MQUXCt?3r2ZnA)x%3&N_3Sh)Yyuz>Ww@E5-)(#qmMVB&%z zPlgcF5=FMA)(wtnZ6w>SZOg}k_o3}6*)e0lJC>+Vp1Z}~Z>e3-PCdp0KNZ4<_k|bG zG_{FrgA&VT=KXDtW_*5{`2Xl9t8zL@aBt~VnskNCs48+cPLBw>F>@Wn6+LN%%mep1tT%*u=73WWj%kR zLHOg;ih}QY1f9-TXXaBcCfJTeal{I+&a2n zt6`n$2QL>ASMGo-H}9@kE`LeLKg@a?>y`cNdkealh>k62imS1x|8OQi6}|EH%|TMj zGig@N!ji6PW=((|5+(GrtilANYsdVX^7I zTShDH+Pj{+9R$Jim)d_Y8*-CrR2u(n`j=LwO{+1z#dhWG{(sPn#hv|^9A^ezNi43D z-c8fLgq$QF2;~nxv~Wh0BAvd^y@9-JdHF#ElJ~%WeDigqiLmRmFXZ@NylnXRgOocz zIMRWUqj1-`4y1jy_N~|T(?G)qIhTLSjWG4vAw`_bF8?rS`xVns{YSlb&ux>}ntZb% zb${{xGJ4Q!v0W8D{}<2ge^H^e;np~r&ru>}_quPB%iC@K>Cpd=6+LeWp~6#aO*G5@W#GTt$scC{(Z)r~WSAwRaWG_ngn5^;EtX4k_^~PxuIxeM!MH z@7JM(Ig)bFCT)l&`v*v)d&aF^tHFlNR(9(d>~*X~byxACO`cvF@Api;Np&*;+daVK zc7ih%q7b_E@*qDTspsJ^oG;E{1`saqoKQ|nih9TsJcXib|u77I1 zsRq3*$}6r1TsD~77-odLF#~6MO(0my%SPoS1_$C|9c(A!z&906zO80}kPvR;GGLX;k z1B@eD)KTstCm->Ti%6&v6UheO4Vkj1^$x^szT9l-MKT`Nk^uY0z@sjCom&zX#qVu@ z5UTOJi{djTOLyyiITfCZnWxFBDsibd70L1fb}eTBRz2r0M6DU7$eO~V@VS|(MK3es zmfX(D?sm6g?Vf{jyBu9e<{U>#q_kl)gdP<m37?VF`y z*Yt$;mbZ`^2M@_sce>LrTfV-dK~)P?TeX+h8$vEwlrJ(kY_mjjM!CLAeWv!xo2oe6 zs=WsoHdFC&!Kb)pF0qv@__HF1rp&g5C7VT}$zA;~*8)$y)e`*=Xg|7jO2YKA?g4r! zW=C&MK}9WOOTL!}ggKd9F8Ear37h>mLf|@_o{iNlrxXwupAqtkYz{xl?B_;_zjaL7 zlz$B#s@O{w{LefR_FTH&*`|=RG!0O3U2;zMkL}!5XCu>YYOti}nimyM!&R(Qby;a$ zOp%@Qw7Ry7?`fHFF#{0jm5<7^F1_9mRpbpC^}meH-7xO;BDxsWyfqIMZP+XOaStFU z6y8&C<8F@Wx-D2hSB?X=Dwr72u#P z708pL-oPox=^f|^$1q9pBNDj1f~{rdE}|!Y<;=)4QZhu3e%KMjLD%q5CutS1X{e-O z>*&eYG~h5X5)sME7*xmh>0n1Ny?XSuI-N;FqvTaRy^K~tWweG)IeRoaaT-=MJ9ctG zLD(l{jY_(j)R*=5fVq3XSG~rj!n>&7Ex(_aau4?(uzph6o_Od>VzD0;xu33I6I^pt zroH>;FXk2nS6+NzOk8{995%@LYLJzY4f|12Ba@Ow@$*I;rf5UX;bP9Pubg4AX-Njv zVX=b`xaCYL5v8!k^c7_;-Qd@P%?-GnpoeS=cY|plL{sn8kYibvcr-)SV+u}wL8)ZE zo>4D#LK*5F8YjnOtk{w-8}cq-F>ayMnfP>tUaGRRhNzD395O6%i5lC5H6*cv)wkv9VRC+F@H+5^!Fb5Q{yltc`9fs4JKvFC|$+05-^>o^Sp^2S(#6(L{fh{D}x% zMoWj7P=}43M2;kv?C|A9*C|P;q2}A(dw}u74sdNUTWg=B601fN_`{3&VEKZu^7)^q zzoLGnt(VXH{{hl}1{ddMz1nQAdw1C4*!5%a9)M!%U`C5DViyj#=Fy84`8u2hSEfst zN+ze?PI^rDH1T8Yk=wA;H@gxE{d%1xtdSa_^TB5UUf<;6f#bUd@kv6WC8-|AQ#(9lNWcnDS z+SbtPioc1m?S14vLn3ZVO)u?}x_~3g=9zYV_b%rP?xDHPlR0H$WxvpA{=6W6wFtPU zaq>n=SU``a8R^0m5WY0iF@Ds}p#*md9{W@SrcEm=p26G=dg4I~UGSEUUI$ZS9!zlryXOXR1-vNKd=*aGP${s|dD$r0)ix#>n5Q8c1ADV|A z80hkFI=~K&C~h)xN%fK1Ym93*<;cP5PUgML@xc%q*g~;4M_8W8*5fv4P0#2eg_$xN zMfXZ#3fxS4GePqg*duM7jU`^M&9gkvSyplqE-T}<;avIq>rL}>&L|R%N z&lekHcu7m_*||Sp$uNYfIu}zdv(%BdX6LJQ6Y2?+-rS_e>sOq+lGurtk*z6G>&cN{ ze0?c%$7R!7aZ(#L@iq+eP{AI>8=Gjg*|2v$G&PI3CLm7Vn9H{%ld+FWDkN?aoIz{k zV%zHabf4L|H*SraN-L+EzAX%oNH%3!)WIf>9rqSI(oaG72Rk|U0DDnd@XVF)QMf6B znWm(mb}`^1D}%Ey-0_qboQ>ze?W}4E*hfozys$P9(kmbQ!fE#A#dcx`Y@XQf()h-> z=6LJ_oomrEe6j5py1lc0R(VC%P+Hi4tg2=p+~|`Mn~hhqoRPQ65w-h#`q{pixpuwq z_7WGBZ|F=S?5MlKht7Amt4{4xPEgfG@K|81{^5Z4HIFW&*~|HkI*lJ*!4NpI=3R!W z4U|=&$dfw-R1}Q4?%z5EreAw4xTF=#WR zW;a!Wr<2DRG(|wAtq`#vV<^M3&8CCGv0Mp|!9jfO!$Mavu0xCr(h|Hs_IBFkwO~InG1m2Km0ZDNtZ~wP^3}V$p3Q5{6$|9 zNKI|8kc<2maY_{!dlMIKyce)#tcTkPBbl#C?C{{s_YCCZDm4!EYjwhkKGTm!V!mjcG`?JF zFBwh&9iLMPVT=nWf8C&wY1eZtt@_YLV1qluF9V`^Gf<2kaDm7>(?Z7E^*bMui6;~l zr`u_(3TG%XJjQpeMaARtVB82-XDV`NPjMk>aX!un2A3^LB+wkG{+d{+zy+72CY$UI zf>wbsx4a%{6UMhW-`AJacIGQ9v&FzYikj$7AWOk@#le9PtAv{A_|NunUSC55-cM5B z^};&gO)iE_g6TjsKO+@K5c{5!6Rvt?x`NQStpEgc;$sc2BhHh|n7J)LGPX0-w>*r3 zvU>uW(u})Ezef*2lE-?Or<&NKPA5cS8*~mi``w?Oiu+b4yEegClAj+MA{e$hFGwS^ z?Ee%jnLQdT>wuhpYU)FI!nlo3P zW-VTKVN1rkH)C!(**NGhIPXm?)mz8P`sGI^MuP2i8q)*iH4dWRx3k}Hf4$=5TjJ{9 z32JvCk!M0@uAx5`7j3=74FyjU<4uKSgK9D=xaV%7NAd|Gin`w#VV$CgqPbK5=*Y(C zaY;CE1zlFbXne|wjVo-+S|*oduUx#~MrP}4d1{iP7oPoi6P|({OP6~~92g7Zje0A) z#a>vBHa0r}hwW;mw#xuUkvR2dO$VlP_O|+-G-c)Qx5Zopq{^Dfhj3z}+T>6356@oo zWVO2muh98_RA%0wVFYeC%&y1#fBv0zPE)?P-(Kfs967E%gzDtTRhm#27#%M;P{tUd zCa`;*n)QsTW(xW5NO{?i^FngKWW>BEGn;R8I=%SmVOlD?u> z^oXL?T=*IRrWpD_xsL~VPm9NqezVB;9UtQCg<{l7Tt&+vFa1S_mx6W>LSnb-_Xb?f~1!9d3DX7rUu+n8erhS?8Fu4f( zPiXH?_BTabj_nni(#<{g5T|qF4?Uc&%Z0%n&K=lO3Alt%us>``%rX=o#CsRgf02P- zr%s?~<9pKs5%M{Q!l1{y`{pW!99h#G*`t#oYNaRJg|O~o9{%K8hpN^gw_@0Yx;ZUy zudv*@*i~$+V3UV*5`kmynyu+kKdrC8!TQ>AJaB;_^|K8YA|LCtho%z0?IAy4<^Lb# z2kabl4~iuxV?`GhRO`uGRsM;$QLOZUPs&-fJ{b@`D61>S%C7Secw?))mm@(}*3gjA zC@$9})S3MKumVTbMWpeckn$fp5l5gXnPjOvr+a8cOrECFK9Km38rS{>VD7id5`qzD zub=*fvtv?7i1yN;QA|ERL`1x1FrOqoP}ZR12%32CDybk+bF0ZFCsuoVm85>rYjCD zuzd`4%Tmki^!#TOTOYy`)u^s3m)<&LQ*E1 z2KKNBc9PJCTnt_3Ral9P&L2O@$!O@Lp`$%GH$oF#_1U5NY5OV3Uw}C8AuFX<(jbl) zDlUBf=%I~Fj84^O|KGB|RrdY`fMo>DDNRaR1?>7u#lpt&y8RW6FR*=`ysQDO@f(p! z&m0f8&mRr);7>N|q;0n)4@jZSh^xx!CgAcxy3<>efbQ-AH))GJq=TRYxfV)c>1Qy0 zao@xbRLyBYx~N=6jgs4e35(Z{WQQHgWzkH;;aNov)&XVRSP-X(+ z$z#UNQ--K8(Nm28>{mOi&^+<1eRj*&mswe`N)4p+9vva*8Boj(y^`ciees_5AG z^MGs44z@}SdXhoJ`mBpkr}#d*icxGpTP1r0#}e+S$NSyD##Mn$YudCAN7Z4)Be|#t zh*!gKd%b4cnRUHP0FrA=@A;YYo+(wQpef52iJDuVpJKDUVr$`f--z`Ki7R#H#j&$Q zW~cS0MpeG;XNehYi%|bqb>~eob(tl$tHr)i2B9{)WeS*#8lqB$_IpZS&)MpyR;H3s za=hos3Tw=&WEO>tN9uLdFnnaBb^2yEV_eo@j65eHz9=nM&1Bnl`l`T-k1z0b=TJpo znp7o?Yb>m?*3rml->~x2T`}MAyi|_Injm#zuC0v6r&fvpbXy_4Jvig?@cgb+6lSR5 zN76~{^0oik=(R|k?NbDv5m&#M;;&K-$-G1=n<<AcQu@j`O=#=Pv-DbM#`(SmK_R9!KFsScG^ShDtkucM`$NvJ*$te zlMLCh%em99jBf&+Dpav3_8!?C-AX!dWUZ)63i0wgf~>R15?&{ZU<&m%6OHQF{plcN zv=wik{&M{#|DOO$V*_kZix{2W^M5|-kx3!&^Bg;3+hu^bDX)LJFKmFkay-RQzSh2H zd+5SBgxhYHAjl@;+W3}PO~Blo+}$|@m2MCJWW;+T-<;`a8JQ{^TwG9*HDTL0Ah&C6 zE5(-Cd`9-j#9%`G{RBp}k9Cb7z0H&1pecm1u2UJr)ZDEqc17K*A~V%F8fFym#$dZS z9iI-9Ivp(YJ)$!`9a^rf7fV^bQ301Rn0xV@c0o=)^vIQe0vK@n>V{Q|xG`U<#ki_( zZe~qYlQcVRa$8Z(z56bJ+TT-*T*_&@Wl|7ctp{_WF?}(iHT(5?-}Lo%Bxx7_uw6gx ze8>?XXA}KGh#qJZG*#Puyt_TAGvM?F20OWlW+Z&d_sBQB?P0f>z(}OPo>{ymrAt@f z9*8-MZQqnj9r%e(`vbnnq>eSE8mjl^ndq0}M?fm(nA72j-S0Ri(}UsP*lZikoQ z_rjO1%RVU`LX@HnqmrJ#F04b$A{0jYv@LVS?Y`@dG&AdGXeVFEIo6x_+bbOAD~Q&W zRfnlP&zES|0J@aJH)L#Vkl;IrgmsN522mD-k#LYwR>6ZEF0o$nrpv5%;}#G+`onG!Y> z;w9?UOzau3#6cku8gZ=Qciu{Ggj*F>Myg~iA=DO@w*mc6q=!J`MfG*1725pMn-Vm( zwvuxW`)i%pH{>NY&4(@g%nGkvotk)^e4?HaM~NoOoY!Ww#q*Q~+j4q0dXv*fin8ys z;xQhf>hBj1QjUoF%SOF_Q+vpn+7~+*8|3G}sUCdnu+e_CdcGeyqUKOjUA`g!lr>re zK28|rVn827%{S9;^H`kI*K;|%@GX*@o>mwJF@|gB$&1_r0!4SKh7M30f+?O#W}3X6 zm^5)U{gw;FVeD`T zeNDj>{W>pE%Gp(CN9gNzEAexWXOe3>!p&leLiQsWhTQ^I+%^jOxn|eMnw|47}uDlqkt#(6G z6wAuN98(WBxO_{JXF9H=+&6BU2dM(&TMvss9=Ov=YR2OeJnL{oiqfnYY zh_pprNIP7HB|ucM;A9|SafZxbUzD}|Q)gx2+%<(E5XZ6toHRK1dcI4q3}uyCtc-;k z^94?`hBlvi>SDP~i?(xz>E*n>ri{9*Aa`6LOkex84Vu(sh-qIz{6^Fmbkbn6q6`W( zeq9Y2X(p(O1RJQ)hkO1a6SzWotCrI3NK>_s{;DlGU#Jbv5PldQ}Sp>;?u;eiLh2BHnTFrMgLVFIrjeaww2&=Vn937P?G+^{p&XugrR-Ac7d?#NhFDq2{$=lJk%=iXf6favFl<3`zzO ze|JFr&UfBAZ{2h6y6>*{&vftZuIlRQJ>9*lE97tS&9j#>=h-?dH8!Nd{DDw>r?B|L z&o;DXR^G|ya{6Y8Mr`?wyB2daj77ZU;;(v?&?^y)=59zxAnjAqLmlmji9TIgj^#+} zl+VxhMO-#`Kd{UfTU1z)Ce;m=dy}_s8!<{TxF;P`D;vEy-$P)CNk9mYVGWs+;E_xW zKa(FlX|Cbe4@EFwZ3PW&uZ2ea>W3}M-r3sOtp|{)7P=Bsh^MZTSl|<2zUSu)A|CnumQz#~nVj2@&R+7;25;RSrP&jgzlMM{Y1Wj~ zRhAVwf?*iU^5F?lt7`58F}-d2fi)7-S=q4NuNLDr4s{!_#lG{wv5~0;nK6TnSOp3} z!a!dWIj(eyee$VkeMmvvFpOE2_w6anS>etn7Ozc);EnuGpIeZ4p|l+}q9GOVg6dY; zL4M#5lNlEuG9wsDxvjie2eR!%dZgFBPxM;c?u$V1x|w^o>nk`o^S}`hE7X#y6{mz} z4#RD>RplHP#P%VAqHEgRDaHr9Q!dc8^U&WfFDz_mW1mZ;Mx45i(-OEAae41P=_Jhd zjmwgS>l8G)gFPE{CB4R`$5eJ+&f#p>N4@NN)w!uL*H!Cr>A$MbYEjpGb+()z6{P07 ziW;=Jd7Ypf`2okYdQtr_dX8s(tO6C^s@GtToAV|t@o_>v|^CWY92 z*cuJ8j%$f{hVgBvDz_5mV!09w?8XR^$Mphf4-A>=2@cA$dfZNZ(DJ{%?`KUE;SWwd|_pe91{tV zUed)A8W}Tr=N>2?OU=$E97wM=jlE>mgWhGko-mAE;@S$St#>L$+L^#}1rMi;pcm4V z(eb^rmJ{u+(ejd%dyg~=Ye#ftz5Fxkw0gKc)E#3X(^Rvasbyu+X%PFX?brk&w81J> zkex)bx<*`AO=)dR9YPSXEWN+d!?+H!0YvjbQh8;*doDB}NM+1m#6@Yf{VvXMj*rPO zt;Us{W@!P8WGs_PjCF{u%qe-rMqb`Xo#$HOZCvT>n}zuw-r5jLSQ|JqiOkUoBruF; zm+2Z#gx5jf0__C>2=&~$5N$~Wi*rEq-JB-PKDmdoJ5*vSj+Z8xgmwjb69(I+PiGG% z9JjMmX+L0Aq7kd*v=d^#0Syz{Zyq9T^BWhf9WtJ53~_M`DfrRQKp35vD(+(mz84Pr zi1EEg+@)$qfC+2Rq{0hlffi^YYxlgckyb5wD5*TQGh_+xt+4{HN#X$Jz?S=heS3i+2|XgmYc&PJ0QXR*Wm5&GS1(;k zLy`g8i|X_i?z0IUE>#+lj-}Pc6<#GaK=R`3fT2A87SY%ZKwl78P0HtJ%-7t{WW+M9 z)(Aq2&irOL!32UhKzItjY8MhZ9H@;tqFgDEL$vt44JDJnDTi0U+4<35e@j9{z3SZ& z2O$JZ5LF}mEs#)vhx(x%t-w>KOWI7bV=%DAPfVy^=NXr7?Obsof<#+1vH5%R5$y!* zwGBOiad3CG%m+@L2aAYC8;5-e=VQ@`Iie?wonr9uHr^m(^z^Yi<8htzmRwe^8~t^? z8^*@vG<99_!r;!c1B^rVrbomsFRYSO`@@pH^OrWHQ%nG4W|{0&!mFFbUsxT}AnIWs zG>sXun~;0)8f}!hu&KU5Eh=MU+Ito!_89kF#0W(=*&P?Y6`>ZYkWF1(T1K7y{ANgF zIhP{b9L;!K|@Vi!U+G` zqZ~(7k}_J(dQQHcaK0ALSNWT!ynZR6vT8elZK;~0>Vg8pf|G=0*^eeF7gl{qFFMnI zVr}<{`!$8G&6Pl$#pooC1|XX1s<-wWu69|0GC$<%ZAmFe?#^$dc@T)ST=@9)rsjKz@9+i9R-L@Y5fq`mG?uG`=in* zG~*>uQzoJ1(s4IMx&D_*$L!{r3WBP=Q!9xljshxK-+1lrdXZ3K+yy^jTrwl9KhZ+q zpcE`{aoBDDL3{o(`fEh-zjFC!dcuadGC}#=s!R{=lajIH;wu-Jto9ty9qIqgn1F8e z0+Q}Lt>LG9$2jcZT4m>+=|U~0WRf{|x0hX~Q53h>B00S!**5>hehf^nN#y5A2oVXa zpuwo$;AyvAs>OXlQ4@7^FQUf>?JmA&tel~bbi!8UfcMJ7YSNW??+G;MrM3gURDeaP zkwzC76F^>_&P?Y=u@cRfG9mr3bsjC|Bn80x2Q$}m@5;@@L_HGb;FSADCSHyFsQdNG zd`InhgBg_}BKL4!n#^@MQ#_{J++1n{ItiWXPCP6VMi_y;0+c04Q{6;KYlc=`7Xy)1 zFiNfnU_?)I5($nQsJGIS>E+=s-39;(+}Hs zDlhSrVEPpA7uKkq8)@C#?iEYu64^#t$=vA|vM&diaUfP=Jgi|6rx5dGNLAclBg%VZ zSkf}L&9)$+8_0McxWT}f07MXBV>Yu;op26v@~{~I_ubKvO9t|?!AlBuhup2&PWd(Z z^Tnmm1A;s>loPmpSBy-vxJ)La7PJ~2n-&f2sV)VS6pGCFV{Py;u<~W^n44bHK@?dE zPH6_MAZcZEh|QNl@%QKhBF@BEv{9#=BPVgt{F0EGtlBYk((huTmZ)rF!Q@ST&$-{Vo(6Z9^9;xdN+`I=S8nd1$ z7ujey3?lu;dxQx#XWcmsjV&8juht7=pQuz2j2IZW#tURxjBlHQwTP$SSo7mu+Gg%7 z+tCE+A~Z~#H0SagO(2ocr5#&(OSA9B>wrlQ3mYD?0pmF|AfRqw8n)r*{}2YIk;qdE2T; zOVXfy`BcP}?q`L!B$O=2H)>Riyx~y(Pfpk}1Tgd2bTQ^=`Hl=~pe^dwO2i z?)n}&9~ERQFA#*YJQT>M92c4Vj`1Ldz|+d{02iVXm7zlzd@@xu#HANIUj7BdE1WFy z{bboM18iG@$gt1#BzQWas}J!ACW%Oj=CfxAzmF!ukc|67(U2{(|9GEYyLu93Ov|q& zE5A%h6FsliOE~DWr0@{5+kf(aoce!JMXn_8`pqxl7mu_5W)fx%7A$Wx-kJpypFTmU z)v@xiCwa%v{nozxo$**ci}!-!>R{4Sh#+hSV$fQ_8T2){&Zbi5I=^1 zX31eUc;wfpguQdNbll|Z8a&BJNq+U(`Jz0cn?hz=BeNPj?x@?cEXn}OcQ(x`BO0Vd z3jd&Wq3T)wq-^#|TGjI}SNhKU6I@;hp%5esVa883OVO~*9;l99%%b{vWyb|63UEMS z5LQ^VyprhWFgGaoX1U4s{cfmBMTRT%ErwQ&!HMJk7e2o>&UB>msn`q#9TpDF$~rwz}Mr8Do6Xuck^A!d2>XZV4MtHWof z(>RK(I&VJfDr0VW0Uo*FV=T7`qkHHh@4v*zQ9N?0cNCnMm>l2dC3C~-W8%$=o@jr_ z6y)NIo{TVjrhihi39UdHS~uwEBa6#Wnu1y^_TCQ;@&*h#yJq@a0peEWjWv+1h-TGt zWJ@&MzbJmps{Zp-i&36VH}SpIRvY3Y(HfdrB;tEdv3hjZou=!l;$jkKwol zt1V)VNdn_T&ac%KNSF|j$``Btco}6+rN7p@I6tJ^z!vOdwhhFjpm{lmJ7>T3Rml=K zk@O`xv4$Uk2t?`$9FhoMgDZQ(ec4$%LM*z zpB4cF!8ZLUOF^Xp)`l?XRu&-H_H}&Edu+pbA*~VDPO9;D@;U{*7FFyUbpbC@j>2kPQ$>!h?M z{wQIBAEl@7AJH^WIM+7qkR9I^g2S;}5a1&7Ma670 z6aYo;hRSgnYYsA+yR$9sVK=^-TWO+;Gp;cfn=m$!oCkAi7@JzbCE&$rRXr{O!g_#x z0{`4@S-c3UFFvnAL#^j0#?@U@-fOjuJC$wK>*nTUH@*#EuBCFJ4)clf^C>jsrfuPV z>9kXnH+u-=tPFdWqCBJm5;f7JF_v^uyqpfNgQ&XBUa~(&#AoWx7sVy_uBU^ZhcNRJ ztgbPnvFH)1-^VtRr(uRJKE5nqMjBI@%^q9le0#f5F7zX1VzT}Ve;p|HA&Aysf^KEC zETX5mF2e+3_NE?~EW?@xa4t@qk_U`^qpEJq4E^we+b@ALh42Ffi!0JU!MXlyKqB;< zVpIVT-)onF=ki?l7fbwaknhKn{a^m;C3<-uNO|gY_Kzj!snbSDMZ4@*RS?-f_kILGI?aF*sADKV53(-KzHrJMNzhhx_b*e?_7D zuU3@gbfkfO@n(LhSv=flfiz0${WSiA6SC&#=M6$4-(2je`Hj_3f==HoK&a55X~}kW zV^2M?(DXf9s3V7*+TKI_#ho5{EQARAy57LIeKD}yxZwGQeqW_$)|cYcK?_NWHz>bw zxgoZt^e{W$|In;f?4^g9!RphS3)tHCrQ9!_bsO0rt_9KcBR%&#P9FQ3;XCn>ybX&z zu(S{ve)uIcnE-f>geP1>F*q?Ktog~~Oc3_@?s%}Q8nvexlo4J(rR}hf ztQGdLW8-SXD-h?dC@Rj|zRM=dS1R5F1+{ndF16O_x%OMRp8E56y%=rSSM@I>ip;9} z&R_V-x0#Z9z@v8rFQN~$9^A`gYR6f4s-!|+>dAx>kFb{%;ZISYt@)_-4k$7{h#T@} ztxeal(RTL9W9OKW6*QNkN-ObzAI{KEZNA zxW+9CbM9NN(K?A{DlPY#`H4vs@0i8=X2Wd13W65pr|)i9TG|1+6*v(kpaQmmedgXS zeWna&f8`^4ib^~YYJW_n-Xg$=57ExYr zFuM0;l{mkvN$;D4AWRI93fkDRn%uw4apt>Cvt!kYEV`TtS;okswCV}%S}luUyAqy} zHOWbnygm4XrTNhv{tLb=%_w}( zT^!18A08#rX0}h`dn6^rwnGw*;@;Fo&7~`kQv?T58}1|W3jB6JR;bl6>y8 zrc0Bo;DoN$L~^I%LAx*?J2!(7Xivq;|z4rq(; zZf19@_H&Q)DkB_#tTes(v$9$4ICCV8v+DR)zchB?jPiqetSAfp&}i`^k&5I(O-%7x za)}k8lCZ>mgV==xa z>Hz)$ruT{$Zy~RmnSP-gyD6^t9PgS+%UO_iXe`OKTyoFv1Crmq*5PZR)+3R4lvMeh zc3E)8nf&K!4B2yu*W?=z#v0u&S`%7pfmbXRS>-9h+KOH^#yAp|+_NNtiN|b? zmNT>t&1&rD)sk%a4yqC2Vj`W8c6@;64>z9*w|ACK$nq-Pues?9wpsBVlzE@yvF=WV ze>CMK0?we^=r$r|^C2m+U#0 zv}4|d$IC}oDaH^!nh#c+jszIqVzi#5)y@z>HK(Y!geVxpc<5H{v8beotb$e93?d=7 z@gtUfD5pNTtU&tp#`^M}H=u6q{*zB-3+!Mk0=e$wFnBtwW>OWcFx{`&yAxg8PtZaf z`t#wEV>Jw(2p-q)D32VhZ5?_rn3rqI~CmkpSUR^YYG9o8jXFjjbU})=u^S{*-}Gcc?Dp>`e!>3 z0}EBhNoDEdv=Zo{EgM2tV^40}4#!ihq!e0I3fC8;;0?a-UfaGT6=P^ry6n-aRuhEqpL4Zvi;kp|29j~uO8TatTtchDW9x3|B*B8mbz(|ATuu>X7Tv>? zwc{!}1>;!lME9 zl2y<2#_xJ_ z6sEf#H|4D5=eND9-gZY`3%)@^OHPNPJf*j-om1rUpmY`QHfOx^0d3 z@P>fbIX3^^jowz(CBY9;R7YXgxFhwy5&ecOJu!HXDkKnmCi~)b?$mE0WW{gBOvW8e z5&JaEd}4=NoM2BWd|8Vr+Vb0+7qF+2WMRsEOFDw$1<{C`_#m2Kr&H+smqg+x}-mPx@- ztRbe8sIp!omXP>q^Qxwl@<5(ez*8v=?w78wd|#xComG?;yik~C`cL}__4WKZvUh?)w8?_dgaXpN9dJ8pR_IulLY zL$zO|D<&Pn1RM2ow$KV^blx;{77pLR7{b%SWEcJSB1~@FWMw^C!i=HiAHjcWjJXjx zmUVwXej~-TwOf3vAX|9s^CDaAH+kvQjbxG2zdIwz8TWmUA}$OOD%1Hp&+@ymDuH3S zGVC`**6&^*uXKw4;P_L*O`wro{+}qtbRsSY32FFGG~7c8#&UC<{?jN)1j)ECrBUqo zJIWh&xXb92k>NSY(a_MmQ|YdG!jJBQzV!%d@oHw8Pz@zYn)$1#@-LMYU3lOBtIA53 z=5O7VKPoF4ALbWq3nFVoMI)!*!=b~d za~_wYqeVD&Bf}?4i#WRXe&3U2`WN@+l(ZF>%<~$wMkmM*v7TSjMV*Dk#ZAV(KY*P& z;VD5v;>YywNiT?w>c0@`8CfVXa^g42D~Ds9_}UpLa@wb_xVgB-Qh0q78rj?LXtlFr z<@A>s*z?!b)Ip{#`P*XB6o|e{xuD`=!EB@8e5iDP@S{wRSEG{74}$~Jm=ZHr57o4S zeFBh|yc@6Y87Hf=SJ`WlVQKfA(lGpQmSH(D4nCX3uY!Az_e=p5Gm+_tw+Ep>vZSGp zdW&|M(=8J7r-H3#pz0U~(TD9zeC?Z+9G<{jiEN}%iI9Sg-Ja06c}aGur`)|fhp}F? zNC^xAH5+1+Ubp@_$R1Fb+Sbf|R*+xPAdB*aFvu#!k{)6p7zl>P-vQ*Gg6PT~*tKFGeRn)v3a%UuY*$wLup`COCt zllhi(6d>^w>o%5ZLC1!3*^gkiMn?}%_pdvQg#o-exY(b6qV%by^c13(A7+c)t&0CK zFFg13bU!y_SDMMb^DT5%qw^5~2xKd=Bt6_)p*zu`pw1*`JSvT0!4t0^R-adw(9RQ~ z>EK_|pf%?b*1_m~=BQW_@F_Mjm_o&cn}xd0>3cw?>q*6MD?x+b4hN=mqq=AP$oy(? zlScc{YC#0BotCbRdSwjKBgL=j)R8g(-nZTuC_qQ#Wf>HkN4(tqB01T@QGhWKJ1J`d zW)eKWkF*{f>jS8v+1G|BEeEv;`{y=HO&B;W)TaGtSNymK>w>!zQ~u+gjh35jz07dSSUa; z717t9k&o{03cYv)^cptJQYRB*XWxv>DrDC3$A)-iaK0Lcer~O}Wy7c}x|pYvTdmtUC?k{t4Ol z1nM()%TrZ=R=ctBsRUw{mfAAopF~7*1ht;4L{7hm92M%{f12dpriXH+nZs55;Npw5 z6-Fb*Lwc7-o;r`?>B6dlQ>Y ze0;iC1l(F4{FpnEcR>w66Hkazj9Q<};L%wq{V=|`^0B3&@Pea+DW^^W6fd!=;_Rs8 zWHEw;2X04uIM1a67P?wX$uQO>3IQjPuvjBBTfD-B!Jc9aB9s7ivlY~(hti%{X`27J z{erf|i(e@Z9}jaWND>BeY!c0CGSW^u@f!lJ>Xk|&BfYgrIOEdggC2wWFuOWCHhkF6 zDmJHz&)%7vFb|BM80^(V4tYGOQZ3{Xn~$;|$Ukh_l!(+J{}E`iCFfSFJ^ln)>K~Ut zhMyH&U0*(hgR#DJV|>1$GZsKjx8ywEW>W11$zr(5=;3N>{7aK4NA5Ck*Xp|BTiXcSQ#JildE>?_YM& z6=*k%XwA`R%mre2tmN8EfLkf28DPe%NaemHGA>GDzRoL$zAGFc5iq?d(7^AabVide#@jGA|;BfG4}UOsK>s)X}q?;sNCka zo!im@u2cOKqFSjmmH3ADk+8F(f$-UPgGIjDhp|@zm#CO9Es^k}qSZ%dHpldV>fJZ^ zRm$umO6^K6dIRHr_Pc8zR$Vb%Ca~Go&9Nd#IrC8NkHN}4B14gn!s?p4deFy|*jc#} z^)Abpa|?3un!h`hp5SRzmu4yqSc%;g<~f2#mHu|-ynRyo5#O3kc0dh|4qxEH2$sOzseG=i&o8VLBF)b z!`mYLlxGYjIQKlXJ|8B3Kz*=_;TURZcPfDifP#*ZI0iGT7QQGOg*!)R^?v|aCvP~y zaG8`ls&1cC*sNzvUl1vZZ10{>o4U}#!WSgj!Zg+e>io2n?EPg$z4LfvJp%5I-?xQ#3$D+k}J2J{(6FHsNME576qJ0j?!Y9JNHH3ec7jW{gwQt zNH-}~*H5;MGY|zz#(hzJ`f)9?fwVM>?Y26hfbQ)-Xger$iQH^DKMXHE{;IrW+qPNx z2kpnlwF34nLv{(lpQz}Y`acs-#t%)eO^>wlP2|8uFPC4QvZzW?+=kj@Vh0;@km*dl z`-2OX`hw>lw6b5yBMmz2S-ijD6AJ%!rO{B$-U$8+OUwGZJ}Cz(Q#KaU^$*(V%RW)< zTZ)oP&fNy$v}#a4Q5xZn{UV1QzPjx#%X^wlj4^W`l| zo;tDPPJZyc?!MvK^rPF9U4Wfjs$8mR_y*0R8h9a5X5#(h;NY&FAld?hH^EW%3K|Ao z_su7i+*_5A)sg-=&xuhszIr;hj_wW06X`#PVsIwl0Pk0GL8|g_1j&mzA>Z?q^WpA{OW_`ZHay*8&dj<2xW+$x$QQ~(8do`P`~(qMdyOHlL4m2W!|!r6yNt> z!GYUjCL2fBT<2xUVCPS$rrgKAFOsju^IM13Q6MTckQXF)sA>OaTPFS)!f_ziIcf+NIAtdZ8#fOn zf7p(?t6wa_{fJ`7&4w z)JQ_=x(U&bP@rzY*05i=P`E6k$*+!lM9K0N9$@p(%3-DElet~DxOU{gz1Nh7Y8;%* z_bs|W)5kLx=rJU)Gu4ees>WAoLv(> zQksdLdho#N>CGF9Y4i+O?bmp{uZLU7()s+<`3hO?6hHU+)4ME5&!^_R^({W>E~Y&r z{CWAE>(`-1?=T!u!`$Y*d}*$hK$LPuIv|PeM1n`T4wDU&VyU({E)iwtP1Qaw`V#VA zCCLo;LNWJ`5+DHu0y5Syq|3Z{kbwNDuWbL%r;VGsqIZwqi8d5}GFSidP88>VJ(-rf zqVVha#ozFA7iDg3vdj3>ipAoC6)Bsc=8em63@*Y8k7ua0$X{>0=pVGs+}qJJHZI#D zTph$gmdl2^6?D~fUBBe)vA%G4h{5TB3cvDe3(G@+sxJM}nqCJaW56*cK zy1YJR@<)}$$WSl{Yz*$oxX{$Tq|mNH->-Yts*GisVyIUH(Y z5e>Hi+Ua~uV#I=lBnzIytcO{Mtyr?2ZFu_>RO zi@mLynZHBeYInrjz>yFIrs-T8gB_ zqDSpV^rxYvHdZr$kV*4E{00t>VYbaeCcgKXFoTnfsI%fix4))FfpsD@<%XRxWd?{xs0 zc^DiVPm}JC8go4mf)up5a`L5MeWMPbsAUPlEZW0MY;ousG;ll#S?ou?p%OgRS`XbX zNx(jbPX%X2n}MPk@P4-(r98Z{sIWGVOOowe@!&IgJfOHP1Osill0>j` zi}9m|aUs7fKUz*#0tOcCd1}cq3W`Osfo&#%C!0Ebs`mDoR#5H$ExQyJ3FG7G$t1N# z8;-T6LCadn!un|;orcXZn&|dekT7QZcV_}n56W4OfO?3UxNhy1)>IE|H!c@(i=n|b z|I~W9Z4br6@`NA+Pv)q@`WRP5d_}HkkD|S)fW1XL7Zx!g_1=XKT5w(peki@DolUG#gi__b^0DBo-kHy#==?dS+& zCO>|&L+Ta&6qVBZ2kk@YC*>(JNYFL-<=dZ;4Q0ks&!teACNT}v0&?lT_2b2)H@NVd zO`vylBVp*6FwNIV8){Ve0l+V-Q{d51PNC3384`B2cZxi=ZtGH3@XzHZPfa&6+52`D>cW=f+iKEezOFMf8p87KUW?1q0+`Zu$mCs zD?#mqqLStgrla1dquj5G1zo427RgNQPMwiik1i@Gx08>Jc*@AOuZxRM?E`(<>*Eak z^VJ@AO-Z`-0n9I~zr5AQXd5Uh(XLfqev({Y=P{z2>G{cUV4A$LcV=!C`X)Mbc^$!n zeg_rNe29WP^F^0~j=z8lQnYANsA}OL-Z;qPQESLQXbadZp#9||?gqtK4m?KtXPrY8 zRG8Lpo};z5caw_bw1=Zf?{F|mQyQ&DhKS>fm@6nJFb+SBqF;emPi=xG{ z417bKW&d7tLf@54)0=prP+b2s@ppC18*0P##!vgOKZT{{jDB89YQ)vF6Lfmapm^g^ zR`-AER#v;s-CrQ5wi9q{4f4 zzMj_`$aUq`hkkbf!1PYEmqLT?aAjb2N5^@ed5jZKE!qV(NI7aB<)Ch#a?8`#Rv%8@8p|u`XTh z*7%B?c$575(w)n5t3ll6A`l7PfV4v`wQ< z!gkDH}Xzci3bB}x6&=|da`YYlz?&_Z!ZR)OrB zLn3zz4a&%#bmGJ-L0h4a&4hu8(q4Ae^&yE`!sqMK38wzZ0lS&G58^s2g<>9LVv)#d ztm3&x?GAFY%gZ2afXk9Vmvhs8ealyUebSR@bOJywdwQ~TDXS7%h@wc(YmvKwfRxz% zsAQS~`;~^-vgf5`K(5lV$@i~CI=(>7oI_zC(eMI4Z`zEqlA3)W%D$D@ct>R<1+<*^ z)bV?hi-`F;Jbz@;I>UZ{OwNSKSU34s`2Avk`$r?2PuD*B@yUiTqgBqAyK8e+66=qfGoUaKxMMM#=8mY6SO5gnmf zG;5US+$uAt3!CFo#Yl_}H!F+$&opW z#uMez%Vm%8-ldDTsUxfTg5fRLMV)}1s}OrYZ@@24#8thtM}QAe&K z=rBUG71us!iUU{sTolF(T~rwjjQEll;YKGG#}fnO;Lp}jivEE2ba39ll)pr{Xn!5#)r~)0Z=9% zdxd`GNvCGirGWeXWa6m`v@?nyd;8M0&%ITLt$V`Z>Hta5c-qnWb+#doV8jGY)m?6b zu&CEf?uS?%fQ=JJpFPiUAB;s~vKQ#&ewfr^y#|~4L5-|fz6bD=#8v9`qrxYh@l&hxcM{@#&5gKDTA+C)MKB*F4=q87 z{1ASk2knU0OO{8bz~ZtG1GFKtOsH^$xp}@!5?)(_8)5|3rwGaW*b!)!Auam}p5|3y z#&*w3;KF?m4-@XGp~KKK(FTH+d_39YKG?Ar+m}2pEr?aZKoD8Um@uZG2YMA1rS!ojVxQ>u{K`?5aq@CJqIQI(5DM#~@&%pw>B z2ozBYVG2xPr|1yhLlk=O634* ziO4z@6Ec4gzxAp_;yv}K(iw0oh`iVLK++H${kw~6rcGqEiD%;oKa0Npn6{9HiZwL7 z{g-i#&oQ0y8fTr35F)1}x5Xon*9f{AG!#XdQ4St1)4TV~F*$lNIXc5bSX9|)#|XL> zhTH3%zU=>#BxE^=dg^cmLH+~pz=N}~p|#-Yj_`dN0a`=_6H*@Mi%%w}|2HRL!r<|3 zKj+WRkjh4CcQ`n%uvyF&Dx^mt2avZbnoyp3X;YZl8u6>|MTn$s%ZpdONF3Jvy@If? zIX!ke8(VvodbBmZ4@E58Qv8XK;`n{1$R#8d$F`x--g9i_kz@EXLbrs4{fX1TTzFfu zXyd91Pg2gNc$1B|{_t)z<;+mNWk=TOx7VHjppgz*UF*Tyj9g&_yI)&q7uT+sI6dN` z*WBlKs-~7gaLgo}MHL=qBgl8`zmd-cjCPrtDyec@)F-G+5sSGfw@sUcjJ27%0b>`0 zK2_;Kas|68J;-!cWX1injXo(+LRYY`iIsnFRcK1o#9MbZ5zF*MR7X?UioS4CR(Zgq z3!6fP`_&bWe4$(Kp8WaW|L{7{_xeBjq^R_hsGZ4v)u?ytM#pWD+<3>8BN6_*dPO{YXhF0+-9^Nzk7TcFQh>;}v!ZSA@wwJ1g+cTNJG)jGQ%kVae) zUv=bbtOa_S9vd5UYkO8*2}9#aM_3PytdZmsikudkgtNkJxH>W|(q~f7-%%H!lO%-l z5|(?NsrIIz{MQ2*9e<-bhRz5-wNDx;C$&+xAP_+tM5ugr1C(*ot2Nt(lC%<|?t?xs z`f9Ogp?w~Pq6?O>=*6AkDjuOc>n&AA(mLKwIko=O9ZMKx5b>7iD>Vsv*Awm(k71U5 z)p7B6^#Y_vPC_kssZqwZC8dovoL}YXiq`tyh8BKxgMoIpprwX%5lj(c>V0tIl*)gn znRj+2?4v`@%+hnEhXj++`d*&+KZ~DW( zLHiOLIUdbZnKBV@>>93o*-C#VCeCREEuOssstl*+jyfgVWU2XYr*4AI;uXja4sO_c z3;rAqbcD~DXp)}Rk=Bzabi^bxH#LsNQ)G`AyNHzC&P5-BV2w8|IktPt7LQRb>F+*p z`-1%4MV|eA)AWRt7H-@v(*H3~qyN{AS!QmudJ*vAMyR9v+W4tl^6Nius>~BAmOEt7 zg_PA|jt3UznV$H?Us2a)`_IUZPH9VxK$t)|6B9`0j9HMdc3o7{`e!t>6U6rgwxR0! zNx!YflDy@2A9rS&&w{R8{E|yD{O0~a+q*9L_-hzh++(Q9s4+nMlm})}C|ti34i~5P zyL@Q!hRaG26_VqAt=H1@jt{lc8j-okJ-JHff6!{r3jaa7lCnF(+?hfJ(saw^>%E?P z$YKrn#t1kRSHMiG7X%>m}N zB#yp%1xjoEh{)t1J(knPV=hXHzq$E=5!#1m-)M=U)FrR9UtYhf_wl+~=aMUGBU6XL z?r_FYsM6bF#*1Wr!KM|_?^59|*yihodD`;4>1mfQ3rB!!-Ti_i4_T;;x&F#J&sn7^0L#@<68=q`{+p6 zRxO?){O|DCzsls5Pnb$WUAF#<>F;spX7XmY59&#Ec@NUn6*3fKGney%8Q%1m8jZRk z^&nO+l^cyQIn^wf${s|I<3*rTJ^Dh$5P`w0wIFoM{M&h`Fj}qmn9v0}ka~`fYsMl= zi!BIY#7{utZZ0@1XvOh+N$`^p>~g=TN?zZdbt_&ut|zQDg`YP|Dc_{ zm`q@@hl!94PB}7aPM+9Nv(H5#-qiN88O_W0_%w67thguwE_kfd%{A3Yhb*Ir1;tdN zxm<=du_Gj3_d`bIDzNQr9-2EC*#5^E>hFvGzrUoCDPb{s_ahMQ3Y8HX<&4d=^^T-f zT3v40O{x!E-Pwy%Hzm`ST)bZ`;IKC>>dS1!O=^Lt}G>583D0=5Jg9 zKm#k5k#6)$ zra70Rop4!S*DvHGBVcVZgJy00RI8dXIfk5w7FF|a+-MeWs4yDgqA4%lSV@{_Dwyi4 zI+iBG6}PtS3;0!4oyRVNqjQq5umumlNH2G$n!M=LQc$uhzV9(kn#7Y-SbFE4NH^F3 zZEVaPhF%%66~|dLXy@pO$g3U}_lzAI*VU(L^!Q}cZOZZXp@qAeyg&~CXyrNgKwn8r_=tTZ{7eFPwU$7KNVCc2?s>#PlXol-jh{4Ggl$jOs&qF2_K~ z0Pl48!-!B0F6s0K5gJZMugccg;u*DxnF&&_`mXXs-tM>2Xf^@C9CB!80`t~_e0q&T z8uYwM7tq=Zl2lckcfC}Oud>GCg30w|4G9ExGwhIfiT0I8`HD*OYNGOHO5;3y9$xjh z-UIEfdQjNx_9bZSRHyRgExoLnNlL$!?my?$dNn!Tp%l67xZRn=SQ#*zKza!gx{)B2@Jwn@Jjy1oQ0upUPFv>aad z<#18mxE*h1?cRJ?_`C6bICsJxxo&>{)cp}maJa-#*N8rKV*I(a6}$PEB8D%swU;~{ z%oHJHUJr|Gi^3w&`uOfwIG)~n^=-+PkuVl5vI&v{sqggSR?s9x2%70fE|En3gtA9_R?#{yQ(DzZ-{s z%~t-SeFVR!K3l(^eN0SK7v913W6}Qa*MmykB^zyRv14_1EuE_&Ql|o#ripuFDR}@Vr_CIf^&<#lMT#l%9Jzz;kdxX^cj54Sp5D zGjLCy@P?;hvcmly$h`lnNYbyCu*fA|FzNg%BK5l^dQu-V6&n9fZS2`Ei!Ld07UYGQ z`y)kS11?R?Y_-@Zr!-&XroUwXFV_}K{Cdb|{`HWbW26fH0^6#fz(4WhL zB{b`WppznO{nu+PAT5?x1QPpEU-=8sSuFo)83Fxma-$YT*-+99xN#h1mmr4N*k5Cs z#%KJomoLW1Kx0d0?e%uIW9`L?wyy6H zEehb;QgY_>xm@?29}#1|!p5)&QVx;#4Y%O)5BmYY7|z9$R8#r3Bu%f#@BHK-1*NV4 zh>QJDmPV(v)we=>KeE$`=Hs;5G>8$#R#H15t*OV%+y-V(aEpK;A}Y$$S$nRF;&pYj9;$#Ma}Ep2^W_qY|Tp>A(+DOfInA7u+f<4bpqlWXT%D zXTlyGi;9$jf!JH2aTH&5G;bHyu#iDtlP7vf+um+^Ahbbs>G1?RI{uu+EZv%~)9h-( zvKV^FH%YEdMgyj8A*b zBxuF9C}UJ?DQH8r!+le62>;+2{<=;5*@sxHbF*)Xq9&_O0=?r}?EY!qD>wVE+*8!+ z)-);V;SNo&+CqK9*}SY++50kc@^Gmg6u-5uuQ^P?uJMyv-W!rT@0O=pFODOz(=@!b zZzotk6QRo$UH1f!Ftrr>!d6HrMe?qoa7X$HJ7J90pT$~v#5|P`;XFdssy-V{@$X{! zK9)Xb$@6u~A1vDH^`v!v7gPB>`elp1#-W9KSlRDlsi&Fe0}j@3f+79hniYZJ z?_$Eg+chYL+%eAF^1GP(?{+2lpaWue8|1MjV0zgo5bf*F+~Rmgi( zQT=;0T%?Ik)&F$&DI~*da*EEBllZBmFGU>x+Yw=_H}}W#TQO?=+j|08tku14%WcWE zSIDtc|Nq*mlzQ@Wt_=6`m;Joy+WuNut$ddIb0zUUi=m75(~Z91$FO!dbyN*?mZ6WR zS@Jb0xGF}i4J*;o1DL5{yk^r+I!-U%)Ye;FONf@uln=8W4+;CZ=K3$buo}_QBK2KW zW7Pqv`l1o&R*_}m9sxFPyFS9`egKjbnTk$mktA-1X-U5!j=?~|fST$@Ft~sU^B?th zn`Yi;2SiXPG!-)S_xsbl_G4juTFXqT@H!-X4>>d^V4ddX4TxF-%_;tYc`SBl)LtMt z?$0-8g-GVb_y@kYl<=KzccZ^5z1Cz&(ooRNDh%U5%EE%@oXDH=NI^M$)8ikX|!F~je?{Pphg=HYajFT?31!dPSOGge9wXZ1h) zPk$`2eEq`L1fG=|L|DWz+cYhH`Ysi6`A+H=okU>p1MtWHU9J{h`vcO#1toAd%XDl^ zR&_ufzlD6~@R}xW$kIyHViG|`EJr4P>)~OqfBG^)=u_kOIR3;dB-mrm*Ds8O6B{EdV@s3BYSJy)>{6oV zY%H(s+O~aO%8`(?2@D+5{FeAX#DZgGKY`ON#b-}Ny zIvo1SAhgsT`_pe<<%zFRI-P{jZ_F*Vs~7fUC* zPK;c#u`+48R+z`k&u!Ld*Q#{Iuf7_P9U$Gy>Q}jz+kWDf1^z@)Q<8nf%@J}f-7vA- zlzC0iFea?UW;#WHLwV3BAa^^{<(;8u7hva|AuzV|I;X6;l0~Z{K?7g&n1g=E`x1bL z7>seU&8Z+mMp=jBBlC5j@3!%p4zhc4bG2X{C>c!4AoD6q%%?2H7CGI5J$0Nfu&yQQ zX(e}_&#jRveQ? zi6AXdZq-SvkZoa4w6Nf+l%mX@nJs$ydi&F!UV){7W7s3jTJ1qHebQBZ!1${EGBIt` zJ*~*1dBS#HJAj>%N-hsKN#PczbKMjvqDS?~iN}?{ES9wlm9@P);n*UZ?>*f@GK2ffNSUXx2vdGM^|%6Hpl9ia=8j~{f_${M z55pYaOQ8P88`WdqyMHn{(9e2|KLDo3&wl`Vw2p+Q@4+*&48Yy4nQ-)iFz#fx<<7a- zZ3VQ+{)6f(G1jifo~--)v9H)e%O3HP?K{UA%X2uEGu-iIs>rL+p!q12w>CI`svPvm zWnql)1TNPm>$etn&XH~G({SM{ZrzkS;a=6+)UyyQ2_uU<-m8jy-2{2feQT88r35x1 zoP%G<$0*Zcr?;IW-A85Hu~&RS{hss8Ao6);Yhl-;w?j!nGsml}F}YDD;V3{qLAUrF zcRI821($id^oD*FlRA0CwV)5fF3*njb`r`zc1*0Fh(L4%@Atsc_+N!x7`N?xkFijt z@R)2eh^72~UJGB=g(pSw+q&?$49Erx2$(ZqNcUQ(?sr!Ah`h2|G4Q^0>MRxL7C-L1 zib8P++E}2e5&YbB7YA?*lxe;onqBF zf9Q1yJ_SdL`pj6o?bxt`Yj@6C*)NTRs8?lEP*op3-*r3H?D-J@V~149IQ zo}ONb*SCJsb(5QSz$d(2@=2Z$9nK0(-bsw>BC>XNMC}dyWC8V+M0?vnNnL(TQBYfD ztgBXSUSbQCmZ~rhmrspNi0*I?R*A)?5J4p-)-gSjl+?Mu)b3IWduAv4q|oxAl`Gjo zvAor!>qC7k52s$e8wlQm?U-{iS!bae->liD?inlL4UH<7^9gGnFLKS9icePHWguZN zMw(E?RQ&dwcl@EK|E)_&`QdFr0Xshv`{OZmDJN?VH$~Tm&9k)_$RbC%G3SRIlfc}= zao-0$_&8~~F+mZoaaxfJwNHBjkXt3HYjSuQx!_Gu@d;Eb)+<4-L7na5UqlhfRPf;(kEq-K8kcMr zO}3IyPeBl7k{5qEM5o?oRuC)OFjg-ljyBAI$wo}lJFcS&uf~31Hj%b7%Hj|_$?Y~C zxo@73fqRWpsuqfc*51bu%-;Pp{G~0vxd|p-1sz8|rq7vZMGt&*iSggapgdS}^{%~o zbl4K=BvQbp`Al0<3tz}0C_PK&P(>h|3f2Q2%8ugQm03)=L~e3h?iP3^gNJ^HFu(1% zhY8dRR5_k%HmtW3SHJi90l0Ag zcD#W710eVVFy6A1IG4nh5|*JZucfA@esU9*RH#(>X|0Wj($RoYI=S!msf%uNR5g3E zN_zMDT7MKp`r7-owb1&Y2o61*oA*BeZ+!=HG#%KHgcQrp<6T`h@SA+?6esZFydenpx zLoCj`Lt4@r5SS%YPgjx_AImfObh=f>wOUw6btLB-dR$3UYeK$F7hGehT&-7>#qdxF zA#_nsJjB(tjCo0h9V8)ab4*bE*u4lM8^X-58Z-MUU*KFw{)-4Zle|NYF=wllbPYkm zf|a%v~7Y%9?uB?J5pM8MO9sCi9QCPk(E z%zZhJC&yboa+gj`%xnw>15)!TQlfJSZ$t>*T@q;Vj1J6j6j_0r>D-T8&FqRb)IenrE@Hjsd)H<%=)fW3Ux^OfVuf{jI*W#?)9GmuR z^*-e%lzS@JaqWzCUqz}|HWl*>q51V0?DzhJ zUpZpOv~-f$he5l{Msx)6ygl;Sg}X;7wK{}vX!Trn=vJ`=R-2Z>cw2T{beP{4nFo2& zVw&D@`c6@Kd;$K=s9%nx)`>HezMs9dmU7D`x@XalCEUF~o zbVRIGakODl|3$F%(dc3^@95-vssQbmp}8@8p7sJTZ1(n5uE9_RwhJc1*=(QKhtr%y z0%;k9jY1(dR@8;L9=j&?Eia1Xv3D*Y$&RO|Bx==6W-wl(xMOPB%ak{cK_ z#(EW;=IK(~q`*x=vf6?q@pR3icF{b{`sRV&hnL@yw?a$VzM!F#1wtmA;yfK|$0qwb zI}->S7U5YbMJ-^4Vf0}WqiT~HvZ|tVxD!1Rrm7KwP}&3pV&Xl$Bex>-99y%2urz3d zpk)&0H-@n^g*~b=WK25j$P*0-yzD5aj=oMs-4qxxkZgCeM%k)PReE$gBR4i$p!w;p zo^C;`{6vHWUCNEtqy(tt!i&K|Dt|&)(u^!K6hMryC=lVeF}1XRwKktfx)B2MF6Z_Ziz!WjSkd>pH( z$BX_OLWPoiZc16?mu&QpEYkGx9gMbo!`nVCu~3y|*6O`{b9rfeJR>MvZ(`29j)Lp- z1mMepVBk`%Ha%suBr8fT0vcnZ)$dJlMuyP&+_ah%DY}4WzQA#~yK3Im+dbo3TfQN^ z*0(189*GF+^Tb5F49tmm9J1r@$;!-^nbj$0*Fv-ATokyFz=w^lAwnd0xJrsxkCWze zuNqXk{BhtG&+7+(^lFWU@Eo0wA;~{(oOd!hXxVx9@Q-x-IX!$L~8U zzYT&i%h9`RmwHl!#|A!Jd)r7@fq(~1}&J9OiWjkn24FsFPFzxLLn3}W(lUvHa?XQZ7uw-V{uz1V| zPe^Ufw=*%V73KjmvH8^0Dj26L(1%E8)wY#Zom;2V^7;;0{H`-wAJ&r9R2#eIkslT@ z&zSEcgTmWm^}N^0yR*vA3G2QfvV#?SVz@2=n9RA5T;y51a_|Rh-iHFhR48CN3<|^~ zbx_Ij$GQU$2|rLAW+dKh=M%HS!dQ4jm_#_kE2cqME0}c&( zGJ1vp&$%&@aT)lCaAwRVU(g&|eYSk`Z#(~kNPf7>MVDcN`7B#`{o}O!=YfjKf5mM? z!wQ#y`eXU9n6WE(qFe8IU)yQGMKPW{yM-m2t1GVyKba<6Z8e+21|;V0RPlLo2B*sj z38xRDI8qO)^*^<0oX}}qQJD`j!FVXOBZ5M??A(Vk=0{{`?6K7?1N*KzUt^`BXzd1` zAH`YoPPXwg3i?AJJ!gM zcIvisI&lAH)8WKdI7Om*_G-$GseRfuqEf=KlHEM$SpIq2w^}p zVhFAOl(JRR9P}NJ_V616ReW~Su_+=M1KQMrW&spOx^$$dRM)1vHrc1c!6_RRtaht} zpi`5Y;q8wt)f3ZlCP*@L@mo+E=ShUP2U7%PmNfIs$?^KyMTv5Ui>kc1h)5)-dP~Ip z+bSOMb3f~7d>wa{n=9Zol+q-Ywr1(CCt(3Ca2ftE?3naObUvNfp3})WAWho7u*PVq zdR)IwyXbu;DPwj*mW(~AXA{C(Yf%f4diFt=AAmdV4E`gs=Ht1|-R9kVA+Jcdo-iau zDoq`>S#yhIr@Egi36#YNh@V=31(GB4GJ1LhJ?br7T*^U+yI}Fozw#|Mq25UR_CN{B zolk78sy7?*@4pIYI%Hw1f>}xvJX-w>AD-n_rJ50#Dn2gBDhV}reWw+}I$Rl6M_C>56=fQKl|zE4JoVnGLuQsqr#1DZEbErQhVW#o95Ppf}G&nf=N`MH%QB;@3`$~m0GNG_y(Z0@WH(!uf zLlfcIM6^s|1r!nKo`6q-qO(u#f~;B<8B+}=_I#LfVQy`uG3Ivt)MLps+Li7#!DgW9 z*mT*UwDe#@jW@`qd5PV&q;h)rN}n0?iRlSfscokY>z*Lun*)Oy6z3)zN|%IF18~cv zK9dkql}_LI_QZ~G?XJy}jb4%SZdTFZ!`iu>q}IkQuULi0_QnkgbW0i(;A-CNbd0!I z@DS+DkS=WB&BCgdd~C%&k#HqDK7LGlveCR{9dkF9(?l`C z;&N~wN1Omho^9gAK|!^v>GhvI5ui)YBk1OK7PHh6Dx?GvV5PYhxUbi z2`1JWq`Y;z{#)X#9G_Do31gW~A=^d!VP2i$GfWZcuRVy8DJVz5c!9GujS&1))91h# z9P+>8b^q$?|8>mlp`XkH8ccKQNex%nEn%08Q>Wqn#MLACA6rP!bDJfvS6%SG?uv(l znBHZov&rzkS{f4LGFO;xO=h{fx`ijayX<#IFAkl<=)hJoP z;j7WbrlFVh`i(qO2Dm6g^>5A2tdLf{bh>I|D_c<+Z5(vkb#=9D`27t$lKf*bGwZ|N zJ(a2R(1))aWJ8#{^pqx!3+hFq8mHClFv!HU;2E{@3+vERMW!eSq*?U8+UL77UD7>2tWwCkJNrN>^+5@9D8lf#InXy3WYdlvFvwg>! zuDI$#E36+vPrIRG@@XxCCrPq6=S9L?HTg6pGQM) zN85<*x+8A5FBq-~_77;AX7y_=wVQ-j51~Z~QF}$=>LVQlM=F;!|1>hvzWf(Re*%S- zz-L|?aCg?;{GY6ooGwU8`X8@+{ksrx*-txqNAXvU?usqD(Y;9JJ?r6w_FZ$J({GC)kF={unJ3x-VKUuTBy{eWybk&-uL7He8|T87{)l{^2uE~@^Ji+R8NFPfC!j^472^-y-?yhJy0ZHv_juH z&BldhI8Of1*`RRN@}#o8>xJ`bqp9}z+kfhQ$m(O?v}E^mKR>N4(i1N#_@*e1s?#7m zJ@tsT|=m$Xbd(zdVgTk3_+Uw-iFRjOAuzDX?jtci3wY;^{$=2?bRG;9B{clU(y}$8N z=BpkZpRd63wJ*vTX1}yzCm-E>PY&)?za@I*GU51n&*HpLCf9?wZ4J}{zP3X1z>$D#LWudxUPqE&i6Q33L znTucjCGS^vc0I$MD7$w5a(prItM~KlEyr)(Qu4Q-`>q(?b#@1MXD?CjV1i}6#g&3f zzF)2y`&j(GRIbpao!Q-zm_NVYmeoRJurc_A!XtoqQ3`w&-UIil<1+sMI9$LwHXJ|4 zeZ#Y{{^zBF^Pb_Vzk$4-J(Kt|07kpxca$X@^J+EvKKcW2%?_7{; z`_|a&ZHym)V48~y>@URP?(A{jN>oSWLO#Cu{Ey%h`^V-hbYI?@ME7O$QfR)JB1oq1 z!tYE{k6WH0c%k3$d+;;0d3IcbM+NY#;OsG5WtNcm*-v2c;9wRAf+ek-Aj{i}l!yC3 zp!#YIL9zjUB15gr5~xP#6j1hA3L&e9z= z=9Cbo*9X0f$_b~DUqBBhfq1%ca@GZh2jJ@~s9>lyGtt!VYqh@<<(ee0%WiY~+ObA~ z)+SUDf!D493Jsn65E|JfCH0#Y`Z=h+YMOn08(;MU@WogBD_>b(_~kM@m~XKO<-3t8 zd_*i>|6ViOdD|KVgB$*ziBF^S$aM98VTgV3#o<=+_2K}>*Y@<2O(S)dt&fM8KLE~* zAJ6Z&ELSTg`& zTO#kMME&|c&sla9J15s{ehY-PSGlIEM(CWSn{u^Nhg1f1u~p-DP&&B3dYx%QBChbsq%yn>3thc5CGjk!Ik<~`ap=j&+USpCc|CUKbPMUHrTWDOz+D7fuQvY(&Xc$rAb{Y`;(g0Roqb& zERPEg^}*Mg~x7k{Fu+kNwjB<$msdauI! z_O|N1;!&G80g7zj4DQh{kaaqn`urgZ2HbD4h!^PpopMxCxu`yKMf6A z`R}E?yEyNA-(1V@E><|Pm^c9kd9A;Oebhe?|2A0udh6l`;AFm)W8WY`ylMA)`h3^5 z);~@g;=g>JyZl$?%RZgoGB&)gnBza0q>vc)3cN|Bl?q^}3Ps#8QhoMx1u^`@Bc`i3 zXMOeadj{LjHPbi zI;=%}ySxWjW2BRvePQ(GF~8gI$b;g6pJT-x9-}i7nO1l=kF5Op5F`;cJ@SVhiLKsw zRwWUIlwi6FJR3b~eMmSP@$+an)n=uZX{$svlgY#O{{M})HeSEX(%3}2_EjotH%9%d+jyC6tBp;Xy@ zWa^BC&CzjCj&`)T{vM|%^HEMd!z0wt?|{;vGHIgsR!rY%I0^_K#bU$BTBHJY!y*o< zaX1h9?OAr#&$oEw!CC1I7r9rD#EbBj&%Y-D}xrDk`dX{G0m`4g-ySmYl_oNrow2ZHNBR@;;O0&GQo!nb2~HyR3K z^%n6QHSR6^7(c&|OSqED10@Uc{8^>r$nPru&o8JFZ}jVQQ~v;v;TXu8+=JG&R?XI_ z<%`9-pN7mOtiG4Roczn=&JflF^Yjgd;{mw#t3omG$YcTdS*V`x2`ActSiF^d7}Bq2 zU1aU@Js&PtfkfePl&>dH<_RA@%Ae|3YfJQumFp?mL3gjuUWT(Xb;CK@{|7g168QSb z-XBTZ=6J@YLcoI!crYOLVv078c)8delTav3zKW= z5W-a+YqL^V%{(Flhxc^AjLp|t3aa-22MDtA$%D7|U?*E_@Pti`O3OyGcsWB`-`y)pr1PoG{tk<*X;lIuGvGf}>arh#E7LYISk?b3&_z`bI=*PVJ&M5qsB_Kr@v0*js zPoUCJn+W<#^z7twljy@i^s^n;B1KV)+b7eq)xwWG=M^N(BZqRTD~{u%rn#swoK>RH zfTxMbH*x5YyxG)VSL>GQ(qqfaum z6TtoOyTFZX21d$WZ-;Qmij5n;#hkQ9 z99%8n5Sm%!i;*8-TpZDlV|-E1`%%y$SGOQ5XzTlkLI1Axyn(7#HCd=07Me|dffdQ# zs?O}z5tp>aqIH}xODPD(xctTH=xMS%L{{N?4`)wQm5o*hXKqTnuD)yhnrALDK4)G- zS>q*?cT67{E9=@Ort{KVg_zqdohXsO`N?)|fI0P%U~Mv`K}AhX1&IBc5M*Pg{>cm? z(;S7FaXKmR_46hg%FGXdjf@O@Rjk&-v>7?88!bj2$%J%^K>eYNwWXc?9d5F(?{`q!(6Q4{!lWhd`A+~cfB}N3K z+?&XC=_C2Yk#ZM!o>{f4dxB!84%E}T!uQ(VtrYZ?AJC3JlmUsK?9P8i#S?vu%5!?q zt9MdsQU7fkGrMBZ?V~L-$K*6H*_9~|=e{t2cUUM?piQ(HuA0WT>Vw^gR6ENz(}>te zK69gRJ4jQb&t%3Wt#WpzKlAY!AX>n21j_`etlZ8Hv)8UE-*&p)j@RX69##@jzxXoW zGb&#-l^qFUH;Dq5qBca`-~11`+=A-rJ)sbG6cIras8-`OEwrra;Oocn!nOH*GJc+! zKC9|Rx%#zO8ADe`f#Fg?!C^V2QNgI)Lw=`M2Tjz9m2q0qrt`|*L=9@j8)bMu*|_xa z;(9hTtH_NX2?!XG&L!(#UX%$_n#?2Cc3hp4&%YO~j_Cl35H$(OVB|M!cbqaIs=UoZCDrJ*^n(E$(tOQ}^tUuWzL`iuc_?=vAH^NHHOLgW4nr;RKxW zIoIf@P{J&|X-k>++T1}{R+2mx{`8w0fCVZ}LNThP)EUD+BDC z%CFg)y2!{?C7?l-2tNSJ7Pj0r_Ck#z7Y|ztzWEgwE|5d!h2w8?r)RCq{2gwoER=j= z8UnwPhkUhNMVxK+!rI``UOtFiXmah%?lGXN1WEJh!RR7CIloP+H*M19NrO{O^^}_K z_v3c8v^uy6bgC<6S%Uc4S>5ZC^Tsu2iybB}k5>evYl{|n{0zy1>_1}(#F6J?X?$ZS zteOHH*2#41JX7M*9PXc|po>EkuB}7JNC8uAM6+Zy=(5cS4KNq&nGcELT}O-5M`HB} zu6mpS&--dS@YX%QhrXb}b@tZThX?Dnx=_Y{JZ zjntlh-Y96O4{EmJ>N@b-j){A-z8b+;Fdz2$NPA!*5N{XS6P8!(PX6TGr|B992{ExF zCF4oMxq*f)U(Y~4GK*poER9#N%iQDxH(L9M@t5)RkJ+GBz#g(6L@k2UkkHlS1F5GsMKiiq)PxE-qq61g(ciwTi!i zgG=+nZf;I2N>Hj^I0b4S8@rACfyliM%=l_aMK$HI%~-Sz9KkqMIzLWeP@M)Og@TrN zy-d$ev$kNj=R3J z;FfRN;IeD*$hMKjYJ<)9t}%n7;f8LDaOPdtQImy(aDcfUX2SxbDoiA$NieS&9zpO^ zSRFsYLuY@`?CNr+t*p?GP!I_9iP*Pvb4gE^zj+9cJu_<;pz1{nOhj4@06roNVPPs_ zF0#suFH3u0t<*jg*hwqSsO|ALC6bsMy>|I0E752sx2?&cV4L7i5)XQj-~@ib_%hq2 zJ9F3r>w>M~e3Asd&E2h(G~IOp4oUwzDO*PsRIU#HQ5kH4qN2*A1O<;hdRvErIM;{b ztp|8f83ZQ&n4tH!c+Qj-^R*``4>TV=Ke1lyox95078|LtTTQ{ukufg%6n3esv=_G0$MZ4)WMF6N>gu*9zn9(f!8EYcv6r3B!as&bNw8?J6K3t!tgO8YLIygI z*Bnh`5T|x$e-75sJA+Q9p3S2z7`a0;lUp>ML?qN2d?1~dg1sNcRr}Y#23s8?cJ(9p zQy2I(+}*eQgLwL&Gh9LFDM`vqx*@bbz!K`e~^-k+DUL)hsTmd1rf|A<#L;k^B z$(`(iyn?(}K7_5$7k8vigd!qsf`-2MHlO`0Uv+F-hI*tZ85OZ&kki%t?SF)8&CEUR9O`EH$$-asf<* z2oMg2b3k4RVIcaV*jQ^l8#|!p#P{h(Ped5c4EH4PJsm5A3eX{rpdbckFwY1pH}26AF_T(XkPPyV@xtNj=-hW-0tpP-=G>f? zB-Fo9mlT$AE0e3rx@=X}SEG?mw&jy=glVnAztBf8;BbMuoraNT&(!DFo$#><+X*hYmkhz%>E5qsJ@wHf)H)&4s z>5Y)LFP%#)u$8c5v^Q;>E1=K$K&fAauhk~#)ASN`c3DZ;{V_HhWRvUM*oyWBpa6!j zkSrFuP_e$s+)c~yguV@Zaa})E#1x7{2w?w3kDgG71BNhqBhN`v%rm%oiA`{=NCV{i z?L9ZBfL;Vb=xn7+B5}_^g-p58#>YW~<@Ii+Sn@V1G9yL)LluWP^;#+eMepm9`)fWw z`mpolz>*5c!KpKinL;Mxz-xYFeL;%3ySTT!yL(%@B>IZqP-l$Ej&NS4T76XgDH;1` z)HNIMBd(Gc9TBkP2jS!Md$L9FrfCPww&^Zc~l~={jdY%y)8`33l z_yUO&j5+e+U=yy^5Mt4$sc1tXg)LL6 z7LBU<69guUa|^tk8uWecetj;OSXRI{dx8TuA}%~Jy{bT$a^82noHZ5j99FdL#3vrO z0&Ybob9Ro421W#pG4Z8z9~RIh`$dMqDJglT`N&qj9b|T~SGO%4Rz>zt6Q0v2a=uS} zoZaSN*(SK%Vi#P;op4=^>*$zS|EV@-~hdl_7G_l{Y|4gWVDJG>?AQSQaQ zQ4hEO2TbNajm7V5=Nnt9h~z z?7eHV1TL_-;la+Q-sHiaCXD4ArcFcJEH9W33df|by$BB+E+VpjJ}bhebfUjg=Vytv zD&u+bz%Ac`9Neh3_%b)E)P+k3jj3o+Z-ysYGHDeHhWNG12zb+C8&_4Br%37nf?eWMz(y zO92mQNjyma7|~*eipx3ml^d33z9nc~5$vI5LcEl8@cbXdB19C25owIJXRQe<;I!Bf zwIS7K*Sx_f)CX>{PFS80s?x@GnSI}^v?MvJ7v2b;T%a_MHsgYJqKw3! z?$PVG{0$8FSdUbe%1`p}tM-AzVlM?b`mnH1X9s_&ic&r`A z<=2-NSGRQSDq->~AE4*zd|5>mH8^OJ;fjdF<1;TD8RXZw8}?gD*JthHv?`O5vMz4C zyU6_3+eWozv-;=Ac!A2oR0*Vsf^SNV zg(%P^`VhKs+O6}dmky=}NLehNl;(#Kaa-<%XS2r)PZ|Od>v2{6WrzK{|7*b{H9ODQ zxJQ>+-5M5Wu4|LL`Y~RFbj3E;K<&N!xQ+F)Y7UV=(i%?1VN>cb@}|)3tS2u5#8^Pt zBqIGIxB*&*V+ITIj?Eg~GIsbHt+a?q8$2Z@)h%mtZGtF>K>xbxM8)osu=>b`Nk|&w zi@eTHHKZXx@@T~R*!$QBLmDM>gH^C2!Vvl#La+H?C0NQ1T9(OD;c3tYJWGP$ruJU4 z*0%*^`axIg!Z4hyFhOfN^R@isa$=e6Y3D)z2X&EUvBFzs1fm@iPv>>`9)(S}^jIYL z`>6Bqm|~=y4jittb8+^OQaoYD6ePKzD6ikI9a-X^={t<5YfU;CrFS`UHbSqq0t0yV zh}Q)ofTg7?N{qWqg_46^TzsM`+lN#b>6!A>yAvr@D229qupW}Msfw4NH#qozk-h(h z81#@d`g1oBv>?gJ%s8L^%z1^PXU8v2hQL3(^sBeEC} zR|8(U9wsv@hkN6w3@DJ4Q?0@>^+x{@*{aIMTI(^EWHm7O(IXijHjmDfRXIya`F&8H z-7L-K>bydamZStoRb_sAkwjMPAhx5o3r2cn5z>eFW<<~63R0V7lf&b3ai*-StE`9` z9ZlYXA%+92Z(yu@9v+-NlZhz*fpPsdNHEG&f1oCv6`p?KJ3$eGEbO)q)RACMx2Lh5^&XNggQE>`{`kE3Z9T`zXMfBcT64zYVz&B3bmW!<(-h5-D8?#S+}*d zq-*3#+*`k&o)X=N8P}v~a9Ea9iuq|wX;&V@o9q#%N$YD*t}GWKbd8#Aat^Z`uJ@_iL(T)pSNV2fCws* z7jC$>T>UM*s9J-aXd@`K$8l&l)PQlBvr!}!$Ws^f;z4V_){wkUlg7-0JptN0(}O%{ z!Q+pBCjB>GsIVjfX-v{hZ;b@XrPW-E>eQwurz9|y>FTUNNW+oiV!A$X9RkPcm^Rln zUPBv8rp&jC#V_g>nG5rMd7lMYnV3tWx#ewcMqI(*QM@ktyml_U-VX;rE;Z1E%G_GG z5UoDMXRKxXrA&RSilnRSC0nwF((aMw=z={Fo@Ze^#Q%|sl;$Z($UUqw>726j26UC#<84Q;>6%Em%(7-; zX~ndMVP_iHz&^fcmMsW&^Ke{!2Wrr*l;t^Jn{Bo7EUuqC#2ijj@P?Wa&QhafOu4*3 z|4=v%e5ilab7?Z()WmwKrMsJ-pQO7UODGsS7-^5>>?U-p{-UmO28HKgu2r50kZ@}l z^cVU>@cUBO`V9^LONy9XQo6r&aFO-g*1qg94?IL|hvz9D-8trq1H0f#8-XN6gsNLV ztlq=s+>X}sbcH+!a8r+Ee!$3NZ4KLFT>L^)Y}1~oF>GiMEm=eH0@6K2jH1>N*?ZO6 zUGixn@q3s$nNPG(9&}1w^FfuKA^*Zyo3KDHLK}t&x{&YtJjlCVUS-X5u~MNqiax)xvC94K{*=-O7Fsk+3EBx>~#wlg9bLv8f$rvb6wdTv?T z54RJ8{fjEmFyU%Ja?6NGv;Aze!D0-evK21EQHQ>v_1|Y^KZH3!=Y-5VP)mvl)^n#` z*5vw2&?b>?_$>)g0c5zY%M7Qa_8z#)(i#rzIp)07 zlobZNl8U^-%T=Wp@A-^wZ;XeW)$WaLzoIf zP$U3^4<(QsfxY(#z?GvC^0EZ2jja&9XWt&nGCJpQ2!cHvsk_od8qPZQ`2VVDlj+KF zY_a5Y;+QTg*XPoC=y7K{>oKW>Xnu$1FjpH}*_`(EbmbcIGE7xXQ|8=lHw55hIoa`q z{DXn^-8ua{xtA%>K~tDbQBe+WeEGyQVPk0$69D40h>=D@0zk}(@T)ADv)qGg>(DeT zT3J()8Q&w4&5fXJTvDRD=aD8dqjvX0CEpqQeLjcdoX6glOT}U-ew8Ak)t+hKr~8@E zO@o9D9lS1+4~A2ibUZxI)YYq`9{w-(-aD$PuIm?t2Lx1#^cIR#=^g2c^e!c|P*i#q z2u+Gol-{c#1Ze>R1c8JqO*%;Lfgrti>59({mgjxH@7!_E_|E;#ANPzqMn-V(&cZun@NI{XRGI)r>GS zH?bZArom=IGbHfbcYge5Yw1)XSma5%RG*)^vX#MD4^11yOy@>n`mAOe*xP3C&Ey9$ zx;AMaW;;eX_n~kx!lTpsvAbH!{zkDXfe1rX4!EFxc);Ze`-x^Ylw%oUo<-=jYG?a# zR=~3hI^4P~F$AWAKIh z1zD+m86w@j+{?(3z*TSAM}u8~o~OE>h*QR|@$t`bPu$&=i7I8Cr3YL9=(R+83s%wqJWXafVvYuHehPcxUySV7lhXNI!#$@J5%CYx=iQuTDXK^N7gs`ei0bzfsF zS*za7E&joIW_8?0KkCWt;!RhI3TOp143=@uA7$S@y>mxde!=bGgL?+>Z?d)@hj6F- zfj2x6>q2tS>?*2&*Ks$=RxVggO>FlhymM6iRc-|Xmu|)iZCrkxf|zVu@cH=vja7iG z>SF#877K@xg)Qgr`-wA}e{1k*08eBIw9M^}8<}$5+w)90@l3KZ5b0uUk2Ap!8DWSU zW5`08IOn;PArK=G^!3bHiTO0^-AVP~ZKsXzvvW}n_g*uN7+N@^zoM7(v&fqUWdQEc zMn$wsjqfPU^P(HN#(=Z1dk)TrrX@vYING*78H=Y^pT z6gEw86t(dopcuf(dghVSDNC=eZP=Ka3Q_YMst&eVlpmQ`<-dMlhV`c(Sz>v!!{O zGd0CHjK#HYT0Va{WRmm31$_#`H#ZgJAtVxxqd7c;n8_)^X12x5QS^mCScp@K1bRYE zdrJMYM|;E&F}&lmgxp?C6J*7U0c)tm+F9&d3Ef=#`+YBKK>WkD?LlmOxn)gK6(a_N zl)*B=jtvDz2ipYw!?ABvf?;;GGDf5GT|M2x%Hcxo#7<%I3~yf4XhEutYtgI7$FpZ0 zv{Ir(@fMPHIB~)TP51oD4#WG(_}uTu<$vXi*bWgTCH*&z^53dk6yrv^CEgo^dYFE3^FKS;gz#wKPAC;7DXiD|lGOOyTBgw{(o# zM9{9EuW$B?Aq*qI8^6pgb<-$Ezs%yhkiyOc6}Dh_YO8fvXlDnp44K~+#@KRHk1A$? z!6xe79N{&av+@s*bxs4cCHWsTQo{2ZtqSTu^zX{kY|ZTtKR2_c-w&7792c5OXnv)O zIH6bGE!_)!`ZNjB>pQMthHHz{TOyF67cUm*78zD~TWH@QGi%b2^#o#2iI7{;`Z#an4 z7JsYFC$uP?aX8;5#~URL?-B0APpwifPTW zcKbkuT|*}qa+q>PtgHy@JaKz)haY7F2-py3%vSKeAVigUV?^qn=@`HS-+L4vZw05o zZGp1mKjx`kE6)GbY(e>dfz4C>!C8WR;^za3?iAZQXMQA4FkKoYc^}8)$creNKmQ?K z`dtxnydUt29{7Vr$FDAGEr+65|DR)@<_EDb45BS(lbPax9Q&gj|K)R3#UYvhSkvKm z7o+n?*{vlGheK|PZ(o?YCSFA(&u8z)FFUq$sO=k(is(c>{=e%ZGBZH^joexHA<}Y* z?X2jJ$cYZku_Xhpvwymj{v#94Vw*#jtMjMJ%|GKlaVHl&#~>d6=~DZT%+iRAqK^Ob zFC*PQ{~VQr`86i$-cIK`(O^Q`uo&d6BSnM_9{$7yzh)@?x&BW55KgR=uH{YRVf*Qt zqbYY;G$-BS^?x~;a|BYKkOr)U(ssm3Wd>ph&oM~>(LkBq>gix%LG94b4>7MrKQ}v+2U$OnS zsMK!}=_yVnEGHcH4oI-B8{5D2fW0M2EZV>A7ea}^>`8wud0F~^yW==sDt)W^s1%o1 zRf8*EjL}PA!fsU;mOp5HS@ym3Wh$LspTmUix?Z5+O9RZOaG;@o+AdciFEks-2x3rZ!vG}RXYGGAd3`t+|9|-5 z@9S0geZ9X930)c4szkrnV`iX0NjmeOw{^L#0uWzUs$LM|9K+|sddi1LqGh(qUiNr! zZ^6)3dXheo)aIupeg`O7H`#d|(`Ck;)$)t6dXixjB=}Y@3KJNn&K$#m*)aWYE%4eu zcyQG(mTTbBmeEzdklq+fP^t#2%zaVQFrn&#P592}j}!nWV)y}TeChDl} z8)MfbeV~}E+~RGVQM!k5iP4sz|*#8 zEEAWAJIX5ThG>tx`i={ilgHy--EZeMAOM_jp~T7N8ysl#sX#q(JBWt^THIu@n3xBL z8PP0;N)m(LEt1ik664EUe#);#k8Pd4s(+m_Nw0B>svAY98p$i}r)&B-v3sVtchYu!4JXfG^SJQk);)7*(W##Sxaly5Q*4^^oYGpe~|u%8qhZ0plRbe+rHDDei~lKSVXI>$I=dY;)XgCxDPh_ zh8`~P-bIrGnHXt~higBt&$^3jh={S6X26_B=c8SZ*sRJpz@SIz{hOXcPEt|C<`6;wg_9Y zhk4e|uGbpdLJt>17RNpG!Z(bX${fHwPAX(NSX2#;Fh3}@z7}8raMLQZd9~BWr3zh> zT%V0XS68^fQ_H}{Qx3u(B&Y?MRPk3=C!6i&;^6JhwxdJUhMA@O6Ci6ei2_m{Yv{i?MW;QK2dgpUz z`cHc;9F5mI6+UqI2z^9DRRARENj6Ny!bmM$LbjWQal$X~B?v1*&t&!ne4AWclEQW= z;if!;@4IxJ=0~Jy?NcDcL;SbP_r6vdN}qOHJok~(=j)nrF81OLUa`Mh*p6YGE%Ytj z)HP@mV&Tevfx{-&y1>f~PQTf~=U}(H?s!*3UI4^Fk_WYyEy~5hb zsTvhqiIp3BZp#ZR9|+?{8|1dk`*}e(KalNm^bG-@H=_vks;XaVz;61RDqO~av#>*Z zxk)eYsJGvZk!@@h20dWZ|_Pk>AW`rvP(P!s`-Vz)gE-mms}jgFJ2Dwi@d3Dg2Pi{DH)t9*zj}! z6J4_bvVq(~>|0xjd5kXzH>)Yk@z|7X?50v6jR^RM z)LywL*A2>=sCNiY!JS>0+WW~X>8&XT?;Sr4&v^M8EW9sE((Ey@2o}?`SvDP5gXf(c82EIr~UCFUh zV9?PgFj2a`51wPfpZrTN3MzKR;4KagJ3hV}n|bj3O`@bPriX<)sSN~dUJE#1VNE^s z;98?yhLE+>jaoT;1#i_FnDd?3<_c{*ARO^8CRC z$vJxr*J?OfUh6xe7k^!l~=P5Y}$N}IK zfr-NTfG=9l#BJx!?P#7=JqCIX9-ZIy5&_D)I}@=Q)J>vC77K##r0Py9RE{l8Oyun` z>+IJl;P8Cz#|$kVnkkyLzc>)21%R*1M*{h?+YYXx?i0fXO8QzH8tjp61Kz|Eq?@-s znZ@UvS?)P#p&8N7Lo{Bep)w^I*9=sss*F!J{e==+&t(HGQdJzKQ>s=$)ks57h2KiG z?lD2=rpaUUydZ&79X|2IaWGKZ8XdVLFetiBJJgc|#9$otaz@4h+9B9CJ>-B)?r#^S zrEcF4yUdf?4jgJuNxzFV0jTG^k>|MW_eX+E0$tw~R7d~QS zYQ_cNBoGU|Id&Jd*?p4uW{)n^i!gEah)Y;dl~uq#Ato%j<9n}%oPPCHFZX7Jp;UQ3 zDzkBd=yf>AfeFWov<{1I{f@_ypb(_38{(`rmqaW5*4+Dk^AW-JHjxQzW0{x3u(wDX zF2^qdK^|vDp-g>{sg!p*&H!Q+zPRP^#MpRH*6?RLT^XUH!kz>1DLZzQR%X(amUkIa zQv=Wk(+?L#tk@y^gsBH-=d{jlx#XogcxS>_eH%xGqoENX#x?P#cpOy;ADS76nXuGR zb62lbfPa@PJRb7iLoSstlZ#bYg%!{?(AFCKwXo-cA*XH}F?9=ty2{%WF_?i?3=e;ncSHx_l7<0V-) zD<`<$#tzJF9^KG%;_0R@j4k0B9xmuBjGY`p!iendo_n_FChF;LQj^Xz5SF+b;7J^z z@^Ne_yoD_%=YjAg8rT(=22V?aLTO;!k$05Of;LrYrBp1adbT!KGY&dsbW$NAbO)*z zkIdpBF8g3Efkp)-1ihqxj@`b&_|=_$Liyb!-XuEH7gi5^p3PLt_VJ!JMK$hRjXJ=g zhdf0Uu=KWoDl!7M^%&B`p?JzKY9?9~17YeBCOq{*Ls&T(PmSF2^C6kHU~ivh7NVG~ z=CKpeu?#l+D!t`b-TOjPQE@HL;U`qwTp%{l#1G2fMOVv(ak7XRT7)-hRO)RYLt<;N z69)Dli{SD=>r~1VXZFk%_gvHm>>*3bTp#Dsq9mV3y-S`0OU8`1!#{_?vyp${$UFjf zC|igHV(tyCON!t$PLqXZ4Th%LwfW7Zjn+LkziQg?w zoNnLDC+jkO`K&g~(S?2GsYiNwtc}-_e=5;v!>qpEqD7Q$vUt$1_or*jQW_R@o%iX{+7{+J?Fu}v1t>%H62-?xqkZ=x1G1; zT^u4NWpJ=nZcC0{xkp-vr(xKo=XSCX-VqlVlJGtA~o9#!xC@}P*l2oMI=}epM z2ntFHkdRYoCu|aY`aXef?m%g8oYY=bRZq=}E9XTfD9Eyf1Ci%gEwsD1hYMa_i!akY zUA&w>6aMJJfLhgcot1vm5Vnp5EzR}RN)uruxSPSMEiY4dC}Sj%mUTM0Ti$LX?%0Zs zt21G+*?$6|d)K6&Eo+F6CsvQI%MeiA9!8yLg0OO`nW{$~E7XbAXuLyHdC)B0MlFyS z_wYdL&Pb!7x*Ar^y$^d_Vv(xsg*FP=G;Hajb?3yAGAa%hvB~uS@%*a7+DP_&A_jAM zur}VgHbXko?Y1ts891)cmGr17buYuDDsaG?tj$*ZoTt>mk=weo&QY+P2J(2GXi1Ha zb#p)_rnv0vqIlo?-f9?q zxU$ph%APZc%EKgO8P+(?inaY3#x|4c8htg7#sME#Kt;w0S;zEwqRsCQ&({A-8=S<54z2h%mq;EFY=Dc~7um9JwfUU{R15nm+BD(A+4Wn&uJ zvF2zWaY|HGQe|M4LE+I!NzQ&?Wo3$Iw!gEUZdxCnD8?&lxhqsHno_w}vM>`k?mXy; ztWw3g2m*O`DNuh?bABD`@WKXC(w6`31KUEAEPT(zquE6alZC8=JTdGqr$nZm=`gTb zY<@5^`4HE~(|;ypgWi9+|W4l#%mE)#BZxys&$(W1QlJKk0<#D9c&g zI;p7JZO*ZGo;U-=Ol3*~af%o6>y+7V?6EbdP?P7-1PQg`o$^?P?g{x9FbsXmbBNdo}gBU|2RTqtR)3Jc>ZWdY4pDgYk0G9!7@n1LnVOI;?i<6awpg7;DWMlV?x4 z;r08X_(0j#M>x}0uI^IcNr19&Iu8HR6t~6$w|6%_t%^L!FDd}9CeCHvk6UWu%boS3 zkN3EOw(Mksxf>{XcdbX_2vaM!n3SQZJ3AJ=V>|O5v!|{O^ls3?h>mrwJAXYk)1f+K za2ic&clLr*VS+;K?YXDV*TQHzTztWvjm*T>ZhcYT$F2Yc)9BPa^BO}nJL>J!PpPS6 zv}Vf&sIc^=3f#jkp|6KabTsZ!k1IzEz)&T6fa!tGC-{erM zL@|=)R1hUg#D{-oHWdhYlW*rA-Hi6GriYFhn)7=_%LNj1<3AEtK`X?NPU~c}v_JZQTF0?#S3<2VDhhai}(;iI`nTAH2{&-?dl8lqt zG`DTXV~yI|NauP|QX`x@f^w;n6TW>nOt_5|du?nXarSivKuo;HWJ4P6+*JD1+(_38 zp}AFhGTgVm6(7E}BH0G6iy@wuH2Y{M!GG0JAv?O|$mu$L>#Gr^eMW10WM&%;ZI__7 zoGbkH);G#I*RNG#d|uVhd2Wbof%NrB{FevRdiaT7xciVM=BOD@o)I~phAw5U zM99O40^M8B`3GS3A z7s$d1i@7wcmcF{J8>Vi$lm*oK`b|IJ$ApLa=ogA5*z+9JyY*A38rSnEci{b@-Rp1J zQffOTVArQFu1b>BLH{%w?`7vZ)aJfE-TcbYIw=j)HAdF>mj_5;HH`rMsO!@hi5YB# z&0L)Zn9~@Enn`;9wT>PQAo0(_hoNff#!Fec1rpYPH#{s7X-_=P0?nk^6kH2TeM0{f(n0XO;s@KvFQfq!F?`cR?{W^D$3B=z{PT(k5IU#eVW9GXFx;ktlRrpn^^PhwP^Cq}=UOu8Ez|f!3 z2g-_4idD)%pz3v7G^hJ?XIm59+JXuZT~xjzBg^R+!4`34e|+D1jX`EELY9 z1{xUQB>L=B6#x2F<4SD^B+wRBp9sp)aD=6)ZD%XU@Q*UxMvX3Et~5*qU?pR??lOR^ zZ9aXwHqK42ej(vt4ez2;27eAxrD1^W8upLq;O>x(cU(C6`KH|uI$s^7Pdn%mYR8x9 zYkS9~!7wo~3^Y{wm|ER`8-OP35n5!zmvIvb!B*`Y;Sh^wVp{*Icfe~=F*55^VxLz~ zQeIwcRp2bTU@cVe*OD3*Vcm^>)0!Xmcs=#ZmNOkML-o00bIZUiaXugH*a#P}H}3wv;kc@nhR6*-eRIFcjQ}T>ffx1JS{&M9qCj9n&Tq!48?OR91;f- zSNEK%MhV8%Rui%S=2RFOKGms~9KIUC!CMoXX5wK<@pi0Dcv@n-d7MI(wF2IU15R$z z9*b?@+JU+-NONX7B}H7AYK|FTGF`Eon6S$?IW0-qjanaMTU3TnaLxWo!{Vt;Jh4bl zk6K$Kkc@wuN}Nt_6$dP)p*5V$$yys#`lpKAbLhUm1P-8%y-9 z9j_pMwBL(#?yV#(AU#*N5mhU$FQ)3ugQq#Va_05UL0D8usn>Y*hKG9OXcZ-ne4Y_npbF9H*kxaaUwHBKj4Xs2Mp4vjwYI{Kd z@84M8rA4lH?^GkALs@>S#F^X2(!(!Gqlb(#I%DPD3;}!!s(>3GRW7@Ku#|BhTl`vm zj4|-jh<~V24Zi>4je?B;<9{tbWo%_EEPqIaZlx-C_W6@gdOa0=rhKWwUM8X^fzGR& zG)^ft;4dq@Vl0{Q3Q%(JJ>q$I-+X?vDu;{Y$X zUQTvKsV>wSjvE>%ZeEJY!@;ahh;;I7i6LG_Sq<@>Y!S!ptp*o|l2Y647bThbw&li* zB#d%~zHX+pl@x;dW8!h}hNL`fRzF1&CyUdbT~`M>&t;j1SXLz%Uuntr7LvZdu@nS0 zvjMyLR1NC=uaOj2oL)G1#mnQMbn6&~hcV!ud+1z2xv#7l`g&YjEU}0s@ekcFjt8`y zOifAJg4z@U}L%#XR)l^{{trNTCVp zaM6TQWmbG*2mCgMCx=UkvQ%6A_4`cQtv}vy?taLaygyrx6ce1Y&J+p3dAremG-nri zBYfmd4PUX1NMTS7Y|(@-Q1OmDg*?2&?i1iJY{geGX4NF#YU5dr?#S4<;UfpmBq&{& zY-3l`DynX?i@26p-LXbF%95CNYZHd~(}TEKd`YYd4qJP=lG}qBF5Fs<2??9gIo2f|z6(SS?Hh;v9t%EvYWK8F{*YdTDY zZp%72d~SJ|=?|>0ju!CPViDl^>tA-jpAz$OFK&<;7MS~-2FsgI^w8lTd#IRiJ53Tn z=_o*YO`H}{%H2dA@!KHtPi}G`r82mNvzNbN_~4hVQ3b^G@~ikEsq*!w6eWx$r;A}? zE4*fA`&n>=JiW8)zCsz%l;ml<7Ea8{L)0!!sSms>(|VEOnc=T8+@L~2F1h&JZLnib zq|#th_%uFOO0y|{!-zwGT!wFtcKThIH+MFN#mF(IS~?=|W-@gogzAiNM++lIJ6XW~ zlDwcFCHJ~Fx?||Jf0#GNVs=!~&x)fqyG)O)JD2R*`Ktn7aYlh7 zX3+NucR;!#suLJlm@jJbY!m;~RUBY7T+EK<=$bsj;vL~v#LNr@=^KmaYl^xh?!Iyr z4Dk}i$@L#67lX8V{?nnwj=`?`UA)O-qM65%p_#{QBODQ&(#L{ll<|0JGY2v!p!K*c zqUO=!QoZM*cA3@8Cm(r~dgBc(n3T!p`=|o#P3uf*wOAtDC!(R<+}X=61)+9)m(yEL zonFi(eD)4=)vWvn5x8Gl*Hfp=c#n~1baZ_Q>uRd(PPB=IpOiP<+`1WWcl~(%SeV;( z3~1PTY1=GO;DlNi=LGDXBhHzRwi!xy-0GU>1fRsXmpwoEGJDaIPGMDFpjG3!BT?|a z0&c~{S{9Hf=qvP*>}D?@fV@33vmBWhuV1_g+s=*3NCo%fU^@$abae1ml~LBl2i(vz zk57YMn5|Dv=|66cf}~c2*@kPCc$SGu9Nn34+C(~mG0GjpM@5jPAM>yC2|CzgNy z&iUSxvIYfF{VE=?*(Q|PDhMCT&IoWI(FTIfNIR$aa6jgVyLF18^sovVokr0O&jTbx zz01A)S)Z5Jk53w@>FPH}sru-+jZU`E1u^4{3njT#l8^W=y-M9{nH;hehbG&z6-N;i zhS{4=rKuIH!UqZX*t|T}sOoE~2r8Pk0~sD?bSZz@$0w_g<>$kZq{UiDz&7F)=P>#F z2AS%`zUzc{H6Ud=w@iZ*&uF6yot_PpJ*-vMGO%?~Ox231Z%~*g@Y!35C-^Eu_3HBC z(AO}hPaBqpf5*4jS9DRUkb)uy@mPM3g$w#fJ82P=uiMaGZC$<_|$mZLK! zAV3A_{&7=F1uqu9blZX|jzpJ{8Jf)2RdlL*)8c{U*JJgrPkfcoz|W(Z++7M|=Fc~^ z?a}+etU5A>tPtvVa?`z*M%Mc_dsxE$=-;HISYrQb9m^8SY5r3qqj%39jbc;h&eUG*MyYfM9nV&r4{D2-(ZNDFH`Q%B8k_KyV6yJV)ft8GhiX2vIb6d5W2?ucztTatrr6G#T zY;<}y!A%fbrasA+clpA(t_pqjYVL0=nDLNb13?yuM|IZ%YLufv8W8kjW2x!J(Rnr1 z@xnA_m(;`gdaINxMUfDyd*{#2>GdHD@sj7~6R%;a>4Trll@UcAKIJSdzj8%FO5=aQ z=Nfa@od^76v<1FSE%+D9!y{}O*1J?BnH5e?xd7wT|tSsN7 z1lEZ%5cM=}BpJ=7;%`4L-@pVZ_7gE&my495KV0$nVL~Z|+f}a1TtC|l(&lHwJ%z;< z;k@ic5^fT#t2dlAr$1_b82S12Pg&Iv`-S)yGO@84MB~fBNpARqs<)rnb}re3#YIy; zz0}X^oPL?wgb9?)c$)vS%#wEZTfz4&>yj|8`yT3DE%M1~-o%NYzlYwnqlifpSECP| zJ^)PNB*?GmiwltD-p$R&!lKB#m&bDNKZX6>DrAc0yw;?s5|yRiZKm@m3?2|7;up(f z$SwGTGS+J!K^dia%Ov5?5?lEqMg?PtME|uy-BG5#)2~#sSu3sdMcv+xv|a6e@3(sA zjP7A-w~wH>&!+rhRlrHT^l{bUsXF!3Et~}AJGUyi_}R~*`pt=47}C^i3%t-NqSa)K z8z`&sQ$*duVry{n)P~U)H`G1euSzmW^3tW77(UAE7y;hI0epQ^_txZ@gvOh^HJf}c zwREa)$lakFv3CmqAp}drei!AUv(nB*ue*9~2WO-bdnQTldB!j!J3CgEal+YMZ+QTT zQ8Yy@V>Oqm$cKH!^A5AHXKVE+rg1t^()M^tX{}df8_uv=BLgLAzMV2JQHoE>A@bhV z?ki(4UW_-q_iOaz)MprOYizTUYI_8T2R#IQ1HABtQNH7`D;sMm=vU+`;&ZOB_jEZl zT_6G2mGj^7B{P_RPEFS}Ja>uv__6*tGB&cJf1c4zZR^0dxUgaFG^A=uL&uD-JZXyI zlTlaey6nEhC{CRN4d?pp3TTb)m>TQCn=;QtVd^Xh?~}XvIfnViFrc)UV{uWzoAJ)r zf_Sf+aVkQe?UIWDiS3vU-BN0$vnd;*z9^qH=hdv6u@@Ivc`=ZMWQActhv-6hnHQV|-6v1%J9U~^jm)^XW(^8uA0 zaiAENxXKaH&y!g}JMP`N7M~_mw1^mZ4aO^LBv|aNXe6RHg*>qP_oEnP?)6=+gSp??%2`r$A4lopvZTlp6k;G} zAFt!Zq~Myyz-8+0VX$dUl~+uC;xOrhje24KuS+K4mRwvQQuTXxG0}`l_d-b3nP2Ng zTpt*0{24D$N?Xrw>IX}Ohfi7FABB#%f7u2p{~OES@CYHYxbs^-tqNoGlZ`M&Kk0Mp z;{GfcMrvCG-uhI|+}`&=qoc)w{e5wMUT)#0&#kIufDOuYvX(Au-3vSMxQw}BN>2eO z_9Kql)R%RhF2ZJBFs?EN2eZ$NFraKYtVNgWRyk%qJpY1|UF1)Hm;mto>$W_TRIazp z(S>#6m>imc*JF%kA@&H-a|W3MV1F$aM!KQ40}RuFx;=JjS}sK~LNkd2fYvV0Lx5hb z!)A&2%giz@1}&f>TWgQ0YU+Y}>(WE~oM)v-akaIN!)6W(kUW|vfiekWL-qdh%;BOd zc4-7d&yH4RGT)jzmqvYg%o@d&my~qtv+97(B!DU5rmU50ps$hq7*YQFBE(~i$5>cj z?+cDwx3uqq3?Cn@49Rd4_pM9+H#j`^3dmXyG|XfH1Q#BHbs49Qc@kwOg0t&RT(5W1 zlIT9Gj<6!}@mK!xkcc@sf#=)WXDivxWh$s*)Dv3DWs{a|r4jM<0e46$@q}JgyJJ~> zi;|M{U%C{U;33{5tlrjLi*c`!agdS=ftar3yPghfn;*dtZX0NE_W0?c ziVVesbZuqp7A_!2MVEHRJnr+zS#!6Rro$Wlpym4c=b|NfdljsLMY3QZxn| zE=7M&kw5FnIiI)Lo&xrqJ4D_4mA$!zYgt(w4aPV#`=ItQvbEL-C%D5Z;(`h~Vpc-` zDhU!tmkcy!vlf=Y<^|P=wSQnF1{g`3C>Uud_z69ZHlwz>K}eC?y1MV3-a@qS1s7tW zo{6=O`78Da|o!tofj-78@FT5W&RK7g!OHU}E_k`lf3cP{9Iq8II{P zy{*>P+snEJa@^#n@?SYg^TRgL0crp^?y?$N1^9)MFrn{GU!-!FIF?UFyHF}+rX5na zwfK3Uom2P5cO1Jz)Ygc7IYLnl=8hY?qB@>}HO;U*Ztt$C^bge1kwb)w?{hNj;Z1Y) z{Gze2T16ChX3eOw;zi23#LEPsINJ3D>i8$`OF|Z_(_o|b)6O~JiNUDQ+gT>`;HjW> zk}K{3(fcLhnHq2UhBraZc^f~g zdNyjzPcBE`%Lb}XTwb^%B~S3ake7c>lW33m8>>877l+TDp(v5Lf~9K2m+D(~*e%0` zxSGO!8`}eHfZhZB;ODXmY&V*lAz$T2k29kcV8!=_uA(>$l<7;C0XGjDrSF*3?aC_f zPZ)Tgb1{u%vAP!*lhU&B5f8V8X8k3R#SgtNIZ`>~U=@Lj$XYn%|La?Is;uyL=4yE} z*B}~4v$as|r2$UKB`ns*cYVY+EVZ6PLmUXJl*o7e9-bq{Sx8 z4#6HdK{WSM!HD43pIGq72_k5Wn}XX%tCWIs2?pWhKb-7*owB4Q-GjO!9!D!k$mWm2SxnStYrQ#k(NTTuDNwG~ls;D1gP_sx z?#v#vJ4=TCwcb4U()4O}(UFZl&#EGAcwEubRo0wx)WfxTX{@*0BHsV|s5DuauMgiS zV=nTiYoad}bCFJ%wZJqs(dIXk94=+vgKa#&qN`m{-x$+LsH{FFD&!_lL?IRM=PICh z2eTybvcC)t9&iS-NhW^buEc#`-<`pgj4~3aC7s`aJ)fslP`!|GW~bV$@>jP$P3=@e z`q*c?MV{Vmt`*cWxbbk5m6iF!@+S%6#SKHA=ZB-fIUXw;#gT58u#(E~b2iDLE<6cF zfR0^H3aI>7|4b&xBVEwO7kw>FR70;>&%+XjH=pfsTPznM z)wOXyX1?g-&=U@T7u`5Prl$uztX(O0o-q4b%m1Jza-EYay`lN>$Ym{X8L)yM?T3C|o$vfWnEqn^@5I;*?(nJ9*dAF#IGS`R{>5TCb|~2%~R^g zY3+&7ify=l!%i|!z+lJx5^@t{zv$^TxiB^7thD%%cW~;CU z)6jrUGvOahpVg0J&_(RCyY=8p@(=n|29ZU3XUG;>_X5U-?gp%#y)5^- zY=~3ayQA_vjFt0YklLbTL)IA?F`dMdfYN&8XKucfWO4V$Y>{wsyiGD|-|4MBO{x=9 zFDw;y9-!pHLM^|?=)Y_WjG;kE_uHs|QG6^osrasBJiTx7n{V=t;af~93+-0UHK4BM ztKW7J%Ra?YMAb;AL>{xd_Pq#zeiREnrKxSg_Y+DR08SVQ`!jp+Stf@0QJ%eF$$ll; z(A0{=I_NYGKm2_KM~*dGMZwjD$Una#&lB#v7em$J>u~@|Y|ZkJ5p2Z4f2vz2TWXR~ z#NrLyD@@(50K+R_E_NE|ARLq^0&l&OsHe-XZ;;`wdx(OOsZCjC1Ko@b=>j>eYZ^8L zhX~utKNR&Y6qOO>D`G;=uC?WI!Eh9uKw5O_XS%kY!$iLWT%Orvp1gEi26TndanyAH z@OJ99SY^PaG~bN0>SRkWueU|i#NDnAO94RLs%Gx=38CE}*G!AyDM6JSiXf?rL-QMu zI6rP9VR#{utZ8+1AVPU^y%>9z5?rnhSSPTpalY=m~?UNR9sfS(+Ha6D+Ba<1>8ZeS8c{wX|&HKIBk_a&&CK4SnYv zB|;io2e?=V7{&Wmnq{bX!UJ{T4F;pP2o!Z)+iBL_C3=^;CAU89)&6WRLa|nQ8n|Y9 zD_YC)QAz&mG10dt$U}fHo3#h?9YkAos;8)DS&g}MRGtXyT0muZP42!VIBPBjaMZTg z`_J^bi%#CO-!pV{(J2m-T(6rjkgOwq8*@M0h&nlbrTFB(8Jg;k5_p+DgmHcdyT&Qu#y3L`+=R*0bZZ@}NB$J7- zniD3m<7eGXz3-Ut^&|}pO54x6oIeF?CYhqRf7SgZ*g9!)#Nb!mUxL~G_Y6W886F(JiwC=eTGdP8h9@XJ3^J3$&wS7Shd23X6l1qoJ97+=sJ1|U z@SY?%-+^<@lxq7`D_4{L%LOQZFMaJF9cO94^+Iji@;qvtp7+1o+e+Fh8kp;?5r!gq z9qmvvQY-!Komf>`vb&NFMTON2Yjy^kyQ^f-WDKK*X07w-yzZQHBcG98cQ^Y^W&pyM z)IhAD;l8)8mKzhxj2v2TkH`9b%^Vyrv+49o^uyK^D}DNtbu3~NiKScXOmm~~wC(@| zio0Z?Q0!oC2dW}FSeV_Q5n4qOqM+vRT=Y%&0f0Gz80h=Gin*gKC1R4ER6;%m?pDr^=@1qe`KmxuhB<0EXS_= zO#QZ4SIO8^binbNtgyZ+Bi)*e`@#7EgI&4f4FCEWy=bHGWmG1o54@oPX4qaq?7ZV< zZ>wUMn{~zhylfg1sPDfc2pp}cfqoY-aP`jXN_&e5ucEzb&$4@~VDyyzDD$1|7r%O8 z$6LnlMf&=}90FW$*r-k~sY!Iuc+kz7Mn3r)&CAYtwfA@@s_bj}V2xub|Wi+<>+3GSH9P&`YY=Hl>3$Wk*^kuDs9l}%EK)+*6FMa#n z3(}az=!XnuQg!c-oy|x2jG~(u02C9IZD#9LQB}r=L>g~z-Yt>@V6!sY-TE=*lFC0| z#&<0IcxuXUs!Bzk2q4`pt(>8u2rXl^+AsC1TUL-z`@c-b=S32 z?{8NMn-08U2IOW$U`NO4ue!I*4$cIz+dAAw!ABKxXa|&k_wkPz`?Og~_@Zw~QKovr z)r$N`7voxx;0|KJW9g3f+295R)pl@xeqNn9G%gznJ|?IQA;=P*lDK?`NwK?aKGYyq zlV$p*A#Tw-a5Qzovn)3n1nEl5I;2^jn*KOeIfaHa01bA$w31E?VGL~~W5Dr!L;lueA%)3y5g5kjJV`A-S?)>Pd+GPOdR zX5Qp2+Lydf+LjmdE{5`xY7i+xH66Q5*Un|lOSy?&H%q1NHvnhRAn{Ro|77=yhgLYk zZHZau_zbF!$FAm>S!N`xX)J|?Wgxv036x>?&{?#2zJR~ngxx!RD7*wGN7{gzhiv3s zGgiJBnr3UJ_Z6ADufQl6y1b$Z1Q8Dd+)g^kHT2ue z4o2U0O^gYrEwY>Q0eO!f@2?c4zL%*k8CVXHARQnbVOi%otx81;6zWatzU9g`#Dt~y z-dS6_%91<;m7KWzAsJKu=hO zv^CP&e(@_=Wj?BWcYae%|9MGKQ7)&sMtIO$^7<<=pq7Cc-$8>zN#>MVvptqw!orJ__h(eRv58y5T^Mxg z9YX8V9QA`e(Er8Wdw@07b?c%aO{6GNqyz*3l`1WC6r}eKLTJ)M2t8C$2vUUzNRiMx zgkBOlQlv}oq4$oW6oF5_8~ybA|7Y)W?mqvy`<#3CdG7PDCNpa-7Rk(7bBsC0JKk5F zg0sV4{Qe#fMhM#S}QRAq`t5yyM9dXBep**dPkx&T=2t<7P9B#a=IX9peCm|b%9M@@Fefp ztm$a7%{su%%`LR$*ldGtN^jCYF9cUu#Rod`n$+uw*l=IR-xNkLY*tFyD2aV3chV{3@!Y+Svc#!ASjEbUdj?JLEBb{EQ@o^KAG>IC~8tiF!W*zQDRvE2iOc|qH z%I<+QG%#4r`*x1i6l9&Ibj^;qGzksMD!=3?8tMIfl+(b|$P9AFE47|c$aLDO9%q>3 z{HN0MavFvS-CP;%3t{r=bHQQnk zeoxAfmam)P-l_MNkKL62IhS=wagK%m}^$}xY19{ zJ=%dEr%r{hP?+e0HZU|>D=if1QKXr`-Xla0`8{P|hJMm|Zn!qS-}216&$N#f?zqQZ znpb#JQYCu?kFSrRvzZ^yt^a`57s84x&q)&7baESKcOTH<2<##e^7dXa)^vyqN+EQ6hQ9?ZAeN|m?8OwTcz-8 zC?`J)CZMWYWUH=Y-{lRDq?q*jxlSJ%jH454;WPJ%s&f8#O>xL;{5)Yfx>pSnTl4I_(>BsK<%@x`b8U#&3T$;GhFFu)vmC)Q zMlHFs;TEX9`EwjDT!g&9mAs+CIdpj~NrFH9hG&47eNk5Vvo9mqVx>JFq^x>|2dM5X5+>BqDc~nEo zsUq?zAy~;TaLRzk`@tmQtjo(QY9?CVIi0+)%UzaeHpI*>ea_pP^SgvjELz0Za-DJ; zW>Nt+I$4nYQ^ajOjVe1&$v^eF6J)#z9^fadRJgC8fFn!z+xf07``eCbn8%B`{&v4x ze_OF3QIiF`Ukd@bzt;!#|5%4rzAE|$KOxy~=lj?={g)XVCmxHRsUKLgDV+-&nKDCh zaRn@LI_qjSPJLSo^V}w@vEZ~H9yhj3w=}1rrD8N1kI0XfB9v}2zMV{_(U8iFEqq}O z)3jNVA1mPk>3j*6GJRyQ#iA@O9Y_YF?au-4a6&Z@aL^y4RoHq-HtJ6#5RZplGsCXA z8PR&0>I6HccvKQ0Dm}cE`DtEWqg~|g;~g_A5|P6Yf@is02Mi;WWgUkKKG>D4kF#3P#$V3K1iUYDJ{r|N;%$_aMp^Z ztGyu6%LiAEkLsI&^AHSRr(JXLaBmSHE?c^mOu;>?CGW_O9@F%qq2(9xFz4Fwfot;F zh_OGRY#&dyqTT_C6)f2_ihgv9?V{P=KGK_FxO?5xSHUt!OPeaABF;y5t|%u*#C=Q_ zD?da&UyfNKz-S;-Rg<%;&FuM0` zZQ3p*y>xG(qT38oY}xrX_f~j7)Uq%71-potD}cH)b|u*XU5ZSp28`n|Yfn5jGe$tM z4QeH2ihc3(cgm3&=k+#d^t?o*pOD~KsMfNJYeEi-wCa+ zzMl&2O}5-!iyQu=Dx*wRl2neK}hDj8Wd6Wfi}oJ4;&i;TQ~$)YSPN_F@QPC2?;g;p#_Gb-bWq{x*%=_mU>QP z@#M{hG!PTZa(D2?ZIW&74Y~=O;t z?pRp#x}g5U!LjTPWx)KhtPC<)O0FvLbz;j=d>U{NY_|bubNuQK7$u{K)->gi3{>g zE0fu6o(6HE&24j_)WzvqJ5MJpJ}WWq-ym@RfZ*mXGj2kcLts;;^w|PokyaC1l1fPp z(gn&0U7Pr?{ zx%?sVc|LwLLzePhd>pvR%thqH*k`n7lr1r~I(q+77x^7`kbkH!v&emawwMhi?Z=zs@t@a|i$%-Q%wuxy6HiWkm zh#oIh>x1?fd%=7j-J_@oIRgk z`|&ahLpR-Jm+iz^Dh-tsmBBDOC;cYj1Gh_A1E}c0U>+tc?wB7Vnms0}yt^nWCS6~H z16Zr#ZTK6jdnG1IDXN>bGXpB(mbdsRBR{Nssb~GN?cEwyjme;2DT2yqgwb=SjW#8& zNs|DM+Pjqje5vxaLZOusA{Kr;A%}tY;<4fHbj~q-hSCvE-;a zk(9I#@x+2bE1d}ArdqmPRPh1Yd+RZGQGr*C^ZPKFa+|kGvK+TbiBk*46``4$niP(O zF2Q_SA5YwqZ5U*HdxwNW-<-lF_$M(lT&U=TwvSGPehBHFQroQx@gAp7m{dELu{=}yD;@uB3l7-P?)*D2?-w5a~YgA zP}Mat;gfx981^u}<^5MGdb*Zf08SSrvRZ(dSS?76>>W;f*) zqgX)e(ya=>h~;-&DmOm=i@u?}Az|G6T~$UxWS+Ra_!>!___9%I>$oJ3mqvbWF?0$g zL2fW?H8Chnmfk}@B>DA(Pxwi>Vc=0oMbwgvCdm3#PJNTtou~I0tI`$f)7HX?H0N#` zu-8qfaq*zQv%K2y;vr9Rm*)jx&2W!J0uFZud1L*WNgQMQM50drxqM2v%)!{|vfDl) zH`X|(86ra)NJt}a_25Xq&ew^7z-)w6!ph)F^d!B#{m_j$ zD{bq*TmZ_&R}@hT{|&;w^H0Eh_*62mE32V75|EIC+l_k}P6uBWpcD_iAMq3S`1iWm zu)r4~v^HGio#>GyOe-Ro;cNvy{~24F`R$eePsWz$8nO`15Lc?+5coq_6>Vf$s7v_m z#Cua(KU*VRv#-s1*`Vy64b_u3Kik9vjjzpW|K=>=)gx}&M!<7}OcTkUt%<_$tM^4l z+wSLjfwn(eJ3H!!IB4g8l}m+16kErvEZ-9M;FsuFjL7>iw9EY~>H2pHwd?1bl=Vvd9-$Yq(*_> z@xRCm#xzU%2`vobnRr=lM6Y6b=JL^ttCz9P|8ALxQ{kA>{ZYe9MMe1cn1G`GhG|@F zjCl1o$tSvf_4caIDv$LvNyLlDrHHJVh*q(Mn{d#dc-{wTRRgdlow<$LVX0ZUCtt}j zra#>ST&tpc*!$|s_F=Hwp?5&`Tw!Udqa)_Iq{)5zA~zx6m)M1g*MLuElzz*%WA9&9 z9DsPGyGSW3sfV==vZjRlQdHg}aKdOj6Fpm@b-wIGh8e&JNg@$a8aK4|m!vlX~G-a)@K206JqvMml-cqB62% z5`pC86L(`~#+FqcUKbEAFyT8f_*G8!b1eRC11bC}ZQ1c~qv}^@or(6E#b40D-x;tc zv-e;Le^Hyu5rJRhFoZY~@c^UMzteO$w%wdN1CM{rF|_5^XET2crchPnfjfY z+i+V3@yBopk+QS1BY)I2v$5K6kblxGavF~- zskzVjD+obFO2EMC0ROA-na^vf^7U8cc?=&efm!(q*9&p__vy}(e=UQ2qq6W@%J+NE z@6uE`F>il}Nrze&AQTQq}^Y8qb3(cBc)lAPsi)aQUIDnlWV8IldJW~mo4B#vsA zsd0jk+&&b!O(rW%EBkUpC84EvKt3eez~NIwoaO zNHfLs^lLeIeI90YpNUhM6WEoQ2!weW;~4RBjAN`H7Ss{oxexO@%`+Cg8oUTAMQQ8;@UdLK}_3YQyEC%}*V^O_E%PsYavp_u2D9o;v+Nu^tfd}A24 zFs>o)AFYlmAT2D;P)k`WM*0DOEk_OcEl=9Uhz0vxQyq5f8YiYw?p;C2ENF_mEog`& z#mF*Y7H$&y3b zU*(8S4cLrV+wJeOYvV8SWy^BFN%H`{d!rQC9qHISrc_>)D?>w_&=Ip^Gm<18)`N%G9&QFJ>Fh3dsf=@nL3(ZsSzNYCR(ZOU*z-n2d~_7s885h2U;P z*qSsc!z$&~UQKGlxWI8{>L zkIr-?CyLT5U{f`#tH`%GP3~dn$n^dsP*I(mi`%K}%T-0G7@iRMH2@N)fV39!!Ojsi zdyS!pEenxT&Pc4-5Gd`+$mvq1Y$Ug;+Q&{LVs3{12!?Qf^yt%8+bJ#s7Ko0EG5~hc z7Ad{6M51k%Sw5UTD5$tnXwMp7o#XG1-BmJZR@J4D4lax91o+uf2d7abek>M}{aJcL zX85Rm?K{;OU&*fyEB3cr`s}yY^e-AQ_}?=w&$Smn&DniXZey}CqJ|?(YxOlBRh;T3 zqxok$b2GZ79us|Y8EFy9XL>qG7{{WzkBRF|C;klrlqC1iPhP2dACwfjr-?fDg)og4 z`7$u9&2~nWaPH31)E@O&YRqyoPAiPF5-opbuj7GN)Gn;$)yXSrKEgR3e6`2GLy9$m z)Vy2oYTCSF&SjrKH)x%^_v92I0oG7yjeL<~P>*)lN#A9oHo2!!{E}=*3+VnXk&I!W zh;TCT_Ol>Bnw8BKnZcZ#Q`l!dy{Y&8t0M<<2#Hv{VK6QX;t9j>C+lOXCqQS_2IEx; zAH7T-+#!D&{#|wK9r6AO2qYY+!Kinh)7R)EW?wh`Ep# zK#I>c^Lio)EORU9*+&xxj7+)r4$k}>p=d9!@+LVWt~0iZq`OtwU8U?K9iOTYr5p;> z4+?aQ_2yZZbS4ya>DI^MszGHk#55WlA=U4Dz(z{DkN zy4E0>;n1GTYTN4xao{%FywDn=0AX@W>mBvcwY(_)nk3e5^YyC~K=Au#+0Pb^1*=E> zi*1dS`sa8*z01?X(9bT18&%G<}6C>Z6;7ZyVE55JYI9_9tqwd!&;(sc_-KGLRMnZ7So%<;iBNx2U9ohf+0x1 zHHP!*M4~39meVDOb4+nbaN-?db~djdiLWGh*R^PLmx}Ci>IQXjPZm11i-#?s%_mG8 zf363{V)D~o65}%BX2TmVGk?{99I zNzKcvICj&(0jJ-+UeVCM;dv{+a1KAjtp0~rNQAVZkM@U;q>gA){>jPu?c)7dNI2H( zDO$?ws_KztWr!(!q7<4Yzuf|ji9v`~r7?~$K9sG-)vEtDbS+S+r&NF9UA;`2{N0O% zUJHHj7n%x|zB)?SRgR;o>a3c?` z*cXFlhWDRSS?=}xA<9F_)#aKPx}Y6fAQkPdIR z!e$LQTmje>>WD`wmJhJZnX|hChUV4rOUjV)e44Q?T|OI|ohN(&GgsX`N%3M7U=<|? z=+=PNZp*S-E6IZDC0*?s?rjgAxj6Riu0B48bw~MQ>$EP-Pj5ApQ9AgsCKHMfti-Br z40Cs=*B)wH`l71M?t6#sC$&zR?eguJ7xfNKc!<=WG~L8g*@`Zo^~Q3!@#wH`>Fe8cdFjjTe?2!NP?(%OYPi#lZ(buOxx#_RU6fv1_ZfQmih zos}!mo)(6#UR3h6b1F`+m};g}vI_%J7`$qXdOg_fuLG_S3x22wXyhMkayr#2pVJlw z?|JkUb2I;mw`gw@^7fD=E@Ye2W5h?S)YWs;IllG!4pqI+s@eia_eIs+N|jAe5YuPE zD4=Vjk32ju9ZMHcPRG;I`YQiz30H7p)tg{;P@1?y_(P%qK`2@fi`kzyFusP`6uLDi zqGNx5j@^T0EtAmAtYygx6AsoIF<#<^R?hG7QP%1yYFJpugsvP0%!A_9gO*9~8oy=C zVqe)$EgjaVQ!m%IE7Lf;z(hKF(6cKD$uQcHNlQo9@j36)E`{@1+G?k3SY*2Fi?gy> z5*b+%3CG#z+oywBO?opr5FFrYqbq;NTa^e;N*Py2UHO<=S*K- zP8lGcG6;#Z5lOQ`=SE6bbp@pA*VJY1gtCk~e@M97kl<1`O!UXYYEMwdxt8QYDh^J)F(H4?Lgyx6e*}Ad+$&vM_=cYh& z@nz==ge+PgO!}dz>Cnzl^+Umxiz&=NG?H}`q>)Bp=TMI+p zkN0Y(n-Zbmto;1q(70o!fOUaJomF!INEp2eE13D!Hn-}Pe;AY!@9(J6lLvorn!2*0 z7T%#sWYqt}BQ;9x<(W@k{cT`yrbhdd?+>HX&*S)awCQ)Sr1W3HFU$QsiI^n5yj}C? z{m1t@&i|OHol3vA=T6%`e`aR0IWO*S>^o#Z1OPUT2Q(fu^GilY0oA{~WYPscNWSD@ zUVk?$_mnnpza>VuCAq4IH>V8utN4FW7v)oZ3M6`Icou&ORC$K`&n z6nR0U((q|5L2|1v6t_L#J&#@@7z0R5u|dCW+wTzkzvTAdQ#ld%Z{4EK0^muGIP116 z?PRFAq^xJ%pt0gw5bo3b@LWX>aV454WAhQmUEIdni7)c{@v!IZcxv2s<7u8hwaPK? zncz8_7;YnGRL=g-J&fs^1i zGqHUfqq{B+a|i}8F5~Eb6zm9p$1(p7oL5zA?cM)N-U)u~cR;wT>+jH};Y)J`#iWhDq0d)s{>7P|MC=BcFPr-Iji$F(^iq!`JGLPn(%(+@~cQt$XI~d0Cl(e66|^N=fOw&xH%6Bblu{ zUDQQA-!TxF=y7$Yc+G39`7?x(^xig^dBK{x4@PgE?{g%W+s3&{n%oJ@Ktxn)0X6)* zq#l@EGpvJCK!0-18cy;6LTvWjZoB0mCUn>hqAa9^CwD&xE+qa^R?!sGii|>(qYCVW zu02`c9UAfi=5l1dwX*@&k1ZS|)R8^QF;#mz$E!`ih{U}c=s$ey|NLMypPO&^j}Y{X zQT-$Pzasms>U2+Q_?H#Zwj(0GxfU^AM<4qHmo!u*mryH9K&UBFXrSS*gx@nd6 z{MW?=w)wf0w+MntHLnl8S%mO;x9;8gzFF0$T{Dws$G-ZMinMxan1gQ}4=gR)!`@3| zIv0R7e>>BHL3VnDC{SL*GT508yb=;=Q>a37nvv6BtuA>d$LccO1^Uls0ig@%pHH8f(TAM@KD&UPHH`1`ga(wj2&&u(SVuTStSvtOXxv{qy7XYW zi6Uv7Zp@5X;ag=O3i*+&w$D(xi39Nc%rf+|XU8|DheNYYe7xvE$GX-zh5;^3DKQO+ zdu;V>68Dn9AnO{?ox(i06pV>2_xWd}Ac6c-pWCD%ETpTxh)GX5ErR;I$qBU7)0&ey z#-;>ONWmsDr|e>5p;5FRIJDP3d|U0@WfB@=Yd(=vR<%%m!G={S)9=zZVY1@ub&jh} z^Hpqq;rRSJ&+#CTmQQ5zCJqlpj;5RzUW*2fNzo5PY`RYwXWsqr#IF8fD~PY^WY{-u zk~05kY&{^{i5=(YDD2ri67Z3)Z#;84d}CM(0-4BpIL-io?dz!Vo?rO>T1=~X&LWBOx*1;T8F6EMoEJ#mvLRDq5(FIMkO$Y9bF997lIdN-OTC^)gHpBx{4 zQ9qyCyl*fasR-(dQOjbrWEmHw&jc%!AG^+2IkI)wHxlVm4o$v$SH7?HwB;pWizpfm4)G^g4-f)PO zU0x&zS}w58%6=F=c!K|65RqX^%W*{+gD+0 zbsLs>Mf;*r_w*$ZlX643U&U(I0 z#;PFPOvyvwVNgocvulISVSSi|Ne2Frd5}jT+yq=~Uv&GlpfP4>3iO&x%a>{Z-=G0n zw)gbZ$G&_Rg(}K>x1S2$SzBFUZZCBS51x!Y`n+5*Mk@+3H+0n~#*AtjH+g~1Ss|HO z(R91);_U3qp$ez!2FDW#U*_)H7Ur>zzrXO7sEli)yCN_Tdn!!xM-OIF3MBVDf3|Vp zK#}L*RNNsdM?EX=rU6w7r)L`FF^R2X@ABk@x;4sa&--DYzB`g(&n`FeizZn}3i|+Y zp$LNh-fPN9>n~Qd7H8&P(WAzP2%8-NQBCIh-j&`5kMGaYI3w-?<0^^KW%LvTvG}T~ z40rDINBl6KWaeV+t3Tqdugt~v*>N4#e+LKnmwXXI+F#BvPE76ZpzAHBLCl=+oqA(^ zeU+o)vLx9tW!~tLy{#r!P4^P)!a|n5WMT6-Gj&g?v@27ax0HChG;Ks}N+1yeGBYg@ zjw|y+D6qK^UXi^Qdtk|*QLcJ0ch99(>&PWO<-i*iqP5jW?>Mp*q{7~@vVF{vaMR$C z-I8~s%5py{%E3T?(pH36CqDz0s)2dds0Mm5^PKqemfr6oxqh-_ZrqP;ZI z{^shVfDCsm-9SK!YqK*hKl*@HjM{C2n?1>VIY7*AuI-U0CBaTd0_LOcv(J;>Nd-Ms z6CG6_E8UWwI}@+Ai_s zdP^#bpTEV#x`W9v&&pdAObEyqm;I!L^!aYeu!1|24FVC&(n zDt#%?Ys~)8C-CWE!)of<4`$#EXu;oec(7 zk_BZ?|A_|{WT>?{6OP{6;DHw=6|5QB)h(7E+DX(kLceifi458=w8NyBLV~BXtt&4g znM^NC$;R_Sn%_urH{=mzUyh8NQFVs%UffJtD1$oFJf_zBb_EO~6L)yv!&>?vRo7UP zh!!YpSB60GbnycDl5(FdUA-cIdumL4`CAlP(RN{_tX1+rZ(tv&ZR7Yp1JBcr&snOq zxenlW)NjXk!eJ@VGCnb(jsYGtOt*8^+|*a5skZ@Oy&pyF5uUIBrbq7Q3y-!7hmpsj z*S&4+RN22L= z;GEb>BOy|$OkzlQn1KL3=$Uw_&c{Eg>;Nq<_iklAybz3A6LDZsnS6hmb+Uy9Z52MF z)){|ztGRcRJ{A6D@V!lDAu@UH{%r-U*r>i_Tr-F?vXP{B{;A&$r~#0AJ{JyeHPVq( zS&xg|C=Z|%5vxtxS~q=HVBkJ?XbAK|dyRoU1Z;CyO%aAuu#*t7JbH}g%FT_+pjZDO zsLb%3thSco2_E_BC%k9U zkIIpj7)@Il?0LxgI+0tnToUAL2s7IKn#resUhz@2<;f~$h$^B`_hX-9n$YtfvlZl`x@K{=@S0?BJe z2CC^EqIbikq(8e)PbK^S&Ucuq2Lf&e$GAV9YPx-&tiJ^N7F=BPY8&EjXYxEs22X`R z+S`%)q^HAwcW3(81hWN95U(LCzgl~ib0t2N|wK+8&Kk3xicen*+ z7;=-t`G4gT2YjuRQClh+`eUtizN4|druY*dlXdX9)kATS%hpD3O%r)d>C)xt0ctbZ za}~7`!`AW)Zhs^<*eU`a;2J?!N!gCLEp!CEw2{|2S2Um%;e$${u)ASFx2c~wkW&w^ z2d{O3st@oQ4MZQFPLEX`rC2BVR?aeYBK#W=B&2$d@AM&AsC)#nAPQ;f`-jTX4L*ln z$t3F~C3<=;u1>;$bScc5@|%IKV-E{X*fGfUOeodWQ#Mo%4byuyM zIm9j(hO3yE5EW8}1)Cc)FfoPwyk&1zGkta!fb`!<>R%uaQ! z^+t_hZzmCVY^Y&db>jV>4+FF*n7jS&-!I{@E3mQD1Lsb#QZg{Mh_3~|3EeUkE$isC zU24~NYn;-HT%Z-k_l_IfiY2&5s%IwIqc`4A6D~(Z{e6P&`jlHw=v-$wOMX=ug(F0k zf&f?U=BWpbxviKNk3^Oi_=l^sYjj&HH9i7}-xN?%zOGGVd%&UeY2cX%&drYaFzoz{ zbtg`ge+|rD$s!KZszGIl6 zzLCa7%8~}wckM$=VpjZ^l<$jEQaw+to?SN23bW3&@Vsjp$JXWKt@MFD;3$^_3 zN>+S2Ou~-W(ip3I77yfx9*+xID?Ql)&Vuodh)U)a08RU}`?Hsm%$y)LR9Q(<4wtFQ z*mSd$s(7z>Z|_Fpx_nh)H}dnCWAUI{{wNbiyzJmaXperHkSf_bMHjualc_;NT|ZPs zp-9C|&;@;zo4VP7e={q|tB}x55XJ(-?IfzoAt*$a%44E6YtH^OS_zb0&Mw_2zR~3! zk))qe?|h=D`EXLH&zksf?;Pe~_%uG2oz3Mg`X7fL0*q@T?IfT%vpyCm{<%I} zNWR`uA6i^kvlvR3a-KXnXfvY?-1ru+e5za0$En7Th(^O$;J9odXe={`i#P5NoDX1AEHG%smE5 zmg~k&$Iwd|eextfOJ*W51r#zzrPx&7?dyCSu`=}vc?MarLIam+NWs;-olWYyCl_KS z-O*nqhAkvY?*B#d8HCn<-tj|E5&OGCk^S2=fBhX6{+9wwazszHFNOcm`a{v)h~`hc zU(sL%0~3bgBGfc*3{@}v$h4J>y!N7672O-2Uexj3KfM0LGnb@(F8QK7k+?+V|BOA& z7c&$xG+&fOuxl~HF6MEuLM{03IofaX8T>-jmHq1EVE&h+djH$myhL=!Mhs%?oPOH- z=9R6>9CIGtIW|jkD4*@oHnD?1OQk?d_U9x7hhY9t+rA|SR+3T8JUc9aOK;#rHlks3fMA?;ZW;{9bg^-P_#jegP@X6B^gu zkJ1JuhFK_Ge9IRUsoa=H_qZ_uju=bF6kTDcbFt`UwB*o7A2DJzWlBOjw#=mY$ggL?6BDGCl3H|-WbCX%{o9?Wcy zef==F6sPQ>_O_r7!WX&kDTfpOc@)z8bS43`tH!vzk`$}VPZov^%z8I~iE|$6?~!9x zzaOxot8ZRZDbie3ijz2rzs{1?3WLn4r@yh@E@EUH=-T|~5%~rDY_Nblu4^MZtKt1k zJe*F@tHASeN(L~YH$1fM1}R>KJo=f9-D$^1?V13onyCO=kuxN!KZ-uT#5v|=4;djM zlag^LYo+a|Pv+H9QAx8rT8fHzY+P5>Bfg-aEFNJT28K$d0T^)%4$**>`*mZ0x=1hFBiyJ(kNSBjEuwG1w- zG@s*VCVJ12y!f8r99VE{eWSG+pM8ylt$cl1hJomQUGs11;j+04RL#V@Pl@1W_&=w! zaQvUI4F6i3z7WVStN&ZUc@=kL=d-nNC+E;df@v9o}E#tviN%M8ZWoDf+@@z1fn_aCVoE{q@7i*{~#h4ZrD*^r@rU&}4LA+*3rJr8(tW;#7RRvr08TUaAH`1RD3 z$j(|%>3WHj{QT!RzlN!)sJ%pja&l+b=;#Of{(@R7CqfqSl{f@h_3ulCN4m@gKpRge z`XgUQsX+HJ%GBDsb7TOs5n|!A3_l}L(=OE-qia$!_5HhQMoCI`Zl0B@(kcXKfB&D~ z+x=oi#-AP51l=OzwDRF}7Kz>vCF9D#4wKW7pD$|wF4Fyuc- zp7AoVAj1YSH2He^l^YC96~n%aqz*sspA&iFDnr_Q%;r4$wE6h%myyt#Ux(;El({|s z3#zSOF;W%14F-IDbDV|(IaL4w!qP4KL_H}WpNyA_{?t!Ij6T91#hoVnY!e;)wl%l1 z`88>q7PQv`^QC^aCb;wbw$_Y>j^Y{mu1JP|L)3Wvu?s)-Zi5TP)*c z!+)WR8q@8%$&Fm^H>|Au`7t$ZN0F1-msj860A74s*)|NLOrT*r@@y#TcdFM|EF;w7 zew}~)7QbhMcl?V%)})EQ(sc?{f7N{a+d7ATB$@tidnCzd+M8p}gNp^}8*e(5MdF%0 zAt9tBJ7gP85;X>A6#3|JL$iwEs9cBDiddi3yjvPuLFVb3{^uT+Mp$lDex^sBxglB? zIZ01Uik6)`uQHtXpC?8I=3*=lPTn}%tsITb+F+!Gob)ted^tN$>c=}bj7Yth9?9~` zjBcWJ4d-pMlE%2xY0YiXcq(_tix>bZE1luM#d3CsIj=_O{B+|SpLs!I(x}qZWFK`V z`&r{>LRTL=DfdC|9S*WpUhG`bhck67ByqDf2O76iR@dLtFbpM6O?oI+>)vUmj&vir zN!owuvF_Y-m%ScpQz0;FG-AQSpwr8;u%^z$#1xM1WSIDHa>BtLG?QM^*ojV64#YM# z7C}8`Q+&dLhuonY#LDO(VezRaZCZly`~1k^1+hFcF4ks~m{!JZTO>kd&!*V(SfJO0 zovB+a2`;-fn^qE_Tsiy>N^#+ol7pD@tQMLP!Ld}rm76TmYzXw-d)^v&2iQM2HmnUj zm5Is;uBSt{GtWZThbqIr$ zE#w`n&8ljxte(|l$d}K!rK;k!Bg(l6So3)iwVi8=F5l58L@?X%T6qRfd?t2pvjG?~ z2hW1aa=|5si3ykzL!hRxb$r;g@DllWxa|bk)0E!56Oa1+df6En|0q@zv*SB!kKJ8v z65@ZOY0k4EsLtdis?<>CE$4y{Cg`GM=ToMz3hQ&y_t&OTB2R8YU8{CYm0A2UKn9uTV0>1>d_{WF9~KXyWLs2Rii_IfCVu zL1h{c5CxsyX>pmAT+k4u^bHbMmRqu}UVK?FY{2bfmck3!UYtl+(+b^sn=1aMvlX+v zRvL?pFM~xhlO4!wHq~z2LDausMFx_|RJ@)Zd(A){qb`zQYg3U|$gAa+w)nbU5%Jgc|*N0;GA&D54ZCvHB zWj+=pqgcM?u)yG$n}{ZU%5{ftJT*q~y$wCR^mJm4`7nP{J)d*R)Z0e>5p{1b1OHj- zr(XP4HcW-VM7Y2%B?Ono3N2|}z`*kp-Sn?vO-094;aiF_(iO21gWV$QwVoS^UO?Y7 zrf(PTpcs~nvzkOYqt-?f`Xi&hnWNGCPb{Dq{#8nG1PKYedT3AQp^;JbczP<#Sm72+ zD&42_fU3J|-3z;WL+(A-W@`@8_rTsBjo+BV;t}IgoAHt+8*yJGMrDwx#X z)y<|9c@-N~SxEW$eWGw`@^&0lzet(?qsftsiVFAhKQ(t6j)JymzPB%>ix{1EJ*xEC zGKvFn87sP`>0gpO_xgSC2fEbj*9n@mt?t_U%;%GN>i_?Ve~sOLwc$|&-|VMzhn3>C zqSDd-!JF*g-d;{GqKzhtzYEG;jX;r!H%{TU5&Ittn|cY+@5l;g&4OtYNEAHyy;%w5 zE(PN!cXD=}iBHcQ+#&7=j%5-Q@VBV4-?CVVN&UWvpQW(R&{-HtJjS zA;kNgDHA~nSVO8xNHj}pI z`_GMa;=`4UNp02uLo> zjQ90~v*KL8Z4jqj3uYLGB@5*2jU~UShVSC%B%PWT@*V;COw>4Q->^>+Q*%(>Z<O-R`Rm)J%;b^;O0&rm)sd5e z`_^C4QpXjskbpH}K__3`_$UeK;$k14TY4e(q=3rRmx|if*i=t%ct*Uw*33F2B7q=i zOb+a=%+IrQ7|98Bb-$d)+~*7{ho~=+3r5QX^~8^md?a4SlRSSgSUYLn|L&W4CVAZ4 zi~Mwk&W=~#XUEWPY;8`X0~K20ER_ZHcP+C|Z#nN2U&imBjvOQ|W{^ybRgD`OG*o?L z@$4nHy}&akN``v7oY|K&nN}DWitdUgPkfjae~j1nfaI0$72k@!OG{d9-vETBxeSmm zw3bm(5h_uXj|fyqUz4~Y>p(DZU596`1_YfL_faj&!R`?ap$hG}>yC}0uN3ot5fB@lV)w- z=@?Bbkwps(#LI4iCieY>-tisS_dO|oF>B*c;WgFoc=*{s@Usyxv+Q}H%2m&Xq-9D# z*pmwdHgHM9n*%fW-3~oF4l5O)D?w;VA#~!9C zLklV>DZC!MwF|H6dEhexAB>=%mRi-%;rcAGrltRW;g7=^KJGw7sC?p?)Z07z(-PJ!HP)!UQMGx#g=dd`E6ABqXGF#e;c7s>HdA zco@q3e^K||(Qtn6+vtcW(b9B72qH#{ZWt2L38I%FB8blDU6_QZqZ6ZdgVASnQKI)A zqeSlnQ4%6~=TpDG^S-}z&ROTIv(|b4Fmp5aJkM^Pz4vwB`?@%b?Dz(8WWJ1VHM&)2 zXoo*9|g-ldLC0Ii;qDPR41lP?H>r+wD17jXBZ<`V&kJLDQ+_zHbamISf z8NEuD>^3Tn62u_*I@_FsHc5!r^E$vM`OEMx`JUF!Vo*CCtGr4x8~c)6qTvW`2rrc- z;h?YL)9sWEs%1J!`^WBSuLm3XKD6%CLB`C`g25s#6ixD* zji-b%V`k)EzszP;8mo zI5uIRH4qKc{WNP4NXpI!V_gage2%_Pshc>eHQuSNLIKlx+78r`ADvcgd&D>@0RXfS z=++tW>GwDGmpWF5B1$lq{V3G-EYqf^%H%nK9AUDWkZFzaC4HOzx??X*fcfKUtF6c{EoT$R}wz+h<~)29z1{n1%9x<*jz%n4b}0yHI_KIpT# z4GAl;g>w6PyI$>H;1KR1s2H8hWW~n`rSQ0|^sH?bCrea8e;9g$9=y>qsm7vvDgG*R zh$h#MuNwMHo_;G!jVarlf&F$|zMHhEy@wTY7HXtqVC0lrT$;-%!p7 zHULaIksZ<7Q|zBMDXQ0ZSU~1B!sKfP6qvp|P^8LL0wPGHRoDE)Ef8vct2I#iOBi3C zo2>=$2bNdDF@zf(r&_#Pw2?MVX_a+jkvTE0_TrRZXniD)N?D?oy}#`d_v)GyR20uf z@?zq57F91E&YP8{f`KZ1eY>VYoH2rD6OqMh0ccd?AG#eKhU{)Di7I+QM5kgr6^@U7ssB3@vE@vAtl>PN!uOL@NdnrucFeWhuXlN!Uz-J?>tN~ z!sbrr_5=JnCehauT{K=6WX5xKKbVL;Hy`$dmIkpNwd;yiIOXKlEzUU<)@zDVxGL^^ zMx9P{O4zcMgLfoGt`U1Ldz|$%`dQ)gQX>SJMIo;waFRjvCJLRe&mx$dMveBRcjH5M zIgYWpg-TV2U*CszOxNzLXfS6ozg#8P(%~JNm?@tFsd?UyN48>ZY*V%LcO*QpsR#zD zH@Bmf(kap@mMz@pN*#&hRuJ*4ChUb)7Hj!$p$z1~Y-?+_K69=EKbX^M zzUL0?P9?H#*0cMISG|%Nk+lDP?+j_)DeCpODZ%=_s7x*fk}x1C50A#Q_B_9OE}n@n z!pOpXRI)nDoy$Ub(x?X_L`6zM5|1Jtnvq{fn2?q-v&jp9lwVJ8?wGzdhG1vM%P4za zf~(GvrrDTzr`K^K1#rA3OyLj&bJOq1%==p6s2FP3e*L=!m{)V99vda04{Z)$3MR~B zb|CH(1s>%1%IIFE1W6X*Tev~0-`v(0`I%W%>7c7#rzTDN5zS=Y39(&JAmFL z*Ya_P7E9>kYLT1C=>s;IU^`N`+6Hul;;mtP%#4e(ac1@fEK#Z-Ws!50oS)F#^C~KNuhflq#?=sW1 zOVfH`R-WF(h%;vNh+2!e(R`@?gKy%brJ%|Si>yjYQg8Uw-?7mtqDgT_L^^xh@5Q(h zRi$=iihxm*g06tRmzuf=?ZAhCM-he0^OL6(lV8chiOMNiEh=v*^Niuo1i=SHMX2)8 zXjDc;T; z)9LTSU{-*TM8b5lde+45EggBMZtJwh+xez4)B8KYV0!xtS=)M2mj-un9OtW=&GrP*I6EvyTg z`c$CQTJk@#uE*J?<}~zvFz#@5LAr#;-A;3uf*Xtp5{TgI*>sm zR@9I)+ijXcfiQfzQt))=tk@G5QKH}Ncy4%4L;#tnsa{kNTBmi}Wo6F=uvFq%zJ(WD z6XZK+opH%63Lmt`D3v|dO{>a2oO&B|xV#OuMMGs>8jaRIdSMy;pi#5=abG>;8_5aU zZARxM(k+klY_YEnhCX((#4GcM8H&$F;+R zpLMWDw^7*Ig2zdL!hI@Xqf`2n^>y^6_n>Mnb&Pg8M!DsbRPnc9CWCh~IVgZE?C{}I z2b9e|J~bZdG}*11qulSqUBi>l$L|z-gNlT{mn(*770(lmO_t2%WfD;5K{0EnQN++t zN%VTbMY^AW>0Q=md-@x2{Ac!hNcu(*H<>HyMBNxvyfi43Js zSx4LH7ieQs0xJI#R zvIuRC)DZ~_3qb6Oa<_2YOvu(}fywhcJs?a^TTi=Q$k(p8lOu+mkK>x`bywD;;eDde zOAKu%f_#L(0jdgX#HWZ=;nPohnO^fum&&b= zk|c6b&TS-!@I4C`w;;8`T!FkoXvqKRGV6YhZv%Epadtt?NPiQe*lT-VNcz)N5Nt!4 zC6R8-b=A>^*$JM@sP+yctSb}yyqazNdKuA(tt~4BL1sXt`<#(#KhD^23y*P&Xlb%r z6mm>dE78{Wj>`gNdjWB9O(NHR6G;veOyE4!W*3wva()A4v$mk8-$`7|`o<7H-!d?? zN$f(M=FAji&G>*KDNIq7inDf<6ij1;CPL$@af2=Z%T>f(-V~}hI#HHUEU0uZCcxB0 z7x7ksSH#+TXw8u=4`HGLgUGobL#m5S&VG@pajLuPrzPxgmsk{LB9uEh!kX)dK6AW$ zPjNdvo`mAiDt;MveeMrjIgo5jh{;yybbcP2m_B0(MH!~ zH|UQ~oTx>!+Y^W+Oz9k~qS{7++@>Xhp7cJeh88OHK|PP>%#76Ta`S7+>IkE9OOvyn zQ6=6HA+b(v@1cNgG){i_u;BDb{`r50kb=Gpk1 zOCxkU7t=qMn^Tc__xVM@&nLhd?>3V~4i@GQ_}$K3xsBQ;I@iM43naG3-`9>s0vk|3w~}aPhLZMgS!Ag?`F0jK{j8$vVTQ`?`hiDGQC7_vR)^x)gp3!lr!_Eth4sdGKJDpDGzWikUtaI^m#T)-?sOEO_*V1D4Oc$?))#MtsPaLuSuBwh+0_SKMa$w7cb~<&&hk>}QHL)~xH_*gu%;?0bR^ z_@tOH&Y{(ke!&40i9-SeU)>&XY}+1Hf-k_Ro~{@w^7fV-hTNAHR; zV6T-;wZHY9p~|BDSgAy}V2nlpxq@Edx{3<39zI>0%t$cTmd(xZ=MlXNJ%>Nd0=6~AX`E8a{5UIcy+{}h}`O8+!7kCI_UC(6cie@R>wdH3{*HRol4n(rmug! ziWfN(5N2 zy4WH=G3Ea8`@Pd`L~2E?iSZV+{JCu=h2v`1yaW|V_@1$?qXA>1iXF-Zv{?`-)t47C zZ1G6Qb?K3W01A(S1TYbdJNHQS2g4TiZqoULI#>H=8n^20c*&Gt=FD@_KEh7S^|bF% z%qVtJ@_5vXkbb<}$mt1h+rY#W96d5mO_%%)__~TTUwYXksPl8;`1=BL{618JD-Ua+ z`$YD>GHUT`$!&#Lg3t3(uValvY=^9|>K@$zn>$j*go=EtmPu)GPQO!o?qH}Wr0&L$ zrHd|cFw0e=*9QorvFS)K!;1n1XMe{e_cWe2IT&4?MZ2P6{*qfa^4#Cc2X}Vc;4{dM zSwfmkdBT~N$A7g7Gv>o!BYvA+SBd4=eFf9_2=IX(^}a=TQShhO4bjmRlPO zn7z0ntP~*YFc7#-(+0l*eW%JIv zkrd|*^71b>Km0ivnicy+z1f={`3>m)4Tw7Z;S-pBfR{b2f#PY(n0^V*^j%qB-oE?| zK>2;t{I&M$%X$R{)^VPU7a1P%RLCY`@e>nxvsA+YXGG_^5rSSzUF2-|JD7!6ewLrEOIX^hyB-Y0KR_m|8f7Ta`(8pkH5X| z9t%kLx2XSd-j#{mSbzL>?fc>j*#C9ChVL;coN126{RX`Gv;F?l3S}1pX^FpXCan7S zbgtU8_WfJs|KjbDrSQ!)tNqyDSwQ0d_YHLS@7DZJ1DVy}D>f(69 z@aE(Hx$8~;9`W{nkN7)MA@~vX5#MN90h$(j4zn%kd9^)KMUE0)yCw^gk?AHHlJ)f! z85dd4mX5w_y?-w*etMWvnsXA!brU!&ap1>;kKACk*}!+7p%ddRtF? z3fQy#oSv075gjU3&7{QfTg z|C0;x>z_Ob(fO-pa|i$42}J(C(+#*sQRl6ZL)=$s0ey=X0wP6cj^~_D=gULOf^?-8 z{_4YZ=|6kn^`E`4#QoyUzrFMI-`)|B#qT`y{_S4p2r>D#-Vf7{TmX!so^ zGve!B@sk_>W*NV1MdOt48xX=Z`&DQB&EX$@$!a0*^e&Ws-TgJBXV zPCT#BZ@{0vcrI$%uQh*MBeV$roe|$?C%k9oNlq1Q3*N#Pboy)Lo7u0bari&1w*FZ* z#iK8@e-t~38(%nQ&929L=f#Uz5qK{H-sJg6jqg8o@bz?#`QrmTgLVV$-&MxX3Ck*H zeiMH~hry6)VEYKi)E5nugv z#nn}L8J%;#=}Ltal*+78UN}GJFl62*DL$yc7dT54YZwyyTv?l%h|N+pHb+^(l2+|j zwNa9?5mA^C5e*M(75+6=JDdBtY7&{QzN}>xIp-g#U9yH{5nZ}uA)HUIeJV@leEz)P zX(gMic6LeFbKPQ47>GKFGz>%*pPdcQQF&e>$Weux9du*PF|mJ(1WLe#Xr5xnfD=)t z;La@<9co*9%B`@5zi0+Q;x}0YV)2t@qaM%u7`om!ea`Q3Dzhd6bdLADVqWuP)C3o9 zF^U@jejN@I;xV&TY3~=Kx?jagc&1uanbNQM+CDNFQfQX#WsSNe3+y<9xh%JT>Wri| zk?-+19{&y4AD}jQCKp}v#XhT&Ny&hDys&Upj{3q^kI6hUbwB301x{$@miHz-Xi)s} zT?#rl7$ZBn@UReBNi6b}+}r}`hG4l3sce*bFfzwGc~@(0TaGog>l;bKID-m3XV<5M zPK;iqgH=T5i;Dp(s-5iGO7OV!1*3+VAyC=o$D@|-jhYInJz#%(<*%K_-*tCs-kg_a zn)uok+0>gjNvU8x7e827vB4$9NpW68csx8MWZC7gx?xV^v#Il~y@kw77s{(C=ZKwU zCKC(Gd$qmY1aIEHMYNJ{xqCKBN`-iMh)9A;BblFHX-Ez*6*S*yYg@)g0I2YhXXmMu z65|I-ZlzACMN}^QK*{j7BSjc7UJR$e-FwJeDz_FfX^#4sKm5@s>?j`gYQk*2Si!wa z8n0QO`tdSzP7Ne)4DFB;Mw+Hu#}YDh>|D#8Go^2C++)Kd$ZxZ$yG=D#>0Mw>!U)Us zRnXxJ78wZr@mfT2Sk{-88H#64y}Ze6md(fGPkVWv_C7=z5k>SqEQ2QzsKRxNwR5Y< zIm3*&S;HbgWL=i}pln@k&@Ea{%PJ5&1Vj@|MwUdR`xIUvt1J6FzgS-Oxo#5Cbs93& z5G^8Wh0B{)&Oa4i>bsj&{s!QM+&<}c%^EIPQ4{f`{lO3a@FAOa#nH--n?HC8@Wv|M z;5)ojkrE*ST|4${{dUDXYmeS^8TX|W-Vv8Pu^+$DT?X{8=V?ltC1-6Ft}(@}mz7rv z+jwf156K01BjB}qV%}>mtQ6n*_*S0$_+cKPzl|3)whT{!7EqF}FEiQMG2DLB3slr) ziPfg#!9cxU&H82J!gqBF{n9(=YiVN#x5Nz3h-g7G!L5|Lzs?%h*K#?u**^lSf}m42QNLk*?l9`{yx}v5o2Kde>iPhjuWt zGL8GD&5fWGjLA7SCl~f>6OnVMw}6** z9AO08(a(s7dc$3*4GOEt@=CYn=2E&93zEFVv?iCo5NsD!qd)%{&BbZ$*@7z@3ZIG0pd_^N z{4k^^cYoTCI9EuQB|a(A1UR}8Ypf^>3^NgMAKucH60Dx-Pt)s|(DSeR#6f*WS9Ij4 zQ=?F{eZjgWo~Mf&5IWvY%(?9qeHw|&g^h*6dw=WIEu}yj|@GiT0 zhsQongC!M+8=Oc?bfmjman(hTTfOV`%)lF}1`hf>!;N3c&Qh1*yPkDp>f(E=D3qns zo5T0FGzYQMrhF$tqDh#^+OKYExY>)P9!=O5ov79G?u?9v8skEEDcwysa=TT56c#nNgx+I4?AbS&5eL(QBNd>w?(?w$EhbL9A}D_NExkoB zm*g!xH{h$jgsK}52DpXfBEL=r-liu%7>U3nvPEvnfbH?X7pC^7B3LHK zS*URl=DCXfOM+VGLi3PpVAP{%$juzR27FG@LT&+K-5fKeq}D=IQYtsfa6tso-J!te@DT|jNbC8x|l*~di~s`9*UQ%btde4%d6Pjlg^ zvUzbqqO5a7wRQKjR_$$;-7roZq2SqM4LK@>OA6;;Haj^7wj4 z{snXTjIT%tfR7K}mACTm=@~|{82{$K=ZJl()bAGGrcjaWz zDjRPdf+;yHLW!mBOdT?8?JrFDRqRjFhOeOO)M{RZ>2-U^gUDC>Ev4c*Q?sr(-L8hq zQ>eBs)he-!#V?3^wbiurIf2X&r!i9L`UxsG-06i=`^#E~Q{nAB{eCI`o1R70NZM|o zOH?`)*SP+GW5k$4SxG4txAkzR1W^+HM<&_^m`eZ#s9#Iy!MyHSNfWI!6??U_JM`6Z z-yqbWQ56}PR^&JimJ8-=bc;(rw%r{j?cKjDkB-MNV{&HC`t0(XlzU&d>~kC^R}wJ! z(+7J3l_z>Vg4b_fsD88%d%fyGCwcWp)|${w<>WYo4|^O3g5%}OOhX;m=a%FKeKiA@ z*-}nZlOiE?>Fe6koAiAywp>AKddB#%u^^RNW4*5Od0i%d%3V6q|qXj zmRRs@r3eL}RLjtj+CVijgSiBrxvou$ECG268OgF{XcBUTk%aGPZv`6QDd9w|QW~#c zO>9OtS>kZ9zR$`Xh6p~E&Wp>?g=EsN0oXO@QjG|A#566xp7&pJc5fvGiyYsb!Tmnq>!l{CNJ8kq-(W{V2s6`I;1^nt=%CZGcW+Cq`= z$r@mXJU#8Dn2hiCF4wFd2S#{pfFn_`zB$u!&4sJ1?9=FXA`^X3>o(I}5_gp0P^we^ zMB^gph0!zX@de|IHo{eFE>!A{$+=(Ij3o2Nn$OtKC-@55sap3F}8rmakQ<6Uy;aw{Ag)JWGv{z#K zpZQ*mo0g58=sQI==gwOg6d{NC5@tW66VE>H>E}2XCF;s)jGh`%2 zR2co36Hf#Kt#EJMI=NIat9JL$D0X8@vB}`Ew0a!UB+`{n5o&{95)*d$<-Oh#OS6-T z{#-G2_{DL2&aQI@ADT0gV!s1^%XO1}bQ+)A#ty&YsbylXX_C{TKA&U8u0rJ0IPlbp z2%BM-c;Of&P8-Ah`{kj<##;({a;47HFLogp-5}KCjLLfr zvbs(x%g1L+7;`r?R^q75lxV`z({tA&V%@C@3OcyGs{ZVx%>OO0`Mz*xX_YUgHt}>a zetf!}MnPnVH=f}GOHb`LqM~53*PjC=5$H)jqB^XaLdo#J0%vPVzG<0os(D%dqKL#{ zuYIIx%I&CVBSP}215^9_7{@c(Rc9PuSYA@u!U+_*NhA8q4e~&PIj$lrx`?Ze@Ltg# zRMc|z`-9LPo9wzS!9jFka8>x@NWvK5A-!6M*MqZ8R28)jS0$mCw&*KqcKXAck4S+8 zCwzD|Mxp!6^W9yPv)_8E;~o8nni}sfF9lX; z%D!^1E7E!eDaD-m#o6*V07E^w|N7haN{Mff2|>-?$uyc*&X)q}n8AEL9R1GQSwjw( zn~V1SHimAI%wd0nZI-QXGI{kOXSNw7D!eg%F}8x1<;4yQQAU&>jL^BG8tSA4ua`26 zKUzo2h_je{FHI5=NfHsrDK3B;bM-WOK;qsjZdfp?DYw%vze}z9etg!STAndz-k@fM z825B#S9Q+Sc-zv6*&SC3iG~bAOiuYyBnhk!bhN(<=%-;C@#3Pp999M6G9a;J2pByr zT*R4uOIbR9J+Qpqp(|q5CUnN^b)7EsB|uJ%Q<%c#=m6( zZ^Aq(pjXLQ$v&k@v6kY{*z;pdLYJsYTu5mRzBH0209pDOPb9QmM!Nk~P){s|#R!f|4iWidz< z{@0$KwXFD~#cu%mHz2G`75Vt{Z@{kC3h32dnNu}&2!XN*PW3+PLy#^teUfTZPt4lm zweo+AM%W+QuMp6+&8F;qb(yZckd8}(yM>OziebhuOTTo+>CMt`25C8-nzUJu;oy{q z`y=QA#|}kj230GYq@mSiVrvc<#AUKP!&@Bo z!oWN5753|)Z;Lcp=7D}KpU(1}51M2HBGwfsts0rqkEUddn;YYn^TS?UKYE=#mI)z? z)NCbU^A}>-g=*U2*AZ0HI9~TeFUcxJZj}8yjC(o%I5R61YgAnC%_4YCm6YBBOGV=W z{!HjY?!r(8+cYEPi{>u5yFmjhtDlnBKY5}Q9?PQ0s6&YV1Wi(PL;iqT@Pf1CG_hJn z_|0e1b=32Mzwj1q+1u2*G!eaJdQXbAby;~;@#}jcGF_f))HEQn;t(QLc<~=NN*FvF z1tKHGgDYgYWcVh}TE(~cKL)pSQuOrG*lcd{1QmR8H2ynp@1P9+TF8$7ZKP=2v*mgn z+pxHm#zTyA@aXQ+5)9JC6eJMu2)2|{+4dOrGBBr_-qLOYw~ALn$+2Qh>)DIkp7+TE z>djB7bs12oP8e10SiypCXr{D+bItkVIdYbXeF%QsS4*dURM*mBpT9T0y%rY(s3Fua z-V{$=VVX3U>F9k}F_es0kutb3`@JZ}@+D#lhA2@jW)e^>wF%5MV>91bTO_QDHIps= zj4s8T8>4ZKAJDbu zCcG-g>w9qsR|B!i^Tr$L2zeLsnzVd8q+o4_*GQk7)3t*d_KdalBUPoMx@`$zav5bT zeEjrnF5Nfn&W66&-h;uXfV9O2PVc7r&l#Ahwd}^HmtHokZE^)IB|@vop04phb#(Ng z4td3ep`cVA(8H2W6O-xIhXco*pS@Y66>Q6!8_TOncewLlg*aj@MdR~2LbqbbQdAac z6bnlTlDRsz)!rWEZV)N87I%qV;4hfhWNUu>>SWt9`Q+jLIT9yHYs6y}j*don{-RZu zGd#aXeZ<#G80tH2?l>P3zr=}4TQikevUOb8$f<8c=&xU%S1L20KQ;;$16jNw^#a-{ zHavxMD*KvS)Q9qWVhpEPpVVwAKZYX{QA=B(;tdX0qYr%3M6tT%r$@MbQOq~dVMH?B z;6`04rLDxhrz>1#I7Z}ELUEIVb10r1rCKm?IPh4=JT~+(gt_e-{R-FvrH{P;^$MgJ zVJ{jt-E#eP9RjfBrFqx~I-0G~AzkRCaodHsxOfs)pFKg>2cBg zBTgi|6`(Yy-L;y8l@^RIKjq9us2XoAo+%MQ_5)-XU$p&T8SUAmA8`4EF!yUB ztfi^D@C~=ma++)#Y}eqhoE!m90n6S1qhMYD??a<>;p^&+pUlKs_OAk@7uRX92yPU5 zg7n^vqD&P#AV6UBOr+|=Fmc%?!*Y`6x7wzs1Bg*_b-t=evDZo>863?j!q1*QonC#b zA^hn=!tGgnL(W1{#F4X~9CMRpmcJ!`YACG=wbA&C%_YJ7Q_Fgnofpzpo3;+McSgMP zF}kff)FSBoxV^z+h#-FRx!&wne0v~;IxMlyQkVaJjje#E77YG_sNwV(cEk+N23f^4 zD90Z?w0utHGTByA=Zqn@r55=|{0jUzs6p zok2auwa{`9-H4~}rI~bLr^D=yv2saKet-$0#6Mn%!Z9!t=pm89QU}ww!j(2{ZD$wd zFiD5iiA`^+$~h zk%ex#<*`aoY=+9-d(}u?KI{Z@@Bk*Bi|I|9fHv#p#@11pim6QZ?h)17;Gwo-gYq+4 z6CPGHyt)M5N=u(64R$i=a5Y|9l4W36Z>uojg(rhMt3@0!I^#OJQ|($rvZYq`FDSaL zEx`vI=Cf3+aKVu37^{b z*JB?wda$V(g!2z-soB;;Gx(ms8w;kiCBThu(+`hHC@k134^`2bLZ(m`4-ZqhY`+q2 z)B`uKXNi?{!4t(4EJkgcAqOGx$rYe6$D}4v%GOgsbm}4fPUiXDW_}XG5$ii7dE`Sm zpFg8^(A8?5jk=wVfhEeEej<--`2fp7O@O64u2Mdm#pJ97iJLZ9eM!bbcd3GYaz8Fn z%4~#U*xSBIMfcrN6)uf3m%XL3=+r#1Z%WO@3yiJo2xH;_Z?cvgIau-YEa<`Nr$(TBkQz0f+4PR9 zXOsj%1ew<@QmjQMi9jZ32()krdVtZJFu1`%ljxY6EN$o%+Aw?_*6V5Fy;%%Ol)B$r zUORPq?w8STY?DRtti;_aIEEff`LeHV#U~Rp!L7Yk1h$_Qn~M^Z^=Wb^^K7Wp`if+r zXN|I>q=MPVkh(~;?dk6^K?d}!)J-r`NvCc-zz232jBgoAl@E>QzxQygCG8W%CkRzh zIn`M)h;~#LM(n!PiPd7Jw6v9{u?7UuPm<$%ZUH#+t(lwk>KsnlGyGiQ{TDv7XLYcU z-ah!r!kp2t(vpZTNQ#zaUh(X*70SLKcU~<0{XNoJ06}jL3t-Kof@9C$gE~oMMy+dc z8CZpV;9O|$Wo!bRsgqL*B_+}7TRz@I@zCIjCf8~Eb$WDkv|Q2X#z}RJ9d!>U=inO~ z)1jhyF%;>R%La9OdZIj;cKqUG^V%|@1X4I(2!FR6Y%HP81q`{i)BkRW6JoTd)Iv>7 zZ2;O~lke^UR?*T)qhF^(zy;;YTbo^aRfa{iCaj^EUf2Ppr~P)`%2VQ2&9dx1N-+lC*;ENOHDhE|zQ)upG=G8=MV0(g@CM z^?&-U-0!Zsp_5jZTf^9xiODtyWxLqc)6z)=%=uWh%&dIcD9Ox zZ0uJMpyF&lK#H7u?OJ7A=ZEWr)8GpI!Y*LSXQ#@ksR>;nu}=2;Ztfc0@k=xCiD}K- zZ$DL-%j&Nc@ry~~$+sNzFLrh$)BK$x7>nmSj(dQ~&9uq&)+8yh|A;TG7qggc{`+GBYolIxUcxD(8a9 z-c|Eq^7*B)^lL6X5MpnEmmse+E9sRvQOcKr8zwFmwGH{VP8 z`vpi8;xz~#4EAHkhnRq!u^XZN5ZIH=SXt8r-c$GFRFHsTS^&8zM+UW&Vq8yXIk+Qt zDBYpf<77rEDWF5L*Nf_IL$+YEzdk0L#O0_elfYMh=dRilZoKesIX!EiPJTn%DKqQM zDh^oEjMPWk zvs9in6V%vSo-15V7ujIV(`6u3Fa(Lv|C zsw|pLFr!b@9p$xVrF}lV39E9Q`MkPj`3u;N#1~$jiRQtvT-jJ}7+WV`>5$FC)nU!e zbOoK+id9~Yn~;09DJ-wa%gb4$k^M-67)FM=A}>r$dfK`SD@*oXc^|-&imvLua&(d- z_UE@;OA;}CA-d^QiaMB~-=BK2Fkx|?fAm}dHX$&UJtijby?KN>$l%nz)<{kIT*ro)vThI;&LdIY|P6R7J zO?g<#yF!skk15SLxkzG4%tNRSW5OG^hg_frwN zTVOhCvTxYHq!Te;IgZOF-8hvSy#rAWe2n!jOWvzDgo(?w^X$a%M45vLV@C6H?7^kH z?m?4}Y9F6koRgZf2txECF&9v)PTLa4+SmaDhPtYz*?WbiyR}hK%k@+nfRlB1J*c>n z9_1OBBVFteP&~ezydronxbr}WVy z!PF>dYBAlgrI=q)d>i^UuIR(mbd%p7b7qoWje(eof)$fZL5x0APPtn!P>4B&1-wJ9 zATy-VTjVe&=bQR9N&$#SiWUScxe^)@cv11gzzob*YVZPUivzcXy_J&O!UesQ*uG-& zT5vNMGjDwGvNqwDT5^J;v&b|k4Wq|Qq8Ol4Z1bU$J@d!);VAGD;Zo2JFyt1fDwdO#y<#$P#N4!4yQ8{ukY7Ou zh*9}L=mO8n_}iLNYsKDxk8-&2ZVL&5*6Ik-JV;S<@q4R87&I+T^eIcZr@(UmLy@Ci zd0}#OHPV-qNtO9->cv*D?dZ?iHLJYjn|H8Z_HHli6oclB3=|CH%^Hzeich==I&a#) zzeNJHYKdd?hq(&CJTRY`J`DL?f1{Mjb`eFy&W^Oxp5T=YQj5rihZLP}`@#XB}( zoa*hbtve)~!Jh5F2eVgpCb4sY)N}&-u|tu0@L5@P6;l{I7p`JeRCY&nLZo+P^SxER z#2s&Zbk~eJ!P0PJynmo&Dh1^B0J+Lz%8vY&BG^`2S;K1H=>5{%ZduejrKPfYZWo2t zmX#&y)nP=2i|b5wt~Q|UXQWKq|&QD<8a1OHs!(9Qms!FMUQvKw~Ut_ z%>aZ%3DXhexWL{@8DuVU-#v7dLL+#psau7k%nd{P5vrzqJ^m4~WTL^|JCDgqZC%sy zYAs9@!p1VGnk<@sk~|Wr%<+wnJ&3TF2WE#>s1=>^Gc~pU@LXr2ZL?Nq0G%YH$P~wO zGh!nlF8C7_o~~>?vt(P#pt`)WONk=Eh{MH7ZOvax+~@X)v<4)oZ+ElZGi$7V0gq2= z!ODy0WmLGAk_LH0Mr`eCGbF3vPp-$(uvfVs3C)c1dx5~{+V12Vul#QCN(@&T$~9|E z#VEp3ZfGA*fVFh z(4S_}1_oAFlB{cuzRU?Zc@fd*yC7cO}JYK<=44gQ#tFAx*+2nJ*3 zpfLs)i?ra_CDZh^V=@z?(l#7pnKRPD^ zv8x0}1!i^W?jsMhWrx^?5xod81oBce+cH*Y<8%IBt;V#oK?aQ|8?>k> z^+ADURUO-utJ8`zi<74Wp5log4+@UrK|wz`?5RdkX}nXg4h^NL6v)o@?Wfl_Tt#ao zHu)ewqO6Qs95WPdSBOj=$viN?)9}P%4h9>^3*6qya-!EmqvCH4<}QqC(-Em)#lbci zq`47l=}A#}g^4}9x77Xzd+!|f*_#eoSK{@G!mtWisaD5Zc;ZQ0+ON0SrG|J z1_dPNoFpefg5=!fCg&hIDv0lGUiJItJ7><^GtWIU&wb|Je|FWbs=X@g?%Gwg*82Sl zgD!hK$Fby$W4hlg54yR_xz7*G&fcqwK+N|l!|BdfOUqV>{5YOJ^|pDuwu!QNbMC&1 zAZa(xZIF0dx*9O5#oBwiw7HI^vL0V#K4|uJtzEUMnWXbdTDd+Ar*q!;xy1}(M8iiL z+rT{9%s+uk8zF|f&XYQi*<;Gh)Ka>oD@5vXl{f0zSu$cD?LtcRFAKl&K~7{AwRo2c z|Mi@VfM@?Z<tZX`2k8c-z_b7SFHp711`-mLvf);(*uX%CvhrPwSpn;>XVhG2i*hWQ#;OTANj^-!xmNP2+W1WB#Ax=pT>_oFQCqm3JwoZP;Y9t)d5K*3B z%{Dzd{36-T&L>zrcBDnIA!>52I>d$MdP%9bQ#lOgN5PkXS(|KOSe3xFVk=qrZ2q0{ zHsel%(F#@y#?pXgu;g9*(m&&H3M_ekfS9*IFoz)rPbvPhW_(9Yq7C zMB7@!1H@kq@V2S$ZX9yE>m|k*&+6+N8eNruLvv3W5?hI^kaQ#TZQaEM+)?HQv{k2* z@oEry@O|abe1pXY#tO5825s&t8-ks#nC29KAIbcENn9N!E)7r!ZK5`VT8^y=K1zVV zw3N`%L?4i{?)|{lF8&<+xn57dU-)$@hgt{$I@5~K>iezJThTc^8~M|hhGq9tCiQA# zRbvb}@Sn{0Z(uVh1(%rLQbZ{0ZO$Zd?|FFR_G_^;3~X;8T--#52#3-DxB=qpJ~ofz zi|SOlrgZkRN(!^0LDE&a(Ez+1mev4CS^O?Z2Gzd1_X=%oxp%fX;;p{Y7=D^y;Z%>1 zP!?YgN8sn;a{!-EHd$;HnAOyB+dSF1lT%>3aA?dIE84d2)eZQ{6%1NkGen&~S+sAN z+aJH$fA>V{K|^b$Oq~_Ya>0=}SLk_r54SLfl$n+NXS3Yu5l0I<5yt?aIkjXfvn2*W zIlaZXREO-d*smstLJ>hFJ_wY^H1baM(=WakOegFP>T2ZALR9B$D(=$^4R?o)~ zfwoADRjzbOds2)4jX(X|M~1DV9O`x+Y+uU8MB0s}Ko%uljQ*7b?^bdybG@_uTi3#( zzQk94`Z)Cio({=NxVe^VQw($JCx+a_3(?;UYOV=hq=InYx>tvOXYZM6KmWG|nL_EOXu*9* zj`xM-H;u^n^c{UIV{Hi49rt}Jlc@#2x$0}7DKHt7&_XC_W#rQ##XIDJs^SmPJ?FXO zant9Pw;%dW`sGTFt_zJwKddsqYkL z=T8+yW{jy7m~|kh5PLM9@Z5X{_ZRrU+xJ|uOU=yk4qz-!X=33bRUYAod+Tj39!)^N zDa^|o5mzU1x4TzO*uq7f-<%Xg<&QukM1G|)>KkmS4_lcZFB5SnZE zQaaxWdowN}A{UH%Ajxbgz8<%4yz8nzm@|9F0T`>oeQ$eFL%z%^XhSU39%EI{2=o`m zpnB@p#M5yT9F$GZeymo9ryuV|NyTUjPhVe%{Jn)eg<}y6H}ZkwH5^2#Jp%(vi!n=K zBg_hTq|^RYeL_*6sCUlUkL)ke8X!s5dqSks}bE zE8d73kubrwejCC5wEpS4NURW{CdTc3)=@`F;v`A6;uZVOQ8Gcz5KC=~U`aTv4QtwTNbLy1%{rBX^q#E`yzp+1!)|5i<3=sl9Bp^q9 z`|UYOYVB|P<-J8lZL2$VYaGS0ye9>8k6vamq)nFU&qizYp?eB!Yo90Ds>SI2u(e^^odj_!nz#hlW|=@A#lwf`!YgO| zCsrS3u2S&6I?Mxl!B{57?2cAW9G{*n5ZS^#0jrqZJQ@sPZhdj~eRn0k`mv)fYLF85 z(B}*_kzT`=IB`dpy)a$VN(F4lgDZRoQBpl|WUnaJ0NO$f`ies}g=1-a2T=oIwjz%!OT)2Ym$4bOI@xBVt*j>&BVXVELO^*?x zO^1AfXH<{67M1#x?mBs5ap^8hitQl`Dvustz~wB5eTH}<6P^j@Jf!U+4v<|=xzE%n zc0I#{cIiaB$Zrdl*)~G4T_+~3lB~A*CPXR8^;$~HMGjI29bP|35K9jVPcbKc97D=J zD7mASD|IRY6-v;#JAh|a_{GtQIeN~rtCb5dv3``)1TDR~Z(3NdC+A#?e4_4980FoQ znUC<~yFJAI@u4zN?{cKngu_t#(L`>78Ox9jf3TX(2>x;zHQl)Fg!G%*vjlsz79Lo*_yXELo z+|ur*nx%_7zTZn`{o)iqIccX>o%TPD(nHLij^nAEdhvJMD>-WYm`MPHP2U4KWLY- zX#kyf@QNS?ue*@lqVfudZCZLJU6#@)KCUi1`%M-{ld!bh zlF9_44lME3VZK6>hbZH`vMR#-!kRV?^Klnlk#1dqxi3>)M0p|@-e^_p+hLZV=O=YS zDD&*>;yPVf_cX4dOYg97CnbO>N_HV8V3QGyRR?NMv~XaI>$X@++cbtbe$s<2KA99M zjHwi>SuRE5qgk&~0EkQKD%;vI81TI-1)74P3fVQiGWErO)ZYcwpj^8F3)IVgTBU-B zUj}n1n-$TMM7bY!B5=p(rg(NILcZa!Wh)wRo80>FU1o{xQU{v zHw1eu48v6rdt>Rh>d{aK+Jj!9snL(EEK0W2lk>V;?oZ@MI<4YRyKSN&n_9pPg^^ih z`UdL<(hN=+1*O}-ezdWmRunC;*TM5}_{m)~Nvm9)nNe48MP@0$p9Yg|vQEMXM?(Ww zsDW2;WWn~q(4I}wD7n?Wg|5)xaYC?d(#jO+73w31z)Z@ybn!uGUhD}&-mtA(-_p}< z9g!gxb0-_*9VcA&=tnd=JGy+ijiN+m@QsE+YZosvaelqw+FPp}W#T~On3^$ESJjZ+ z|3fOClX$n(b{8%`vSw-z-^ONp#j5zVlb?P42MY{U52GJ^)$1YpREJWy6#cx2uPKvF zbV)=+ib||G{))sTpNfN)tHt__TMUe|mOt5Y5iQNoj4ZWF1ILvP%W7+Koc3CLgLSb|>r8@CdW>Ex-o8_{Pg#$?lsMbhl*jL>0NVGv>5?w_q~j*F^m zzodOyl-8)`d)Tv_oBc+6U3!@Ft@ye*2{&~M=nXOgFdcZP6b8_z&r-p-wN?h?Z zPr}v67SNu0C0t_gpFd?UT|JHK4+SM9`SGDn>K84Xs#7vTbiVrG;b*Up5j*fTnEsP4 zqrLMmQH9AO7({ga1X?MaJ0~SZwNxK74th6E#%f-N$Q&fHw)F=};Yr)RPOx`y(+@XX)^-hguFKS!9^`RP9#!TSdnsz(Vj$%Y25MV_3k@Z zbW`GO8RBz2fC)*CKK#L~$W@hMCn&kaQx5^WH9xl4#>xWZyeUdCLomgu{k^>GU6w~^ z55;?E{^0)C=ce9aH&;h2P-2|O%Zi6x2Vc;Xk-s8(o0Tz*JvZ)<&er$d_-7PqX+4AT z956oo<&yX(w%xJEzKf(nJq%GgW@@m(x8S~2%j)TZUk3aTvC!D=nJ7gj$K20uZ5Cq1 zcKdnQz}A{otxhGceNu4QSHO{xO;%FsVD88&z|IAWVL2s5vg_0t;$HsLqGiq@j<8R;gaWxVoy}@2O=Z3GF>{I9tzQl$Wdvp`2n(%(Verm$iuk zk-;rRpCH4UNRnc_hX(N1xjr(q_j>D-42&tckEw0B6gt%DKIosk{5b z`s35n=lV4FsjiyMN1< zw}B&*n{6~mT=ITon6iq_$ISG#-g2=}vMH%f?3rMGupm0{4HRBO-ht>xiTt#{^Y zSyYL&FKQ)tn%wR`#<3Es7^KDusU~qL%$&hZ^ICVa%CqL~eko@eIoB=cDt&41)j&7Z zj&H(kZEF48Q&@J)|ApIo7!VOUW=Ptg1DVo?(iPP~M(;3xpbCC-;ay3Gg{JbBk_H)j z^Cqc=1li0SIZmG&1nK4w=KIobiuK{Xaqz@U!; zTW7Qn8PSGL&^6El$t~i=KmkYe{U5O+y{cEGsGo*z#AXilS5}`r*vrf+NkqzerjALf zxqhgkr@@eNmVNGW&U5+ze~xru#!*D}8qh($Tw9aquJo0?KQg;Df=zk#*sMqajorn_ z5LOZ}rmsS2QF6C00gcR6iYip*BScQ4f16Z0JsxCL4xQs#Ey1?nE=*lQ0#{)=l<^g( zt(5)}+A4w8!;psAjeml2kVCG&W@L_=(;os))&?{1K6CY>tw`u2QcrZmz+28C@YXN5 z0kCGp)4qEvh=){O{?QXSQx*OgJBG>{s9cD6Cg>Bv%5%u*Pc$?W{by3Gf5c#EeTx4{ z6}#5?4uA-6?QwLjyNjK@y)KT3;Lmr_`S}yR>MvCc&|cO3_|9pm&?7-V4o!`sZv`!} z8Qga*0@kG;6t84ms=D@-hlXw6%i)fvTHB5@p`=DaPK$1mcpmqvf2`y4hU#~Ap1$7} zHKC?PRiUkMWbOS}i&s{zAfch3!@~ApRI;i;Hj5KK;3hVXML*uOOnl;2r4yTYA&!rV@a9+$}o>5p<-V?S5cDpH58_JUzdvmMZd{YQoSplW2tNG{S z<^{4azncg1E^kWd>2YYAy!`wAXF0kV(4aeVihsu-uD=>IPH9c&4L0fQJTLgw>biS8 zd7Ssr;tVs(}S+Hrr5ipOCt_P+y3N3hZt`-0d0})wjWkA{EW1&9=-U{T8l5Pee)#CN<*1GC z2fxf8Mf>^gdHL>TOk}YByT)|+y&)*3zWi*+cY!Op@6eIeNxe?=K}$&b=+37%=;DRH z!{Sl-VeYuieIDPN)MDVEehhK-!?-63!o8}JMd_?eRxf!{Cp0@d_K4T@wS|G)`#UfA zZxV4JKj1PWKcPKk^N{1r;QXGOK`m!}{ysmMIi6owk#3Q@tcU47 zMy^RrM68(LDiG0zKaj^UJ_ehem>)MWVmW1%DX{-k14q zLtUg?V4|(9(LGE*_l;ffvl@WW2mp$Z&CO?!FMo7j;-^DI_S;FCrP!sJRTo3yQ^8## z9L0B$w9hk$9jFaw20JbkD*E|HXZNTWGEoMO9-*%#zjzo1LR<&kEusYgm{+BanD4fX z8T18exckWr{MILByMmLWnGHxihYBF#P_AnTNj0vlrZ=o4AwtdMU5I8@f zxr6TzwgNpa-YNzma7blCWkvM_&2wu)HG$@xi?jgpX-jHFCIO99WOVz(-s~)W zI^Ws}oui7ayUmePKBO0s@ma@yx+uQ(ilO2Psw*pP4AxExaVu?$#;cE)1Yyo$ET6oH zfMYZC?GZG!RQkkisAdR=rk{J3lTLQ% zjuY)hJxXE{~LuuovK47?E-Zl}e#z9E{ z)fFAg4qC!}ASL3|ip4*SajOCRk*dfOcqHVFMDeP}TOFUTwI7baeVLeA0iHy@WJ`t4 zFwyEO1*qLjF;1T@EVm?o=2Bv(uQ(;v>Tnga#LALvH78aRa~lSmC%g8_n^AUmo^cFa z_Y{qR2(>8DY@6pXKMVuaTY~nM`$I2^no>roP>Z%r#(U8B6a=+*i&PBQlT!V0Y9DRH z#|kJj-@5ZX?yE+-;hvD)Nr8m?<8HuizQafPU^_ORVcha0I9hAmt3mNJ-FLx{xMa~B%gR_3XuaNj ztR#YAv`Yxaew{)PlvmzxExc9CT~Mx{gfpoJnbfS06=E0uIT}=EYVG)3QOV|v*Ih5z zFEGL4+5OO0<2bJ(xpY6w)5+DJu)X>Y8FfR@o0&J-O%CkNy}tr`p*Ooz+H!oXFUWh@ zSG~8HA{=nFJ6v*za*?pS!s-j-@|wGE^XiOC=TGdMKYezRWSm~0O=BsuHtl9sBH(>L zE2-}{JqvEKO>HdCNuo7(1NgsXBJi)6VLo(^R_-Qof>c3D4EHXcK`UT-k@h9& z79krE{$5yk?Jo9%{%hkQ^jYAGS8;Ab^_ud%SzE`-E26jN7fQJ>JZD-rO4u91JX{G2 zdu#oqZar8RlYLqt&}O`@7qa00AZ8;eg!-e_>ej@(G}}?M|a7jQ4BovvchZ zfNw)oT+)s~+nD+OD-bbL`-qRaDD1OTI{Y^cHK^H={?EcsQ$b?|t29FrC!OB(uqud; zrtEEL^BR%kG4$bxO_Baq{2f?|bWVUcnSd+}cV=;HgT3N3jKR8c0#*`*f|Wulbov3v zQ{w@rkQRNBh^OJJ8lLCpTTzXalgWOLjU(DZXA0g~wNO09&Bxz%@PKiND@5;lHnHzN zd$EZ&fg%wqyYfrh)P&eFH?(9neKQ9l<#19l0|}_M+jOa2e3WOd;%f7XR&}+V3ha#) zx(q{9TnCsdmRm9AB@{7eu!`S;$@P0cr$CGC<`Q}q!?1zxtVBR$Ok zG86h%55{clDC3RjtLdc`bPc9@E)&;2=!my1JxHxIYmKhn)C~q9Zu#8&Ay$>Si4Q#=iDk9Ko{%DH; z&lJYi16elgOZ_1*sQXmJ+GRUO>C#Rt%9PCj0D0q$3UbKF)px6NuMGBigpWr z-KaP)3`WFOE>@pk)aoTLEF9C!vT;V_e;G$EJzDNLF;RqDPz^vLnzGAcc|7^t)m}cX zk)sbwgCCBvrKVHhRFq;xdZwK552`TBD^MQoeVmyw+fT1NCE4Q~ms9Z;J%MymqMp~} zJrLX@z*sK5-R-~A3ZQx?{XS_bv-;L{d#}O~1c<~^+Zr^2E}dY2jN6LZ+|R_iq@r2BoeTT?l0wQj z2*i!hTo)XzR+ZDx_)*jv(Kqp}N|V*E3MFS;tE)R=6qIZU0|}|HU5rV;U(u@;L3Ani zq8;@>K^xk839fX>L0Jnxm`Gpp#GUe-n6bgE)CfX;y9|Pc1$tjNk<^e-)9_XV?qigV z@tNL%6$9H&WZrtv5nf~zZb-n0=VsJMRo;vdolk0PL+p5>J%whbKed?Jhox4vdMA&a zZayNefCQW}p{W`!t1{`K;-W2i(Kwf>4mLx{4%66sYqp1&m`m{h8(fU@vg3D4Xs8&h z7wc=NTCy3t*sgC}rB2llD5R})Pa;*j+~5VB5KdLNS+V$&8m>0epE%e3o;t8Uj&^8| zyZblZjxSeu4VwO~R*3}4^B2bN(}F@c4uQr_FVl5PP|Jlfd(cSyPH(5^o$CAY}~h^d|HX0-Vt1-I^eARaSbB3cWZu(`_l?j86`;$-w%+m z>Vn?dZ-v=JTxqf9q(L0D}c!_JDBPir+;P(3kC*mpm zM~1hB+MH*9$s*6w_4@J<4zy)XG<7iimx6c{gc_(jP)EjcE22F(gifbf- zH%YD$wL!5-bk+azAig(ea4CaO@(|1AvV?_LiriWIoF01aGWpjBDeq(!6a5mB`m6Bs z3Z8Mgf2~m`lZWtkO}`604k&#Lul&`QJHI+Xb@ygFRrN1TH3|*vzuoWgFI84k-iCin z;M*vT805=G#6AX59TA?!R##+(+bVH}-;U8LQNZKn`SU`_do7ib)AVVT#-}NH{Jn%< zIYN6~tZo1)W$_`SwFYzD&JUlVAZUE-eot2}vcp{uZ5<-7`|{zDylREZT%7k#ez3Sj zvU^m-8Sn*6Td#b}^V33GC;x!S6sR9sJkp`!#QPC^eOHX&nEsSIji5C2@mRfO^kSwM z-!bfz)5^#IpF`hyk=^f3gJY~rkg%+psW~NZ%s1wgbGt?Pl58n=JsKhg-er31(Eaq1 zqFz6XqFl^f@|HvQvm|1$>hXnHg_x;`ehHOY&qxwQ0b}ERw#Qbg0Sgrd<@eE%S38vW z{quqIkzWcJv%cIsN$xI5IpVEKo!Z3kwu#}D2xCA%0a`S8diU;DSl`{;M#ra@C zryQ;I5T4|`?`&H-tmvi%)~g!w+#O-Dj9tt$XSwMCEFk%A=MW)4nTaO``z6(I2Hy-h zo%5s!(w(l*mtx8f3$4FZFBC`x|8m1Pvl(-g2+1mOv=$lA;V(a097 zTEhfB{2d@ZKx~jndUKp4|7(JL)tSI{0+B*Y%fR*k44qpkthb^+==lGJQNs%b$Qi*` z{c%+8aZmzxtBy*GCC9pB9hBhjn0~S~e>K#Z5(rIOZ-FmNES%2O^(Uj67Tz17 zc#`A4yZGq)j3E~J9c>Q0Ayl=YdCM*EmYY&y{R!w&akope3hNyc?$>xUixD2LpO;m~8K>#|Yp@((>x1YKPz{(8 z$kq8hrpYNz=2aqpVbDH&q04PsP}D8?;k;XhsEjb0;Fb@usowh!8GlgJp|(W%9n`vS zU7DF+4&OzViglQtIbNa47?6gh0+sEiR%U_i{dWRsmTzuwK=!-euN(acpU;;1yPEZW`5=at?_#lgykAH_L8wp9w zNi4O91#&96Ux$iC?p;y6U>7`MX{L%;ZF=!#L9%XF^DXX|;8UZUJaW(d#R?gACNla_ zQ~EQ6)ZLk&f@BBIvq07qPD%*+Lh_iq-4y#jh>R&*QG543q=g+;kN)gks6eCp<~uEX z480W>cSCXT7|%$PZmSvg^SrcQ^Dm4Lg`LHV8Uq!B_EH2>Ayemp-eJ)k?hfpS$(r6r z_l$(ppnj9u$T^MnWBwIf$_Z27oe61U{sj;DBs5Uj&NOgC z!hS2v(}OEGyA{#eONmC#4xaHTKyCAEXEqt0 z2TacN62{ktxz7<xJThF`#&C zB;Ae!2a|ZF4wm?0a-+=paRL6~?s1zQY!=8_SCoBvql$)EoeI`+aN>}w)`8Jx{KwDj zV=ISJk^03^qxAp{>?2QMgOBG~XvE(M>%TCZN(aE#qFn&bY|3XS6Xo`^Os2x9^iY}g zbyM#{;_>buZ2Km}VO67d+P__IHbQ^Od>yk(t&Q!Z6csK+89EpEic&^I-`ae1K(gI? zh*0S!IKtXBFyWgmTc2VmM_JhvpTGBrm-tYWsQlIUxfQ-(d?$!y(6%8BcN3n~G%*bI zP~HzRaYij}4GkMw*4nMx9fCRJ%gHpRPnIe&J33m2yqqwA2m4K)sL&?Ls)o+2(7a6( z2Qi6|ISW=gJ=}OI1`zuWwj~ne1lo9@Yj&P&?f$yaTn|_p+byf@SCm_vzh$59jnoXB zBEvThA?*MNcKaK5>N|C#7R^egr8AQ{+-pWI>c>?y`hv&5z~OEmmu&c5Qq$jsU`AL; zsSfRl#oLvNj9*D(qMn;{22{?oFA$y9e4%=Xe1wsvB-M2%vrbZJUM9h|_7*M@X3fhp zubl+qWn?ahez8_BWkj11>8oN4rglQx3O{%QJ~k{9x%hS-&{%7P0GkgE!E!)WxKhWv zPMdLK9_rKH5oHf-`k~8{u)(a1aUH$8?nCqirh|O!w}@~@d(~b|&VqIK#>?uJ0MN3x zdG7MgUA?agM9%zOg2F?DB-agRP1Dlm)d=l1)rulHg^VIFlc)T#(sK|FXe}&X&Mpfo zgtftJCX(H1qkM`k8G1a*yMAjz=R!vvIU20IFZdZWOop!saKdkkvg766JD%r=qzm=# z&GiG2sZT2hNMRU>B!~~K_~0m!H1XEd#vVAvhpyr!#lvk)+0g_@Ur3Zl3ia#;iz?|1 za`S!6@&d=YSQC$a7Wm^6#R@H8y&C4&{Lndfw9*lJnjjAK^OHZD&>qz5Q);u1r&3}z zOKgs7J;8RuwyY-LD~G8diu@ZF<-b|6H7F?CN<1QFSD$QZ?GDv|4NH0PM>I*=*hyxp zT{e>_MXdfX&@cmBMvYCyW%Y1dK9csozxgjW1`{OT_UbTQf^DVAmFDaL z+RYFv-X#0;K`?sn^L_Yd-2QIMz^L6Clr>hhC^R(TEf0@Pu8^8=2nO7*&8N{sv#oP^ z8nH2p6Og9XZfJzAd;vhn;t(kWkjh;o-hHeQOZ?&cYAKkoi@u><)XEsH1|W()z0y9o4`6H0?_ zo+w%1yQNj!eF?|FAHaK5#VTg9Ks!cH+Ek8};k8 z!|qNob*$d3RZ3V_x(vRA1Ep=*4QKL`c2s0rF(OAGu7;nYs_%oTHN*JULlQ@yAIprS zQaQ5i;g89Gt31|28j#VnjDlour#IZbJp1j=r?}l~cEHtJ5hrxqN4flPgNR zXb%S3>E+E5YP7P=$b7FLp7vnrsHa1&Qn_bi!PWwJdn6*hPVY&XcCR6WJQ|-iU$>Xg zlz@2EUz#z+#hCCf42w<+J4#p5S-HeduJ6xWn|gA6PB$K7GBUl6fP3;v-mj`KwCb>+ zI39P*O)QHf>5}`z#`nev=!n@N1+CA?C)sv{*7*c=#tC)+Tb6dL7NtEyL0LprBU@nt zZgm$m=COkPcL%@CdGk*gXMCDtPMS5K2jjC}>pn6!59nxX-H{?DuJMR_05b;r@Y>jq z$4(00u9Bfj2zOpl^mKFQCC@^<*ek^@mW+Y>&QGZ5kyRg znzA)+aj;X+sip3t?*l^tTM_o z7Uiz`+yq@y=6b5?7YBefZ)yYU453c$U^YH5Fya!&fJg{)2N!D#AIi`Y8fv(nos%D+ z(&JhlDlo>f+WEz0!|ObbFaM5?b;(rSgbvuDVV1Wn!u%A8(`aLp020$B1}uedv#|lF zk_tJ}7>esvs-fc(RDFrwQhWu3kphze^Xk`2ngmSIPR7fXsKFjMVdJ}ethAU0hfl{t zC6+RA>$B$?iwyQ|Xjt0FvMihK@lN*W5D5V}#9UhYo$nLX`C*g_3P1O^_!D@V)J*Hf z#w+i7)o~1YavX3a5;BVcLX3*d3+wj?p`kpU&cv^Fxz@-Bns`%d^w^ zE-q-qi{zH?S~?T^KftHp7wPX<3@m_>9FdB$73j&C|n+`y#|b|H3fjTIK2Lm8|2wOA$!+=FJU=F*aq{ z!;tQqC9NGV{3UVJ#ODK>k6~}#RF3@FbcWF~(g?i>q-8C6?zP^gc=l6&x|E}owYd3Z z+&SmX-FqV>Pt(+QlV^@5>wa$){;RUiufxnZS3|D373x@7Z;S%}*|BW!%I8UyZ+)K% z#yrccrEzAa3~!@1(y_!Q|EXKd1*NVVa5 zSZ%oL6imX@<;^$~l?+i9`vKT72$kzQRPdNKFn` zQ6Ex!X+HO2yKqX6yGeP`JyZq&di>9~rJ4MVI>_NeUZc9IiD0GUaQCn-vnw7{sCj3*lKaMifeU1YixS>T)NBC4R=+t7^NEz+|w3)wja+DS`cgq?0$Bh+_LRY z@k+sE99S(*>aaMERo%KzHZiECtK!I9TbUSX#!e>+vSvSWbU-+l)Xuo(91Ki;FQnIo z8lc_;W_ISj3b(fX2qMopqJ^I=OgY(!sT9qC!P?$_(#;=rK%a0DNH!b35TtUK{y z73(rWj)o8~@Ly7wBWjKB9SF_w-Fn-eml~}`sFu}^B^AKG%n6WMeVNz;Cc!(swi!4{L z|9)ll+`dP$8|qaRz03Ajo=z#(ikc1MML=&!iaBPi$ddE0(MbtaqLm|+Bz)iYY8EVQ z9xNdLOy!BuWS#UQT7gn+qw@e@R$&9Q)27fha{kMSHnV_iI%Vl-c$)docbp`0`{(d}({RJl7$Yz)`{d@oybYuivErj(Od{ML+lQV%HmMAf0%l z3V<3iVZ_+rGy$9TSMl|>+pw;wUWVh!(6nXiOncsBas}HbtHS-VGTe0Nvvm#6b@wA` znh)lL#LUzGEHaWr?a4VibQlQpale_}_eM z{tG@c_;94vl6Yxo&dqjIk4#HNY6!w_=D=M>t|SMfx7o}NR+1=7iZ!FCtgd@yjZOZ# zTf?(xe5%k`)-ro$#uz4w#$ha|uYX6*CPVefbO^yNLY#9}I`&|f)EQyVVaAi0QJSA` z@8Up^o$jt;YKEp?_YwYLj@i0d%73K<1B)W*B)q*i^Gfn^1C^DD7J`}ZBH{4&uo8ui zem|5|PGhTeDS!MFy$z?Q>#HikL4Lxg8&jrbD=M_zw_A!%zkTHw9j8$A>MXkZVYcu~ zz4&k|5SIqFE*`Ut~PBR zIJ63SzA0b!-jT+bQ6rKL*8TCeiFwgRPK$Bj*C00l>6nnauHHE9qQ7z3qaV?@EdH7l zS@Z4pQw&%O8Yf@{Uu|qMg7acw)ujQJirY+lEqMI6AY0`niNmCF(Q1m3b_eK3*gho$ z4RTQ+CsI+x64<2c?5ybHnoD&l!B1D)NZ0(v0@!FY*>9*b`|`R>NychnTaXQ|!o||q zco{!fnCI%jV`X25|Mt^(U$-nd7b}lvl470sYr;`~rNBGv%lGPR!Zy!J3+uJ_6sG5F zt#|CJ&?a}RA8J8g)QM;V$mH-T^7{5h<@`;QgZ@t@^N zMWx1G-{T;b|M5nPmG2kt2ztd+Dv9`UMrnbE{WLu2nU%{$vPym99oF*F%rpig6ZJz( z+z&3wK&i&=+pNywwqfag`n@eQT%4Kz5{_U{s?30{lB!u!UXfS2K7(CV&&JnS zMXLBsEX)=>8VrOn|BV!fhc)M%&(ip6jl8%TRX6*KR`H*{za=nPQ--BUgxfB4TrFn# zG#u0hHN-}P6A7|AVh-Aw$H#P}lc!}*H!^gew|tp zwlT*H5<0ayq>x~+(OgZH+hn=jmduVV%PUioYDSKOsL8zjF)*YtFq)?AQ3-Bk~HjIE3%^nP_j~KU3 z>zGhi0uOqqA+E#bGvbqL)sYX zhpbG|zEcEmYYJ>?$H2g4)wE`DpN-n{dt3i=YC190<~Q)Z%p zvf`UQiQRNgP(yFM(zRv+szcW|ZT%JBSRYK`uO#k(qe&3%kBL-#WT=NmNun;~kAd%t z)DTB}e1)Hl>>{3b$Rop)(qyzPxOq|Fo53E?O{ll~!as{} z^cLj5;9v63eZl`Y_UCW-Kf7xvgd5CQWJM~5bZu+o)tfV6Zoc{cNFS=Y7^LOB`WFV_ zj_JIG^nT`vgd>_f%%d@6V7vJp+Mkru`iO_pXI^f@bpg#ycHl;wIOfB76_=wvwgTa~ z_+6}bVTVaL?;L!=tDKnsr2zV;*t_O&8B6dD*IA6P=3A}eKgGCw3Ver>(QIXel;L<>{-;>#pLSKhP4ZO@dh(~39|(} z$p(;92gAU&r_?LymBVwB=fEpa-)lu1uHqt=3DG#6M9Y-$?a7mIAp{P_FvmW7^s-Oc z_#BW*0+>~+sWbY#I5Mv+jVl@z`pmU&N(2DFDzJ=CDf6*@Fgh@cpNopm%Q8-%f$1xw z@vhVi^`W2vJ9iKW=jqW9epS$!>ZLO8md(ALRWe4F(9r5LiYSt;>OL1D>$Cms{T}-r7qxbEp-PoM4cDA6eJW z`OMeJ)>X%{T*H?$=8Nx6DKBy|7KwNbdy_T;|x?q6t-4} z>da%|Mag9m+15xBGEy0U!ijt->qT9AZp{3d|K+Tr(CTMV_8H#~78n%BGy!>$n;ytJ zm-^M_-%ossU-sv6?&*;Hb_XZXtN+Qwbfa_qSnBjc<{{d2y1A&GpC|6~-HfyDpY#(o zPb#VV8(lSR+23BG;PY}Z07WHjp2vOdJl+mW3~IZ^-^aimtA&1HpC$CiYra5E=VT-j zrkhUkl0&+6uBczQmKWN-Dcr92b-Q2Tu?ZF9-_g zOKur!e%|uYcuVJEs>p9ixqfPJ1K$l@(Vx)}1+z+M<6wqccGB%j!Jf za$je8+>1#{LP2@&HCav7?*nR?j%Cva`?0lT<>u48FY)WKJi_Hy@V1 zBNN)4K*H`zA+t@YSaUliZdw8@1;~*{U5c4#X0+9h=4WPrP2lUmw+v-1@SS5?^#T29mIjiG4uI9)*IF1eP-d-v_6sXQD_4e z2VUo5?XEJ+ak>lNpLl;^h?=>^3(B)$Z|V+HgHCv#Q!ESAJQMh!8`zH5ZmL0!$95+O zq|O}9iB1prUz0q_3-5N!8mbt~zUa2>D-&$nwM98k5d_qjDiF8%@46(Pf`Yi7i5bW% z{B4j!LTT+e+4;8>4$r4_!jK!4MZJfL-QW7~Y_#sXPLo^%zsnDcM?zi3WE$EpgEt%4 zK7lkn{@I?&$#j30XFdJaEaHPfH=i>=ur;l->aJbM--`2$|DMVmtxMNJcU(Pkyk}31 zfRmi9Mo3bp<4$1D9y;k`yQz>QWErh~{mYNB=Uybe@o%8aHP9eir!*?X;A z3$zMeUY?}*^KykZTI&v*e0?~5V|P@12e{L08)kawJw%yuqkWG@L+*U1ETM0^FHW)S zyOR}LXH$D(Wsh9spWLBW;||oG%_B)@H0Xcp@=;~I*N;D{HS&RVXJ|G7eE9pDs{!$6 zo$)Q@S*+jor;Yd}f!-hKfFGOXG$G%UIl#y&A8cn)|E?(l%n-oql=CzGgQvB%goT+Y z&mO?7=GclOx!$r7iFbTBirz>8IPH8N&W^hDJ^Ut!1bJ4Orss6p3YLG5stbQ2aCD!_ z5NZ5~okURY0qD``qx^9q6A)DY;D8xD16Evxy|8 zH$^up5u`lD5KqH!f#)`vov${^4BERRXU!@-nyiZmE_sc)R7bHR1#5voAfVbw%Bj;P z<3-pkL(R`fd-tB{t3{;GUl<}I2Nx(YIxHgBZHcP#YG*j`pvld?=%hO4FAO;wo08hQ z75`Uz?;Y0E_P2S*1Bgl!q!Wq)BE9#f(tGb9y#xro7g3Oalu(2qMS6$OYY?O;Nbf;H zZ_+!0fZ_y?c+PpAnYn(?yz|b?HS_$D?9I;Fo7IxN_FA9&{;C+c1UKgO^;S$cMP6Gv znDyea(IjjB5IT0>1ZrcGA!5oK zYG0!4)>x_Aj<#B!7j8tjNd&mIOUwCuCoq_~e&cw~ij6P9XS56RQ(C<@QN5m+_Z}7;YQOn&z>2t^5^nd zK^dS9la?vR{C()vt&eoew_99iSZ#K^OQx){vr7u|<0ttk0{M;TM(NW>_?Lq}n*pPM zd-GMVtjcP|PYV-dNM?{9nb(SQp2B#{@1))}!)~Pz8sND$50_zVftDDvOi8JJBY``_YROBkkT&<~Jo-m@RdE_U| z4z_;6fGLqz=7a-L;OH#J*BY0zGRW+R>VgW`0@9m2Nc&^vnA_BVYSOgRFX zR8eK?9y43JW&3ptLuax}{%562raMa+eJ%MdMm2n$Q?>pYbAjniPaID^70K^<$UYvu zqoiaKd}EO})?ZX_<~vVN2~hTs&~%B0VpMJpfCVBIO(^EiD}&6c{~OrQZu zg*#zq#RoEYDJn$`cYJO}F6-0hk0VL(k8*F9u3hwAW4kmyLP=Phpy55rB|H;7T za|_^TtRLU>_u?IaKW?Gm*(m?-CWzuWHTOWF0!UF-)p`Wai1GB^fG=m($R;*+BsYoc z+?;U5;8QVg{yPg7#uW|`l5a+j!oZ=Mt&&=_dohi^O3097?jL{~=SeGHo5W*#Z-zc- z;6vX{v(a}~#mn0&dZ%rFV^N5t^0wYgQ>CEOmM$Sy;8fEcubmI(|2#z6$*a~=ckgd! z{=URNc35gMs)H^mFI17zq9pnTlj#4dx!I~xGdu6{i2@z5iB;<<_0MO^8o2VWm2#aI ziM3B^L-$;OP@M$L`}yOG_rAPQy8~6a9tb4%&_|O#XfF&>ik^cwzh+WS`fcW6ivx*~ z3{u>xgI2T;S-f63l}9IXZyq4OU}ei@hh1yaxYU(w!Hfe%%#}H!y4kg>rXF-yMjvg% z9nev)U#zM-v~{@lk~0l8F?MYyrkwagi-QFWb2C#PK|r2$hMK|+fE%t`boJ@C`q-K~ zeDfTWV<}(1!3m#Awyc49ye%p+-0x5&#;9r5g){a!>Ah`IlBtPC6G|UVtOwr72Zg zxDd7#UzCqA;yd}`Fuz}2e|G)!23if1B< zXWcCH9$S#~+v}K*J~Xx8BnQ!l0n6y$yk$=gqhpqk6`)dTJxt z)(j%uHZBU?F*#g()<9^7mXv39 zNn8R~7fJJ286-g%?$G!A;)Z0?;7j2_&4TL##)WX0rTm5h>-M<&V76RiWpjM8kUbv5 zZ5olr;1tSoQ9HWq^h0(}X$Ym>V`M8`$EVeb+ik&0y)UVyfXysawMX|-S`>@zH-rlp z)`_&nq^a)4^w@P{+J;GBrxdwPrfeW0^R(`xz55MQ410_>+@)hHa3!D3%hQKx3}%54 zTic(%PQ8%{dX!%Xdw#?>^npITt*C9le=e5>j9zX)i*qKcwyF7%jYrXW@Vqpccb4{9 zGrN`p{Pi(xaf8?~jDpeG1$7fVFKcRRYoRd#<~fl@ults?svcs9iR!LzS4)Bl%h)~q zUo%@j?`PQHaDOc;WJz-kZP(-^unFfFR`9Imfx2p`qI|Q8;)dsI;wQqcH-)`;L=FtL zLlhYt*Fr0Xb#&x3^z@80)O-B=eBS9FSIH3&&*Spp#y?(jtXK4JzEPY<#E*DGwUmf3 zr9!!UZt%RVM}7YTQew)^)7Wcc1I1iy$}Se#PLayn+aRjIP7e{HsX)M!>Ln%}xq20( zI%Dht8ceSg+Z zb$|~cC)(I_9HAk43PoIR#(eC&$vj`7V>Yao#IHCC^!D#s-m8TTb=jwTCu!fLZXtX* zy*Sh_G$udqjy4rhgeRwzP1b0~s{w76^hV76A9+Oyh>M^-`Q+Yvv1XNc(u|HGyBT?l zO5k5qpN~Ffr5k-~2Q;1)bB%+=-x?(y>%c-FadqlNP$>dBq$8uzHaysyz!`lzrV^%@ zYl*U()Gp~%Wn(IinZdy~lW#-ZwvWoqF<06e$g%M#Bp)BH*J&Ce@G=^2-J(1Z7{y|% zsV!q0d59@ohUZPgK=@*4d|MJs=h-u-CgW6AS^zKL$4w_c;D$lvs`Y5}7Y12NjdR$% zSp+9pz{RFy+u(81PPOeW~l9<|e7_TBV)LgUlD2;djx$u`&;W2^Hxs z|Ip0er``NH3Gxdbs-?o)K(EUE>+BGkjH=|}a(9c~6f3WMU=xXVxfLuuXnCKN!uE4J zTitU`1hK2Q+}`mUWZhDv$nLn@s{H#?xGO6w1O#fMfg!SgbelKZUQ$)*XsU|n1F#=2 zG1>=H2pixcXW0L@k9cJumr1G|LK=aY=m4QIgyAO^S?+zY07eJi4i1_`gw2eJ^zdjK!)SBm2P&o66X5k{JPL*%tR)|==c}Vk zW>0+P4KcXW(hlYu#542CtvhEdz$PZv;?g{W^!cUJ2;XaVBS1iHX%3oO0+)Eui$!KI9ke%bpDDJh^_gNIr<_lcnHg-6`Dj2c-%G108Vr*|wlrGp! zv;XR*Dh3p7dffYL-p7+!rrDmQ(4pvFabBzfCY->siwOf+&zYz`=vJ6JSz~gP^uPko<`4S}FBZ^ugEnTT;5~*TTeOt;WKD|z^a#;E6EQ8fEA*khZAz+}eLNH@s z5(`q^PAFu&Fxh0x#-zZRy@v)0i5bPKPUTbov_vMf?Ph04V`U+w7~f zGonbZauklVM}>B;TVBM?ZbAtI42(S&*EHLRN)yleAs!|@y%8+|b!8h+O6!GjmCqrq z;xb!}wg5b!As$Ylsxde$FBo?5CjSfYFL2^bbV4vS9p5EM}~+%N5U)Pi;y&-s;ikBK*#)y)=G2oNN=D+JCi2r@xUofWlDabZItttGX`)RO*4&rHuFY3mF?X5frWJ;IC-qy}R zxfZa@-CV%LA<2_qR2aYQ6fuvYU|lyb+_Jz{96C&Yg75ZfW_#>QUv_#6Or(BEnKLnK zRjk2QOmA>An*39Lu2tKcFS{=~egKI3tiS7~w-~xY^ET%Y)U(UyjBFlp(aY}B8x_d; z;2UP*&LVO&nQ+n#hh5UCfZ-3OrV1*oUu$0}BZ)*^6k8_Df=60R-S}f?1llWfjk5VZ zlzWh9FG`8|XBxIEkb@SlV-qQ!v6pFxSH;_OG!Q-R3a^Z&uoHReq^L~DsRKn0=_=_6 zMm*K2b%(wG{ar_Y>$O2|_0G`ZCA~ySXOfF5wN9PrcyA|7V&)r+S3|uktn<_F1qE|5 zLPG1TUsK1VFF&~#25kB<5C{~f+-ji4GUisp`DV9l=dRXst3=7BPIE*_NyRBQB3G*YlK4IcA;fGK!OpTz=O-LVuOa2$2Os*PsRWNH}QI%HIPUT;`I$)6FeiG-+%)43jTBzX)nZL5rmWuSvpA23$Q>Zk$Pj!mO<~H2 z$GT~-IIx!TL{yB;8dNxW&aXU}l0EDMvwfrLzy5kv=r~@EWM}8HdVij7k6UXB-#i#KbkuTUBo(%Xng*Cid zYah1Vi3FFX`#5QniPBHXZia}i+=x}x(F3k{iOD4z`DAew zpWzQFSp|&v_3e6ls>GC(O$GHXb#3X%NnU7NW>m0y@9J_&a^CuGAdWzTkc+WI4|s)R zRF6jlM`f}`GYoO-^qw6=eKcy%)&0>12L@}ULrq$Cr|_=6w6w=nJN}!6qW>AqYDmL% zP+!VmQ%0Y{8^J*Nh~?bHLiZ6r?Mh=LQo`~C9loIr)_a7(QaLoxR>K_TFv<~T9w z!gPFg@MN`KN~EKDiaR)6P)kR7dS$1hLP6NQ!Ckoh>@KGJEu!g+jG5{Q)|GMxxG1|? zP?&9gZM8erlFtMs@*{ON`}#+agGYL4xshGbj>)J4YJ*jLV1k+&R)vjNz{nie5~Uoz zJ-&3U;3bd@6=&SB#QlWD$(uT{99}hpejH#|5H}gIN4Ve7-&ZwgiproZ-CKzAg1ecN zY*&LF_s~xzW#UEo5oMn&@_Brm3kYB5vww^V33-1oXmL)`9+9#n!_OE%IHuV*!y`lQ z$-h(qt*C$Y%ea#*K=*^1+YZR3n8eHf&3NLmJ-J07mT18r`RPou7G7fNo`2k2h&t2L zvi?+@bV8OpsklyoWw|EpM%Fng?aX9AE{0Eld&WaH6vr08+h9^`gf~lR*sIe?l_VX= zuOMJIA3-$17i=#_fl;&9>E3tHbDrXEWNCx1sEm4VWVRe0zV(mw=f9St{~gZoAE)N8 zGy%UUx4%f9f9zdx`Q^XKP%4IhWpSIG#2rCBYH1b_|4vHkO|Bc$%eD>dBImzx6wDJQ zpO0o|=HmD#e47R9y_a?4IM+1G5W9l;$2;F%mNoUSH#fbzSMxIHaiZt1Br2Y>&MMn8 zrzE*bXT&8}NgT=!{hXb4Kf>>s4&|BrN^-}70w=pvM5^H-9T&9}3uQ%wh~ z@<~U`dcTrLU**J5`dyYH;#ZRUzj6{W!ub7`&Q+5C+cN8&_-BohZWx5h@M-bgX0Y?Y zE}>_&_)R;ALci3?{(2Jez521CxA+%^CM17>RjA)6jl6-;m7M;shaF>vnUO;4r;f;` zXk@J9!_x2$T%z>KJD=lHN7rng>WpvW)xQ&94Xw}ZR~nfQ5dEr3OOq)pmlt4Uzx_qc zni_9lsK-7+$+EN+Z3q-dpCZN0&KyqE<!<%SgC}SnsQi=Ytf=2=AQ@ zW)_2{6l|+`@#cr~u|8-!q-M{*b7=UDW?Fou1jIb9@fyscBa(4ouSX9M;34@X5meUi7oKc=~km2NymY^1;@}H#qB1AMIxx z%5uu%lXJVN#if9T50_nB<}z+IH`%S_*Q)DG+ve4)>0#U_KGcy_@-zbg@qs)KKYnge zbE^N~ic%szXRfNp0ScU3YYPghJl+7$2Z&gPgi}pme>I@kUP^Sg@KPQ(tz$kpr&@u8 zSnCg-TQjo-=PJ;k;rW|@@z#ByVvW((+W%5qMse}@13z)R5RX8QQrglpF>HLMmAlHK zrDfSAbCk4V`N!}Rv`KBiK`N?#Hrckd!B9r8g-^m)W!q?1_3O~-ieRmCGtc02mzs$3 zt7LEXvkrTl?dSZgGyYfrkvzFdCb}fGnlL-wa?~q<)jq<~e3i_*Gi;E#xg>BM7Jw6) ztK)x_Y&V)9UCUqhe3b1g8IFAM$yKteeB~SLagfI8{7!b2FXifaWA=OhZ)LyOP_nr2 zyOQJ$tvQxITP-h_bcz|{Ii_^$qm6lzqVTb%#Jqp^+av$!%~V3@t;aW|rLnLSgM)u- zTkC!+;PhqQ(-D3k>kC2=$yHl`47(b$zHB)>6B-kA1Jb5p9J22TnNqz3ik7HyNQMQw z`bZ=;U+KuV-P9Ym#2eImAVHZzSR6nQn#JH1J(CTY7Zj<-Lnv7P<9a`C%0}&W5ZIk^?M}pTMEg#+BvBGuZ&7KlY(j_zxQVU0Q6D( z0N~4fAvxGRhWr3DdYGPuNG^PA7RR`dC79{WMXz+;IUKpi`AGus&yiB_Hg*1r5&kiB z&On3Iwd{fM@oF_4n^~RSppupr7Kw7g*j!y=WjtEQ9)5tcW`h$7Hi1Cn*G*&|VZ~Dx zKUbsgt=XVkvs8PrxC+3|GkmL~ASb8iT-S=!dnChpcK@&~^I4SqYQ|ili}#)L#?~jC zDIYuuzn%R6NRFu$%zrR$Q4Mbf$3}=mmsUxMIl?GdGc7;u;TVt7KfWUPFHFtfPS!aYHrJP+2;I{2gR^n_mFdkLXbh>c-clN?O4Z`) z(?fOq)!eB9by6UP5h+OR2`!J;#h9^mH>4o?6SZANFUh-|aWme~7q~3pEsp*GuuB+G zkD*tYYEF4LI?(}sDh<6M(#;O{t2h1yZg|0z{RhC#`8-+X2jDgD1u$|$CZPU%*1H{RvJ11puWp=b z4}SpSLQdS)B~kCc9fwCA6FmDjBmEsaul!G-wK*OBV@Tbt`$Nsg>-xxUDuqt>E|`R; zMm;#AZp2t6Mf81bV!~h^M~R1dw8O`-T}&c1f?Br+4FN6Krjg;gV9zQeCy&G-erO8J zM!9uQNhxJs)^)s1S7z2?F>m00t*1A2LrF<_a0`xge((Z`8amo@tZ z=HKioL(-C$^l(^9iBHvoV>Frp&){R1eNTpyEx08;rE1u&JU$V^0xB-*owhJpos_xw@{71!ocSoD=op7}M03@ONPKW)ssCOog=r4Rl(8|#dz#giFbJHYBvT5xi z1NHudDv7@CWE64!3W=HncDVNIe%ZR<~y$F_tS3~-yOEkGgC9)*p38D=j;V!V0Wao+&v@r zK0iE_JR3dY+C5pl%vb$Ruv4&Sh61_-Z3h9g_-{vJtebUB6pGhhFI)Yrjb()*ylS~+gor0WNu>)p>5m_po=ek3xOd)g>B ze=0$cwDUE`KSBtNe4L+3ej)8VxsC1@K9cc202+b0vZUrk$UxR#jrGp^LzoLX?948~ zWuL8>Tr+U28CI8Gl@QUVz%bsi;?K&=$TGU7xX`s8@I*}7_5S9u=fWPBOKsa9Kc^|s z$ikmbjp&8C1u7BM?OwF+;ZH{SI;)Y8-iM7@2_!rZl%a*gXx1II{2VFLb^5KRU2)?< z^mg3yt-Jm7bfK*5H(yAjjad>d-P9QVC%h3apn(nM_Dd%M7QVOoOH=fN=%VY|+uZKz zxGv{n>9077>EDpjw~CQrKZbTBFw@LrR{uHw_DH=-UEK~hKsv?yy47+`R89KaUj2h;)?qB>%MZR{man#a`3a+~ zSASe>|MTE4!utxEE*vRPw4>$;61+sy3-ZPSq@SQsaXd}R6hKko*YIYvvAp*ejujx= z(FVsk7xA;IYu6hVJ9=jqPYHHuvObBDj%gBV{?>DkYB58(bFFto48#< z1HQj6t$3~YypEZI)l+u<^BJjq6x^M?n5b)a3>wtF^1qxxk=eG_kUaN&+HOndi!*E@gDsA4zD#_D7{}b@- z;<~jOgRk?llGK#v)C=jY*gVyg*1=As7(Q;Q zCUY>8vr}a1R95>5oXV`_Hbsfh9oII@E_RAQOSlvSxCe)@Iq2y{>jV)ANeGl>nYPHLD`uW8B1#In$1nkDfOdV8kb@xqjD^bgK-)(lG%9ar9=)qv^ znDXq?r~Te~DS^c~lQUJ5e%ncndKg1dhc~m{beu>(&$K@Z2uY?mK8J(*nlQ|=8njUU zoBSpo9&82cR53TVeX(_uJgdpOO!9#+kC z;wS}VC$)&1a7bENn3KTj8K+K_%Slt7_t_HOb?-Als`l&eh&MWYT~0M~z2sXs{dZ=J z?B8xP1P%41l&}YvYUxhXm4V!Ow)m&ATYbJYY_sN0Ic;deB=$%2;fZADGduUk)Q7a* zav|>VF!)z^)Ke^lET%ssN~B*4Z?Ua>GrGw^^<*Y-iaMomvUh8kqDX)J{k}HeAF4^u zuDcMh#bEAZ2Y+T3Z5}()GjZ~#&{C;pE8`%stpTX@xbD9 zPaxW>Q0)F~7kS{<)p+fg!t7I;xf5%zc5TPEqK~R$LQnLiYr$YM^ink)@H)Bl>+WS) z7es3Mtc=nOj*Wm#dUP$<9CKcgwL4RXK9YNo-04-}?O9*o*+{OJor`d3MCq!%u>OaL zD$rS2H{;Vzn$TDuDo$TrQDQqs;BT5hdlP#YSu|YuhAxo)-^&wMV2uX3|F~_CzQT?}G z8QQHkmq3#@_2w;YELN7wXPe+)(qG~M_zK#qb8qdZ1JX^_Tkwu!`mgUkme|*J9J60= z9|hACH);oM*eO7A%_vM^%Z(etl? zLAHL)c9NDqiCu@^#O~*YCUKYUrvWIHBpYN^h-6aVI1x$3QqAp4?e#Vy6Q+$L=H zy=v%?DA}5{aW<%6^*k}aq`RrwnA2a5+ObL0zrIa{dZ?z)9Cn{Ow93r3efU+!OM1Zk zVl(@Yx57@5PHn2Bie`UmwE@e6eveA>55`c3Z=}ms&&!22M!P6*j`-s0WJ%?b&0)_S zAdfVW5I+7){+wq3Gt9o2F0WK-&!mR@o|DIu4TlDDSk@#XglXEE64^G(OYq#oQ-Ylm z9g+&8pvlcfaZjU2P?1+iAnUVkJjXAXY4`zueD{i2Wj;*PvDXc| zS@%?Jv*AYwrF6W(V|wY@c6xNUC|7=g%)h^d7d_6afceL{suhertaE01(mjyQ6=!=b z-~2THlq3Vx6teBvWLADMQw&S-vCs4T*v-&S`C3WexN{j4Jx-;5!~bLjs5KMPKgaoNk&GwR)(%}u-j3H=NWhGUyN-f z4%%_djBjpxddACuo4WXQ@`a5*m1JSCbvv zAl;{RIPghx^0*$6;kIG2oJ7UmIcP) zy3cd+JTX>dEbRi3yeGdl*vBJI!?{zAv`db#Sb)W?>5z;U%X$0GPIonZlge_Sg^i&E7gEH-HMfjz2GO?D<@`JS%V! zK01cJ<>ZL8DZ6j)^xQTuJ745h`27-3DeAXx`mdGEFZu?1T8af;3;gp^Dx=>rY9)vQ z2xbma#9>W!AKVLa3*b5F`u8kz0)}c|372EFAw+uJ_(juQ^mHn`y5R}q@c8s%7&9Zj z6)%E~0P_#SPv1y`pQc%Rrd}E&#o-KczkpJ!l+j!_XppAo35e(k|3uCud|aB8gR3p> z!?p6_UVnI?%gRW6wWu&Q_ooH|%&H~vCtz>R z=zyi$fYC_%;X=l+eb|U104M_0X%lJVyMOM{IKzlS>-ln=SWQEWsAQ%K3hRFrKN(%I0!g_rOYg&_{7f zC$tMI)io?n-C?l!McIswbMV@ks~Vn`l=UdFb#7zhLfQdqW4pN@LT#Q>aOUfFr(g{=JnIBpLxn+Sm{!MIfYlE}vk%!6OE4hW{$bbCa%ox#zR!MsbG z%{%P+XU0DOM~!_g+V6Wr&QP|)qhoC(>_Yx1+R}!G*M--X4c7w3p#4IIe6 zQN-Y0f7Hw&2wNyK^NQzP!RYg}*0^{*)Q!cuw$4EkuPwVii6?e<B~@Fj^L?cVLvQd2g#6;y_HnbpRi{f0RrpEf&OF&0mO6Id8mpue?obt zgQ>XKJ}43H&2%mPhL}&tSORR#MGMPE;CoSav8R#!-O_qQ3Qhci5$V?Jr(CXLLIEwI znkrS{MjQ6Yc1?>yc+!UJk<$I-i}G7$q1M4J|X)wR8k`7N~x%&vHfxl~4ZwO!g&@MjH(9yPzX6!ynWupj#xl za7rnFr1rK1Y~DP-aBXy40Jq~#H^AP@*A@8uGfx#_JCp>`Qk8g=MA3iSp*S}f&oQ*j z&cz+-Yxc!;-iMJ9f$qdlNG9wbY#;7e7L=Cxo0AZE2-Sw?2zR8p3D3{trUPNAkcpv@ zeoS>Oy@?ikU1WtuM>$v8S7Z4u&KgG69CS$rP<8Ed?(qEJ8x95}TE?Ja>t|KWk+Q~= zYQSd%T*5#$c$0yYsgW9{x^5SqRvOB_$b1xcB{LpPus)27<%ObUV7TZW+W!vB{-suX zp9i)85FpGjC9qz74t~CuNA#9Obq8D&AHhJTbMMruLOC z2)<7q>1DI1-nn%Houx~GE{Q67BJWc4vqQpyMnNmlHufdcZ?-qInQyWDCcl3fIvXke zS9E}qF9};d z6fdzMB((u6bBH>YPBcw>Y7}lkK-bXMWs$hcw%K5(YS7lYB|rJd;|RgR7~p)JZk{@J zgnPkl^T;yB>?KI`R=vdzh7G)??re?1qk#7&3(XC4tF!v7O#F#IBww&kG(#QAv^_nv zNzc~ka=>KLrLJZUhHex5ZDmXEgSorC){=7z-33IkWblv?WwPoWK;08f1f}}t+HxN3 zuXGo?5)7;2iV0MLZrODP(e#DO<@Gv)5-({!cOENREfn&_KJ?l6d~0i%t$@~bGUA__ zmL%b9dHw6Otu73N9fhkyH6~tO`vKsA?01=?+DZ@4DD5ob2F=FiSH*Wn2n3=4LReGT z25R^H9m`SntdS-(pKlqMxm+q!a_E8FF@P_}EM!z*WD?qPZZcI)sHgxeHZ=BeZO>4_ zp(Dhkqn$1a|B*)_Olx(-(Zq7QE=;G(sSKW-l6Nrr(fDq1wOCU~P5g+Ek?+*8oe;+ zJF*lNgS8rtja{qT=s1HqBw5UF7}i3LkIE@va(`S!k8IAm{f~kb7!H7BE-&4*l2O#D zfs3me^ea{#qehHb3|7tM?x^`KH#*R^!KjvL3CJeY;2|FQ zAUaXIVa%r+=&n?1 zx0n>V1PCEyky7yiM0bZ5To$^j#mHn|T1y1!Rl4((QRWXB?l^e@`9`|*Px#~*z#}wW z*VAgCmKd>5zGrufQ*Th#AIU!Mb@Z?ix@PBqI#|jpc;&OVlsJ@apeh?_!(vhG*iU@$ za0%?Xj#cIb#8tBmc10}=-h%c!zBrC3CZ7XWH6}oX8}N+Qse-8>X4l`rRI`Q4_KB7Y z=+&xq;UT>=0n+J<`yS6$d^ScvKHULuOo_k$e4u0;u-g>#?IBL66TH9qhaJ$1fvH_@ks6ZlHbae!Z8(UF(MVwDWFD8 z^R8>F9R@>Usjk)$eu_0--psBy6f2XLqI;0zW>8LPA$`*0pI(Yvz?dHouB`Ef4RvhyFmb= zc&4k{ME-oFMr<`HBrW&E-&5TEK|y&Q6#jJ-XT{EUkb`27xX0+zrJ9IT#l@RGadBdn zz3wz8qNOE1MQ`8XesU>;*L-4NSsPX!54`N&ho5?ZpA!MU&4{kL#edZO?C+k*3QJ8Z zy2zh;fsJ;hC{&iJK#uL4*AVz!8Q$zge=WIF@Ydb{zoobXb#ZB(<`k!!;c|Y&Aqh^p z)w64?2I&+oM`gwTy2t>zokW6W*0&m_N>xfqN(%}h2Fo7fsYz;d?37X=Y5;r>05)HUoy46KY zByoM4baIVCiXS~pom_lq;RN{>LJsVyR8UfrQ|&y;C zpn`aE*1NL~p}TvkI1qq?QCGwnUgyLkcf!bM5`NvSmDIJ4qi@L?E;Um-8ePGHA z`RM%7SD3NjYg4T8%GBm7Xe3}5EVclF*eHd^b(8M5OL0*)m! zx1@fMWIl literal 0 HcmV?d00001 diff --git a/tmp-mes-demandes-tabs-narrow.png b/tmp-mes-demandes-tabs-narrow.png new file mode 100644 index 0000000000000000000000000000000000000000..8f6c18f87b0ab13e24ff2782019bd430b0013446 GIT binary patch literal 31321 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 sI+HqiN1poj5 literal 0 HcmV?d00001 diff --git a/tmp-new-request-entry.png b/tmp-new-request-entry.png new file mode 100644 index 0000000000000000000000000000000000000000..925dea9744f5cff9963318558cf02e075d6c5f74 GIT binary patch literal 30949 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 z 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 O%N4@E0idt`occcjvIBq*RnNkh&# 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 literal 0 HcmV?d00001 diff --git a/tmp-profile-entry.png b/tmp-profile-entry.png new file mode 100644 index 0000000000000000000000000000000000000000..b53d7cd49ec87e978f43c7e2355966c93f97f683 GIT binary patch literal 34977 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 C8j;KZ literal 0 HcmV?d00001 diff --git a/tmp-profile-header-mobile.png b/tmp-profile-header-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..5cd2f2b7075b04746b9a2811a24fcf9efcc5bf0c GIT binary patch literal 34728 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%0AG`U_g@9hf9X>EPm+j# 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 literal 0 HcmV?d00001 diff --git a/tmp-requests-entry.png b/tmp-requests-entry.png new file mode 100644 index 0000000000000000000000000000000000000000..e77cceb9eb91de55ad709a71f527897260e0be95 GIT binary patch literal 33899 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 Yb?hGM)n8Z!za>&C;bU@gyuarE7Zh;lr2qf` literal 0 HcmV?d00001 diff --git a/tmp-requests-header-mobile.png b/tmp-requests-header-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..ed50128713c1f0e59a84d12875df71414b615571 GIT binary patch literal 33895 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 npG7bL04J6^F06eKUh~g2Xlf2 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 BGaLW_ literal 0 HcmV?d00001 diff --git a/uploads/portal/stats-reports/weekly/M4T20z4sa-myDunNJhP8W-rapport-statistiques-ccds-7d-2026-06-14.xlsx b/uploads/portal/stats-reports/weekly/M4T20z4sa-myDunNJhP8W-rapport-statistiques-ccds-7d-2026-06-14.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7e49edd938770e05123af4233bd94caa34ab71ba GIT binary patch literal 24288 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 literal 0 HcmV?d00001 diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..c86f840 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,218 @@ +import { jsxLocPlugin } from "@builder.io/vite-plugin-jsx-loc"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import fs from "node:fs"; +import path from "node:path"; +import { defineConfig, type Plugin, type ViteDevServer } from "vite"; +import { vitePluginManusRuntime } from "vite-plugin-manus-runtime"; + +// ============================================================================= +// Manus Debug Collector - Vite Plugin +// Writes browser logs directly to files, trimmed when exceeding size limit +// ============================================================================= + +const PROJECT_ROOT = import.meta.dirname; +const LOG_DIR = path.join(PROJECT_ROOT, ".manus-logs"); +const MAX_LOG_SIZE_BYTES = 1 * 1024 * 1024; // 1MB per log file +const TRIM_TARGET_BYTES = Math.floor(MAX_LOG_SIZE_BYTES * 0.6); // Trim to 60% to avoid constant re-trimming + +type LogSource = "browserConsole" | "networkRequests" | "sessionReplay"; + +function ensureLogDir() { + if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }); + } +} + +function trimLogFile(logPath: string, maxSize: number) { + try { + if (!fs.existsSync(logPath) || fs.statSync(logPath).size <= maxSize) { + return; + } + + const lines = fs.readFileSync(logPath, "utf-8").split("\n"); + const keptLines: string[] = []; + let keptBytes = 0; + + // Keep newest lines (from end) that fit within 60% of maxSize + const targetSize = TRIM_TARGET_BYTES; + for (let i = lines.length - 1; i >= 0; i--) { + const lineBytes = Buffer.byteLength(`${lines[i]}\n`, "utf-8"); + if (keptBytes + lineBytes > targetSize) break; + keptLines.unshift(lines[i]); + keptBytes += lineBytes; + } + + fs.writeFileSync(logPath, keptLines.join("\n"), "utf-8"); + } catch { + /* ignore trim errors */ + } +} + +function writeToLogFile(source: LogSource, entries: unknown[]) { + if (entries.length === 0) return; + + ensureLogDir(); + const logPath = path.join(LOG_DIR, `${source}.log`); + + // Format entries with timestamps + const lines = entries.map((entry) => { + const ts = new Date().toISOString(); + return `[${ts}] ${JSON.stringify(entry)}`; + }); + + // Append to log file + fs.appendFileSync(logPath, `${lines.join("\n")}\n`, "utf-8"); + + // Trim if exceeds max size + trimLogFile(logPath, MAX_LOG_SIZE_BYTES); +} + +/** + * Vite plugin to collect browser debug logs + * - POST /__manus__/logs: Browser sends logs, written directly to files + * - Files: browserConsole.log, networkRequests.log, sessionReplay.log + * - Auto-trimmed when exceeding 1MB (keeps newest entries) + */ +function vitePluginManusDebugCollector(): Plugin { + return { + name: "manus-debug-collector", + + transformIndexHtml(html) { + if (process.env.NODE_ENV === "production") { + return html; + } + return { + html, + tags: [ + { + tag: "script", + attrs: { + src: "/__manus__/debug-collector.js", + defer: true, + }, + injectTo: "head", + }, + ], + }; + }, + + configureServer(server: ViteDevServer) { + // POST /__manus__/logs: Browser sends logs (written directly to files) + server.middlewares.use("/__manus__/logs", (req, res, next) => { + if (req.method !== "POST") { + return next(); + } + + const handlePayload = (payload: any) => { + // Write logs directly to files + if (payload.consoleLogs?.length > 0) { + writeToLogFile("browserConsole", payload.consoleLogs); + } + if (payload.networkRequests?.length > 0) { + writeToLogFile("networkRequests", payload.networkRequests); + } + if (payload.sessionEvents?.length > 0) { + writeToLogFile("sessionReplay", payload.sessionEvents); + } + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: true })); + }; + + const reqBody = (req as { body?: unknown }).body; + if (reqBody && typeof reqBody === "object") { + try { + handlePayload(reqBody); + } catch (e) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: false, error: String(e) })); + } + return; + } + + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + + req.on("end", () => { + try { + const payload = JSON.parse(body); + handlePayload(payload); + } catch (e) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: false, error: String(e) })); + } + }); + }); + }, + }; +} + +const plugins = [react(), tailwindcss(), jsxLocPlugin(), vitePluginManusRuntime(), vitePluginManusDebugCollector()]; + +export default defineConfig({ + plugins, + resolve: { + alias: { + "@": path.resolve(import.meta.dirname, "client", "src"), + "@shared": path.resolve(import.meta.dirname, "shared"), + "@assets": path.resolve(import.meta.dirname, "attached_assets"), + }, + }, + envDir: path.resolve(import.meta.dirname), + root: path.resolve(import.meta.dirname, "client"), + publicDir: path.resolve(import.meta.dirname, "client", "public"), + build: { + outDir: path.resolve(import.meta.dirname, "dist/public"), + emptyOutDir: true, + rollupOptions: { + output: { + manualChunks(id) { + if (!id.includes("node_modules")) { + return undefined; + } + + if (id.includes("/xlsx/")) { + return "vendor-xlsx"; + } + + if (id.includes("/maplibre-gl/") || id.includes("/leaflet/")) { + return "vendor-maps"; + } + + if (id.includes("/recharts/")) { + return "vendor-charts"; + } + + if (id.includes("/react-hook-form/") || id.includes("/@hookform/")) { + return "vendor-forms"; + } + + if (id.includes("/@radix-ui/") || id.includes("/vaul/")) { + return "vendor-radix"; + } + + return "vendor-misc"; + }, + }, + }, + }, + server: { + host: true, + allowedHosts: [ + ".manuspre.computer", + ".manus.computer", + ".manus-asia.computer", + ".manuscomputer.ai", + ".manusvm.computer", + "localhost", + "127.0.0.1", + ], + fs: { + strict: true, + deny: ["**/.*"], + }, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c549003 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +const templateRoot = path.resolve(import.meta.dirname); + +export default defineConfig({ + root: templateRoot, + resolve: { + alias: { + "@": path.resolve(templateRoot, "client", "src"), + "@shared": path.resolve(templateRoot, "shared"), + "@assets": path.resolve(templateRoot, "attached_assets"), + }, + }, + test: { + environment: "node", + include: ["server/**/*.test.ts", "server/**/*.spec.ts"], + }, +});

+
+
+

Chargement de l’interface...

+
+
+ ); +} + +function withRouteSuspense(Component: ComponentType) { + return function SuspendedRoute(props: T) { + return ( + }> + + + ); + }; +} + +function withDashboardLayout(Component: ComponentType) { + return function DashboardRoute(props: T) { + return ( + + }> + + + + ); + }; +} + +const HomeRoute = withRouteSuspense(Home); +const LoginRoute = withRouteSuspense(Login); +const InvitationRoute = withRouteSuspense(AssociationInvitation); +const MaterialReturnFinalPdfRoute = withRouteSuspense(MaterialReturnFinalPdfView); +const MaterialReturnUploadRoute = withRouteSuspense(MaterialReturnUpload); +const TerrainMissionsRoute = withRouteSuspense(TerrainMissions); +const AssociationDirectoryRoute = withRouteSuspense(AssociationDirectoryPortal); +const AssociationMapRoute = withRouteSuspense(AssociationMapPublic); +const AssociationPublicProfileRoute = withRouteSuspense(AssociationPublicProfile); +const DashboardHomeRoute = withRouteSuspense(Dashboard); +const DashboardProfileRoute = withDashboardLayout(Profile); +const DashboardDocumentsRoute = withDashboardLayout(Documents); +const DashboardNewRequestRoute = withDashboardLayout(NewRequest); +const DashboardReservationSalleRoute = withDashboardLayout(ReservationSalle); +const DashboardMaterialRoute = withDashboardLayout(DemandeMaterielEvenementiel); +const DashboardDirectoryRoute = withDashboardLayout(AssociationDirectoryPortal); +const DashboardRequestsRoute = withDashboardLayout(Requests); +const DashboardRequestDetailRoute = withDashboardLayout(RequestDetail); +const DashboardEditRequestRoute = withDashboardLayout(EditRequest); +const MaterialConventionRoute = withRouteSuspense(MaterialConventionDownload); +const SalleWorkflowDocumentRoute = withRouteSuspense(SalleWorkflowDocumentDownload); +const StatsReportArchiveRoute = withRouteSuspense(StatsReportArchiveView); +const AdminRoute = withDashboardLayout(Admin); +const FAQRoute = withRouteSuspense(FAQ); +const PrivacyPolicyRoute = withRouteSuspense(PrivacyPolicy); +const LegalNoticeRoute = withRouteSuspense(LegalNotice); +const CookiesPolicyRoute = withRouteSuspense(CookiesPolicy); +const TermsOfServiceRoute = withRouteSuspense(TermsOfService); +const NotFoundRoute = withRouteSuspense(NotFound); + +function AppearanceDecorationLayer() { + const [location] = useLocation(); + const { appearance } = useTheme(); + const scope = getBackgroundScopeForPath(location); + const decorativeElements = (appearance.decorativeElements || []).filter( + (entry) => entry.imageUrl && entry.layer === "background" && isDecorationScopeActive(entry.scope, scope), + ); + + if (!appearance.backgroundEnabled || decorativeElements.length === 0) { + return null; + } + + return ( + + ); +} + +function AppearanceScopeBridge() { + const [location] = useLocation(); + const { appearance, resolvedTheme } = useTheme(); + + useEffect(() => { + const root = document.documentElement; + const scope = getBackgroundScopeForPath(location); + const resolvedBackgroundImage = resolveBackgroundImageForScope(scope, appearance); + const resolvedBackgroundOverlayOpacity = resolveBackgroundOverlayOpacityForScope(scope, appearance); + const resolvedBackgroundPosition = resolveBackgroundPositionForScope(scope, appearance); + const resolvedBackgroundBlur = resolveBackgroundBlurForScope(scope, appearance); + const resolvedBackgroundPresentationMode = resolveBackgroundPresentationModeForScope(scope, appearance); + const applies = + appearance.backgroundEnabled && + getBackgroundScopeAliases(scope).some((candidate) => + appearance.backgroundTargets.includes(candidate as typeof appearance.backgroundTargets[number]) + ); + root.style.setProperty( + "--portal-background-image", + applies && resolvedBackgroundImage ? `url("${resolvedBackgroundImage}")` : "none" + ); + root.style.setProperty( + "--portal-background-overlay", + resolvedTheme === "dark" + ? `rgba(6, 12, 18, ${resolvedBackgroundOverlayOpacity / 100})` + : `rgba(247, 250, 252, ${resolvedBackgroundOverlayOpacity / 100})` + ); + root.style.setProperty("--portal-background-position", resolvedBackgroundPosition); + root.style.setProperty("--portal-background-blur", `${resolvedBackgroundBlur}px`); + root.style.setProperty("--portal-background-presentation-mode", resolvedBackgroundPresentationMode); + root.style.setProperty( + "--portal-background-center-overlay", + resolvedBackgroundPresentationMode === "edge_frame" + ? (resolvedTheme === "dark" ? "rgba(6, 12, 18, 0.84)" : "rgba(255, 255, 255, 0.88)") + : (resolvedTheme === "dark" ? "rgba(6, 12, 18, 0.16)" : "rgba(255, 255, 255, 0.12)") + ); + root.style.setProperty( + "--portal-background-edge-strength", + resolvedBackgroundPresentationMode === "edge_frame" + ? (resolvedTheme === "dark" ? "0.98" : "0.94") + : (resolvedTheme === "dark" ? "0.28" : "0.22") + ); + root.dataset.portalBackgroundScope = scope; + root.dataset.portalBackgroundActive = applies ? "true" : "false"; + root.dataset.portalBackgroundPresentationMode = resolvedBackgroundPresentationMode; + }, [ + appearance.backgroundEnabled, + appearance.backgroundImageUrl, + appearance.backgroundMode, + appearance.backgroundOverlayOpacity, + appearance.backgroundBlurRadius, + appearance.backgroundPresentationMode, + appearance.backgroundTargetImageUrls, + appearance.backgroundTargetBlurRadii, + appearance.backgroundTargetOverlayOpacities, + appearance.backgroundTargetPositions, + appearance.backgroundTargetPresentationModes, + appearance.backgroundTargets, + location, + resolvedTheme, + ]); + + return null; +} + +function Router() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function App() { + return ( + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/client/src/_core/hooks/useAuth.ts b/client/src/_core/hooks/useAuth.ts new file mode 100644 index 0000000..806b9d1 --- /dev/null +++ b/client/src/_core/hooks/useAuth.ts @@ -0,0 +1,111 @@ +import { getLoginUrl } from "@/const"; +import { trpc } from "@/lib/trpc"; +import { TRPCClientError } from "@trpc/client"; +import { useCallback, useEffect, useMemo } from "react"; + +type UseAuthOptions = { + redirectOnUnauthenticated?: boolean; + redirectPath?: string; +}; + +export function useAuth(options?: UseAuthOptions) { + const { redirectOnUnauthenticated = false, redirectPath = getLoginUrl() } = + options ?? {}; + const utils = trpc.useUtils(); + + const meQuery = trpc.auth.me.useQuery(undefined, { + retry: false, + refetchOnWindowFocus: false, + }); + const providersQuery = trpc.auth.providers.useQuery(undefined, { + retry: false, + refetchOnWindowFocus: false, + }); + + const logoutMutation = trpc.auth.logout.useMutation({ + onSuccess: () => { + utils.auth.me.setData(undefined, null); + }, + }); + + const loginMutation = trpc.auth.login.useMutation({ + onSuccess: result => { + if (!result.mfaRequired) { + utils.auth.me.setData(undefined, result.user); + } + }, + }); + + const registerMutation = trpc.auth.register.useMutation({ + onSuccess: user => { + utils.auth.me.setData(undefined, user); + }, + }); + + const logout = useCallback(async () => { + try { + await logoutMutation.mutateAsync(); + } catch (error: unknown) { + if ( + error instanceof TRPCClientError && + error.data?.code === "UNAUTHORIZED" + ) { + return; + } + throw error; + } finally { + utils.auth.me.setData(undefined, null); + await utils.auth.me.invalidate(); + } + }, [logoutMutation, utils]); + + const state = useMemo(() => { + localStorage.setItem( + "local-auth-user-info", + JSON.stringify(meQuery.data) + ); + return { + user: meQuery.data ?? null, + loading: meQuery.isLoading || logoutMutation.isPending || loginMutation.isPending || registerMutation.isPending, + error: meQuery.error ?? logoutMutation.error ?? loginMutation.error ?? registerMutation.error ?? null, + isAuthenticated: Boolean(meQuery.data), + providers: providersQuery.data ?? { google: false, facebook: false }, + }; + }, [ + meQuery.data, + meQuery.error, + meQuery.isLoading, + providersQuery.data, + logoutMutation.error, + logoutMutation.isPending, + loginMutation.error, + loginMutation.isPending, + registerMutation.error, + registerMutation.isPending, + ]); + + useEffect(() => { + if (!redirectOnUnauthenticated) return; + if (meQuery.isLoading || logoutMutation.isPending) return; + if (state.user) return; + if (typeof window === "undefined") return; + if (window.location.pathname === redirectPath) return; + + window.location.href = redirectPath + }, [ + redirectOnUnauthenticated, + redirectPath, + logoutMutation.isPending, + meQuery.isLoading, + state.user, + ]); + + return { + ...state, + refresh: () => meQuery.refetch(), + login: loginMutation.mutateAsync, + register: registerMutation.mutateAsync, + providersLoading: providersQuery.isLoading, + logout, + }; +} diff --git a/client/src/assets/ccds.png b/client/src/assets/ccds.png new file mode 100644 index 0000000000000000000000000000000000000000..8187d4d702445e28611267bed204c298085a675c GIT binary patch literal 22907 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/components/AIChatBox.tsx b/client/src/components/AIChatBox.tsx new file mode 100644 index 0000000..1c00871 --- /dev/null +++ b/client/src/components/AIChatBox.tsx @@ -0,0 +1,335 @@ +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import { Loader2, Send, User, Sparkles } from "lucide-react"; +import { useState, useEffect, useRef } from "react"; +import { Streamdown } from "streamdown"; + +/** + * Message type matching server-side LLM Message interface + */ +export type Message = { + role: "system" | "user" | "assistant"; + content: string; +}; + +export type AIChatBoxProps = { + /** + * Messages array to display in the chat. + * Should match the format used by invokeLLM on the server. + */ + messages: Message[]; + + /** + * Callback when user sends a message. + * Typically you'll call a tRPC mutation here to invoke the LLM. + */ + onSendMessage: (content: string) => void; + + /** + * Whether the AI is currently generating a response + */ + isLoading?: boolean; + + /** + * Placeholder text for the input field + */ + placeholder?: string; + + /** + * Custom className for the container + */ + className?: string; + + /** + * Height of the chat box (default: 600px) + */ + height?: string | number; + + /** + * Empty state message to display when no messages + */ + emptyStateMessage?: string; + + /** + * Suggested prompts to display in empty state + * Click to send directly + */ + suggestedPrompts?: string[]; +}; + +/** + * A ready-to-use AI chat box component that integrates with the LLM system. + * + * Features: + * - Matches server-side Message interface for seamless integration + * - Markdown rendering with Streamdown + * - Auto-scrolls to latest message + * - Loading states + * - Uses global theme colors from index.css + * + * @example + * ```tsx + * const ChatPage = () => { + * const [messages, setMessages] = useState([ + * { role: "system", content: "You are a helpful assistant." } + * ]); + * + * const chatMutation = trpc.ai.chat.useMutation({ + * onSuccess: (response) => { + * // Assuming your tRPC endpoint returns the AI response as a string + * setMessages(prev => [...prev, { + * role: "assistant", + * content: response + * }]); + * }, + * onError: (error) => { + * console.error("Chat error:", error); + * // Optionally show error message to user + * } + * }); + * + * const handleSend = (content: string) => { + * const newMessages = [...messages, { role: "user", content }]; + * setMessages(newMessages); + * chatMutation.mutate({ messages: newMessages }); + * }; + * + * return ( + * + * ); + * }; + * ``` + */ +export function AIChatBox({ + messages, + onSendMessage, + isLoading = false, + placeholder = "Type your message...", + className, + height = "600px", + emptyStateMessage = "Start a conversation with AI", + suggestedPrompts, +}: AIChatBoxProps) { + const [input, setInput] = useState(""); + const scrollAreaRef = useRef(null); + const containerRef = useRef(null); + const inputAreaRef = useRef(null); + const textareaRef = useRef(null); + + // Filter out system messages + const displayMessages = messages.filter((msg) => msg.role !== "system"); + + // Calculate min-height for last assistant message to push user message to top + const [minHeightForLastMessage, setMinHeightForLastMessage] = useState(0); + + useEffect(() => { + if (containerRef.current && inputAreaRef.current) { + const containerHeight = containerRef.current.offsetHeight; + const inputHeight = inputAreaRef.current.offsetHeight; + const scrollAreaHeight = containerHeight - inputHeight; + + // Reserve space for: + // - padding (p-4 = 32px top+bottom) + // - user message: 40px (item height) + 16px (margin-top from space-y-4) = 56px + // Note: margin-bottom is not counted because it naturally pushes the assistant message down + const userMessageReservedHeight = 56; + const calculatedHeight = scrollAreaHeight - 32 - userMessageReservedHeight; + + setMinHeightForLastMessage(Math.max(0, calculatedHeight)); + } + }, []); + + // Scroll to bottom helper function with smooth animation + const scrollToBottom = () => { + const viewport = scrollAreaRef.current?.querySelector( + '[data-radix-scroll-area-viewport]' + ) as HTMLDivElement; + + if (viewport) { + requestAnimationFrame(() => { + viewport.scrollTo({ + top: viewport.scrollHeight, + behavior: 'smooth' + }); + }); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmedInput = input.trim(); + if (!trimmedInput || isLoading) return; + + onSendMessage(trimmedInput); + setInput(""); + + // Scroll immediately after sending + scrollToBottom(); + + // Keep focus on input + textareaRef.current?.focus(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + return ( +
+ {/* Messages Area */} +
+ {displayMessages.length === 0 ? ( +
+
+
+ +

{emptyStateMessage}

+
+ + {suggestedPrompts && suggestedPrompts.length > 0 && ( +
+ {suggestedPrompts.map((prompt, index) => ( + + ))} +
+ )} +
+
+ ) : ( + +
+ {displayMessages.map((message, index) => { + // Apply min-height to last message only if NOT loading (when loading, the loading indicator gets it) + const isLastMessage = index === displayMessages.length - 1; + const shouldApplyMinHeight = + isLastMessage && !isLoading && minHeightForLastMessage > 0; + + return ( +
+ {message.role === "assistant" && ( +
+ +
+ )} + +
+ {message.role === "assistant" ? ( +
+ {message.content} +
+ ) : ( +

+ {message.content} +

+ )} +
+ + {message.role === "user" && ( +
+ +
+ )} +
+ ); + })} + + {isLoading && ( +
0 + ? { minHeight: `${minHeightForLastMessage}px` } + : undefined + } + > +
+ +
+
+ +
+
+ )} +
+
+ )} +
+ + {/* Input Area */} +
+ +
+ +
+ +
+ Annuler + +