Journal des modifications
Toutes les modifications notables de Moorly sont documentées ici.
Version actuelle : v1.25.1[1.25.1] — 2026-05-21
Fix — Tarification : décimales de pied perdues par `round()` sans précision
[1.25.0] — 2026-05-21
Feat — Rapport financier : génération PDF réelle
[1.24.4] — 2026-05-21
Ux — Rapport financier : ordre des colonnes + clarification taxes
- "Tarif" et "Coût svc." inversés — l'ordre lecture est maintenant : Services → Tarif (amarrage) → Coût svc. → Facturé. Lecture de gauche à droite plus naturelle (HT puis TTC).
- En-tête "Facturé" devient "Facturé (TTC)" (FR) / "Invoiced (incl. tax)" (EN) pour indiquer explicitement que ce montant inclut TPS/TVQ.
[1.24.3] — 2026-05-21
Ux — Rapport financier : colonnes du tableau ajustées
- No officiel TC remplace l'immatriculation interne (`b.official_number` au lieu de `b.registration_number`) — plus utile pour réconcilier avec les rapports Transport Canada.
- Emplacement retiré (peu informatif quand beaucoup d'ententes n'ont qu'un port, pas un slot précis).
- Coût services ajouté entre "Services" et "Tarif" — somme de `dock_services_detail[].price` (exclut `quai` qui est dans le tarif d'amarrage). Affiche `—` si aucun service additionnel.
[1.24.2] — 2026-05-21
Fix — Rapport financier : "Quai" affiché pour toutes les ententes mooring
[1.24.1] — 2026-05-21
Fix — Rapport financier : affichage des services
[1.24.0] — 2026-05-21
Feat — Rapport financier : wizard de revérification des montants
- Montant total recalculé ≠ `entente.total_amount` (changement de tarif/service en cours de saison)
- Aucune facture générée pour l'entente
- `facture.total` ≠ `entente.total_amount` (désync)
- Facture déjà payée avec écart → avertissement non-actionnable
- Contexte read-only : client, bateau (avec largeur en pi), port, type, période, statut facture
- Tableau diff : tarif amarrage / services / sous-total / TPS-TVQ / total — lignes changées en jaune, deltas en rouge/vert
- Éditeur : checkboxes services (cochées selon recalcul), input total override
- Recalcul live côté JS quand on toggle un service (sauf si override déjà tapé)
- Bouton Régénérer la facture (UPDATE existante) ou Générer la facture (INSERT si manquante)
- Bouton Suivant pour skip sans modifier
- Carte récap à la fin
- `EntentesService::previewRecompute()` — extraction pure du calcul (refactor de `recompute()` existant, zéro changement de comportement pour l'endpoint `POST /api/ententes/{id}/recompute`)
- `EntentesService::TAX_RATE` — constante publique (DRY, single source of truth pour le 14.975%)
- `RapportFinancierService::recheck($portId, $year)` — boucle batch + tableau de rows à reviewer + summary. Subquery pin la facture la plus récente par entente pour éviter le double-count si plusieurs factures.
- `RapportFinancierService::regenerate($ententeId, $services, $total)` — UPDATE entente + UPDATE/INSERT facture en transaction (savepoint-compatible pour tests), refuse si facture payée
- `POST /api/rapport-financier/recheck` — body `{port_id, year}` → `{summary, rows}`
- `POST /api/rapport-financier/regenerate` — body `{entente_id, dock_services, total_amount}` → `{success, facture_id, created, new_total}`. Scope admin vérifié sur l'entente. Status codes : 409 si facture payée, 404 si entente introuvable, 403 hors scope, 500 erreur infra.
[1.23.2] — 2026-05-08
Feat — Dashboard : scope par administration
Test infrastructure
[1.23.1] — 2026-05-08
Fix — Bucket C : compléter le verrouillage license_admin
- Liens Administrations + Ports dans la section Infrastructure → cachés pour non-license_admin (Quais reste accessible avec scope filter)
- Section Administration complète (Analytiques, Configuration, Contenu du site, Licence, Utilisateurs) → cachée pour non-license_admin
- `AdministrationsController` (8 méthodes)
- `PortsController` (6 méthodes)
- `ConfigurationController` (4 méthodes)
- `CmsController` (9 méthodes)
- `LicenceController` (1 méthode)
[1.23.0] — 2026-05-08
Feat — Tenant : scoping des admins par administration portuaire
- Scoped : ententes, factures, renouvellements, demandes, quais, emplacements, tarifs, permis, infractions, incidents, machinerie, rapports financiers
- Global (tous voient tout) : bateaux, clients, documents légaux du tenant
- License_admin only : settings tenant, admin des utilisateurs, admin des administrations
- `Auth::scopedAdministrationIds()` — retourne `null` (unrestricted) ou `array` d'UUIDs. Lit le user JWT en production, fallback sur `$_SESSION['user']` pour les tests
- `ScopeHelper::adminFilter('p')` — retourne `[$sql, $params]` à injecter dans les WHERE clauses
- UI multi-checkbox dans le modal utilisateur — remplace le textarea JSON brut. Master "Toutes" + checkboxes par administration + warning visuel si admin sans restriction
- Defense-in-depth : `UtilisateursController::resolvePortAccess()` bloque la modification de `port_access` par non-license_admin
[1.22.1] — 2026-05-07
UX — Bateaux : indicateurs visuels pour `tc_inactive`
- Modal : bandeau rouge sticky en haut quand `tc_inactive=1`. Apparaît à l'ouverture et toggle live au clic sur la checkbox. Pattern : background `#fee2e2`, border-left `#dc2626`, icône `fa-exclamation-triangle`.
- DataTable : pastille rouge avec icône `fa-ban` (cercle barré) en bottom-right corner du `.ui-row-icon`. Background blanc + border-radius 50% pour isoler du fond coloré du parent. Le badge texte "TC inactif" reste — l'overlay sert au scan rapide, le texte explicite le sens.
[1.22.0] — 2026-05-07
Feat — Bateaux : Gross Tonnage + Port of Registry persistés + tc-verify-bulk section toggle
- Section toggle : 3 boutons en haut de page — `Les 2 sections` / `Section 1 seulement` / `Section 2 seulement`. URL query string `?sections=both|verify|search`. Server-side render conditionnel — la section non-active n'apparaît pas du tout (pas de skeleton, pas de fetches JS). Pratique pour focus sur une section sans le surcoût de l'autre (~5-10 min de fetches économisés).
- 2 nouvelles colonnes comparées en Section 1 : GT et Port of Registry. Diff highlighté comme les autres champs. Tolérance 0.005 pour GT (cohérent avec length/width/draft). Comparaison string `.trim()` pour port.
- Colonne "Bonus TC" simplifiée : maintenant juste un badge avec le statut TC (REGISTERED/CLOSED/SUSPENDED) — plus la duplication de GT/port qui sont en colonnes dédiées.
- POST handler : accepte `gross_tonnage` (numérique > 0, < 100000) et `port_of_registry` (texte trim, < 100 chars) dans le payload. UPDATE SQL inclut les 2 colonnes via COALESCE.
[1.21.1] — 2026-05-07
Feat — TC lookup retourne aussi le statut + tc-verify-bulk auto-tag CLOSED/SUSPENDED
- Section 1 (verify) : compare aussi `bateaux.tc_inactive` avec le statut TC (CLOSED/SUSPENDED → tc_inactive devrait être 1, REGISTERED → 0). Diff détecté → ajouté à la liste des champs à updater. Badge statut affiche maintenant le TC status quand non-REGISTERED (ex: `diff (CLOSED)`, `à jour (SUSPENDED)`).
- Section 2 (search) : ne filtre plus uniquement REGISTERED. Si 0 REGISTERED mais des CLOSED/SUSPENDED existent, ces résultats sont proposés à la place — match coché par défaut, applique `official_number` ET `tc_inactive=1`. Les radio buttons multi-choix montrent un mini-badge avec le statut. Soumission propage le `tc_inactive` correct selon le choix.
[1.21.0] — 2026-05-07
Feat — Bateaux : flag "TC inactif" (CLOSED/SUSPENDED)
[1.20.3] — 2026-05-06
Fix — TC lookup : entités HTML non décodées dans les champs texte
[1.20.2] — 2026-05-06
Fix — Ententes : `update()` n'incluait pas `port_id` dans l'UPDATE SQL
[1.20.1] — 2026-05-05
Fix — Ententes : port jamais sauvegardé sur `e.port_id`
[1.20.0] — 2026-05-05
Feat — Ententes : cas de location (locataire ≠ propriétaire)
- Modal entente : checkbox + dropdown locataire + auto-détection en édition (case auto-cochée si `e.client_id !== bateau_proprio_id`) + validation au submit
- Liste ententes : badge Location (jaune) à côté du nom du signataire quand applicable
- Fiche bateau (modal Contrats) : nouvelle colonne Signataire avec badge Location
- Fiche client : nouvelle section "Bateaux loués" (read-only, masquée si vide) listant les ententes actives où ce client est propriétaire du bateau mais pas signataire — clic sur card ouvre l'entente
- EntentesService : queries `getList`, `find`, `listByBateau` distinguent maintenant signataire (`e.client_id` → alias `c`) du propriétaire bateau (`b.client_id` → alias `cp`), exposent `bateau_proprio_id`
[1.19.6] — 2026-05-05
UX — Ententes : retirer la séparation entre hivernal et estival
[1.19.5] — 2026-05-05
UX — Ententes : regrouper hivernal + estival sous "Parc à bateau"
[1.19.4] — 2026-05-05
Feat — Ententes : exposer "Parc estival" en plus de "Parc d'hivernement"
[1.19.3] — 2026-05-05
Fix — Ententes : page blanche en français (apostrophe non échappée)
[1.19.2] — 2026-05-04
Fix — Import KML : POST jamais déclenché après clic côté eau
[1.19.1] — 2026-05-04
Fix — Import KML : finitions UX
- Curseur bloqué : après *Sauvegarder* → *Ignorer* (ou *Annuler tout*) en mode "choisir côté eau", le curseur de la carte restait en `pointer` jusqu'à un click ailleurs. Reset propre dans `cleanupImport()` et `processNextImport()`.
- Feedback UX manquant : pendant l'étape "cliquer côté eau" en mode import, le bouton *Sauvegarder* restait inchangé et `importStatus` ne donnait aucune indication. Maintenant : bouton désactivé "En attente du clic..." + status en bleu "👆 Cliquez du côté EAU" — symétrique au flow manuel `+ Tracer`.
- Nettoyage : retrait d'un commentaire scaffolding "Temporary stub" obsolète et d'un guard `typeof cancelDraw === 'function'` mort (la fonction est toujours définie dans la même portée IIFE).
[1.19.0] — 2026-05-04
Feat — Import KML pour les lignes d'amarrage (`/quais`)
[1.18.98] — 2026-04-29
Fix — Upload bathymétrie : csrf_invalid
[1.18.97] — 2026-04-29
Fix — Toggle FR/EN backend : cookie résiduel host-only
i18n backend — Synthèse globale (v1.18.92 → v1.18.96)
- 37 vues backend (chrome + tables + modals + JS-rendered + PDF templates)
- 32 controllers (jsonResponse error/success, page titles, validation messages)
- Infrastructure : layout `dashboard.php` débridé (`I18n::locale()` au lieu de hardcode 'fr' pour staff), route `/lang/(fr|en)` accessible depuis tout sous-domaine, sidebar avec toggle FR/EN dans le user dropdown
- Bateaux→Boats, Ententes→Agreements, Quais→Docks, Ports→Harbours (préférence utilisateur), Emplacements→Slips, Tarifs→Rates, Conformité→Compliance, Facturation→Invoicing, Renouvellements→Renewals, Demandes→Requests, Permis→Permits, Comptabilité→Accounting, Machinerie→Machinery, Signalements→Incidents, Alertes→Alerts, Utilisateurs→Users, Administrations→Administrations
- Taxes : TPS→GST, TVQ→QST, TVH→HST, NE→BN
- Préfixes : FAC-→INV- en EN
- DataTables : `fr-FR.json` ↔ `en-GB.json`
- Locale formattage : `fr-CA` ↔ `en-CA` pour `toLocaleString`/`toLocaleDateString`
- Templates juridiques PPB/SCH (boîte à outils) : artefacts canadiens FR officiels, banner EN d'explication ajouté
- StripeController : messages bubble-up de StripeService (couche service, hors scope)
- CSV exports comptabilité : entêtes pour réimport dans logiciels (pas user-facing)
- `error_log()`, exceptions techniques, comments, SQL, enum keys, IDs HTML, brand "Moorly"
- Strings éventuellement manquées dans des modals rares ou flows edge-case (faire un walkthrough EN complet en prod)
- Layout `client.php` (vue client portal) déjà partiellement bilingue avant cette session — le pattern utilisait `$en` au lieu du canonique `$_en`. Si tu vois des strings non traduites côté client, c'est probablement là.
- Emails transactionnels (renouvellement, factures) : pas couverts par cette session — gérer via le système Email/Notifier existant
- Documents légaux (cms_pages content_fr/content_en) : déjà bilingues via le CMS, mais le contenu doit exister dans les deux langues
[1.18.96] — 2026-04-29
i18n backend — Phase 5 : controllers (flash/error messages)
- Batch A (~64 chaînes) : MooringMapController, CmsController, EntentesController, TarifsController, ClientPortalController, BookingController, BateauxController, UtilisateursController, PosController (incluant le template HTML du reçu thermal POS entièrement traduit), AlertsController, PlatformController.
- Batch B (~75 chaînes) : MobileController, TcLookupController, IncidentsController, ClientRenewalController, ChatbotController, RenewalsController, PortsController, NetworkController, LegalDocsController, InfractionsController, FacturationController, DashboardController, AuthController (registration + login + change password — tous les messages de validation), AdministrationsController, ClientsController, ConfigurationController, ConformiteController, ComptabiliteController, MachinerieController, PermisController.
- StripeController : messages d'erreur viennent de `StripeService` (couche service, hors scope vue/controller)
- DocumentsController, PaymentInstallmentsController : déjà uniquement en anglais
- `error_log()`, `throw new \Exception(...)` : logs serveur, pas user-facing
- ChatbotController system prompt : envoyé à Claude qui répond dans la langue de l'utilisateur (le LLM gère)
- ComptabiliteController CSV headers : entêtes de fichier réimportable dans logiciel comptable
- PPB toolkit legal templates : artefacts juridiques canadiens en FR (banner EN d'explication ajouté en Phase 4)
[1.18.95] — 2026-04-29
i18n backend — Phase 4 : modules P3 (specialty)
- machinerie/incidents/demandes/renewals (~245 chaînes) : opérations terrain, signalements, demandes de réservation, génération de renouvellements.
- comptabilite/rapports/transactions (~135 chaînes) : exports comptables (Sage/QuickBooks/Acomba/CSV), rapport financier interactif, journal des paiements.
- legal-docs/compliance-docs/alerts/permis (~165 chaînes) : éditeur de docs légaux + chrome (le contenu cms_pages reste bilingue via content_fr/content_en), suivi documentaire, alertes/annonces, permis commerciaux.
- pos/ppb-outils/search/aide (~202 chaînes) : POS terminal + catalogue + rapports (formattage monétaire CA/FR), boîte à outils SCH (templates légaux laissés en FR — documents officiels canadiens, banner EN d'explication ajouté), recherche globale, centre d'aide.
- Format monétaire POS bascule entre `$0.00` (EN) et `0,00 $` (FR) via `fmtMoney()`.
- POS items utilisent `name_en ?: name_fr` avec fallback FR si EN manquant.
- Acronymes officiels Pêches et Océans : PPB → SCH (Small Craft Harbours), LPPP → FRHA, RPPP → FRHR.
- Templates légaux PPB délibérément non traduits (artefacts juridiques canadiens en FR).
[1.18.94] — 2026-04-29
i18n backend — Phase 3 : modules P2 (usage quotidien)
- ports/ + quais/ (~155 chaînes) : Quai → Dock, configuration des lignes d'amarrage, bathymétrie, légende 3D, popups carte.
- configuration/ + utilisateurs/ + licence/ (~230 chaînes) : 9 onglets settings (Org/Tarification/Taxes/Renouvellements/Stripe/IA/Widget/Système/Réseau/Compte), gestion utilisateurs, plans + Danger zone.
- infractions/ + conformite/ (~195 chaînes) : avis formel d'infraction (lettre PDF avec date `F j, Y` en EN), tracking documents, types et statuts.
- tarifs/ + administrations/ (~145 chaînes) : grille tarifaire amarrage/services/périodes, fiche admin (officiers, membres CA, bail).
[1.18.93] — 2026-04-29
i18n backend — Phase 2 : modules P1 (haute-fréquence)
- dashboard/ (3 fichiers, ~120 chaînes) : KPIs, cartes (Mouvement par port → Movement by harbour, Occupation, Renouvellements, Activité récente), heatmap d'occupation, tables de non-renouvellements et nouveaux arrivés (avec PDFs imprimables bilingues — toLocaleString switché fr-CA/en-CA), libellés des mois, alertes.
- clients/ (2 fichiers, ~140 chaînes) : liste + DataTables, modal édition (Renseignements/Navires/Ententes/Factures/Dossier), filtres, status maps, batch operations, prompts/confirms.
- bateaux/ (~95 chaînes) : KPIs, filtres (entente/port/assurance/type), 5 onglets de modal (Identité/Dimensions/Assurance/Documents/Contrats), import TC, alerts/confirms.
- ententes/ (~125 chaînes) : KPIs, filtres, 4 onglets (Détails/Services/Paiements/Signature), méthodes de paiement, méthode de calcul de tarif, signature.
- facturation/ (2 fichiers, ~95 chaînes) : index + print PDF. Préfixe `FAC-` → `INV-` côté affichage EN. Taxes traduites (TPS→GST, TVQ→QST, TVH→HST, NE→BN). Mois courts FR/EN dans `fmtShortDate`.
[1.18.92] — 2026-04-29
i18n backend — Phase 1 : infrastructure
- `layouts/dashboard.php` : `$_lang = I18n::locale()` au lieu de `$_lang = 'fr'`. Pour les clients sans cookie défini, on initialise le cookie à partir de leur `clients.language_pref` (continuité de session).
- `index.php` : la route `/lang/(fr|en)` est sortie du block `if (!$tenant)` — fonctionne maintenant depuis n'importe quel sous-domaine tenant. Cookie set sur `.moorly.ca` (déjà le cas).
- `partials/sidebar.php` :
- Tous les libellés du menu staff (Administration, Configuration, Analytiques, Clients, Bateaux, Ententes, Conformité, Documents légaux, Infractions, Suivi documentaire, Comptabilité, Facturation, Tarifs, Transactions, Administrations, Ports, Quais, Machinerie, Point de vente, Signalements, Alertes, App mobile, Aide, Changer le mot de passe, Déconnexion) wrapped en `$_en ? 'EN' : 'FR'`.
- Toggle FR/EN ajouté dans le user dropdown (visible staff + clients), avec checkmark sur la langue active.
[1.18.91] — 2026-04-29
Fix — Création de tenant : schéma canonique au lieu des migrations Prisma cassées
- `prisma/tenant/_schema.sql` (851 lignes, 43 tables) — généré via `mysqldump --no-data` depuis un tenant prod connu-bon (`moorly_adminportuairedesiles`), avec `_prisma_migrations` retiré et un wrap `SET FOREIGN_KEY_CHECKS=0/1` pour gérer l'ordre alphabétique des CREATE TABLE.
- `prisma/tenant/_seed.sql` — extrait les `INSERT IGNORE INTO cms_pages` du migration 20260401041000 (16 pages : accueil, services + sous-pages, havres, règlements + sous-pages, FAQ, contact).
- `AuthController::register()` charge `_schema.sql` puis `_seed.sql` via `mysql` CLI (avec `MYSQL_PWD=` pour éviter le warning password-on-CLI), à la place de `npx prisma migrate deploy`. L'échec du seed n'est pas fatal (logué seulement) ; l'échec du schéma jette une exception et bloque la création.
[1.18.90] — 2026-04-28
Fix — fetchNewArrivals : `Unknown column 'e_new.type'`
[1.18.89] — 2026-04-28
UX — Mouvement par port : section « Nouveaux N-1 → N »
- `DashboardService::fetchNewArrivals($year, $lastYear, $scope)` — bateaux avec entente du scope `principal`/`secondary` cette année, sans entente du même scope l'an passé. Approche `FROM bateaux + EXISTS` pour dédupliquer si un bateau a plusieurs ententes du même scope (ex. doublons d'import). `current_amount` = `SUM(total_amount)` des ententes de cette année (reflète les doublons sans les compter en plus de bateaux).
- `previous_ententes_json` permet de distinguer promu (avait une entente d'un autre type N-1) de vraiment nouveau (aucune entente N-1). Badges séparés dans le header.
- Markup miroir de Non-renouvellements : table extensible avec Bateau · Client · Port N · Saison passée · Contact · Montant N. Boutons Copier (TSV presse-papiers) et Imprimer (PDF) identiques au pattern existant.
- Visible dans onglet `Principal` et `Secondaire` du card « Mouvement par port ».
UX — Non-renouvellements : Copier (presse-papiers) + Imprimer (PDF) à la place du CSV
- Copier : copie la liste en TSV (séparé par tabulations) dans le presse-papiers. Coller direct dans Excel ou Google Sheets — chaque champ atterrit dans la bonne colonne, accents préservés. Feedback visuel sur le bouton (« Copié » 1.8s). Fallback `document.execCommand('copy')` si `navigator.clipboard` indisponible (vieux Safari/HTTP).
- Imprimer : ouvre une fenêtre avec une mise en page propre (titre, sous-titre scope + date, table compacte) et déclenche `window.print()`. L'utilisateur choisit imprimante ou « Enregistrer en PDF » dans le dialogue système.
[1.18.87] — 2026-04-22
UX — Non-renouvellements : flag visuel pour téléphone/courriel mal formatté
- Téléphone : on extrait les chiffres via `preg_replace('/\D/','')` et on vérifie 10 chiffres (NANP local) ou 11 commençant par `1` (avec indicatif pays). Sinon → couleur rouge + icône `triangle-exclamation` + tooltip « Format invalide »
- Courriel : `filter_var($email, FILTER_VALIDATE_EMAIL)`. Sinon → mêmes flags
- Aucun contact (ni tel ni email) : badge ambre « Aucun contact » au lieu d'un tiret silencieux
[1.18.86] — 2026-04-22
UX — Non-renouvellements : colonne Solde N-1, courriel + bouton Export CSV
[1.18.85] — 2026-04-22
UX — Non-renouvellements : « Cette saison » remplace « Montant N-1 »
- Downgrade (badges colorés visibles) : le client a gardé le port pour Avant/Après-pêche mais pas Principal — pas urgent
- Vraiment perdu (badge rouge « Aucune ») : à appeler en priorité
[1.18.84] — 2026-04-22
Feat — Dashboard : section « Non-renouvellements » sous Mouvement par port
- `LEFT JOIN ententes` sur année N-1 puis `NOT EXISTS` sur l'année courante (active ou pending), du même scope
- 2 scopes pré-calculés : `principal` et `secondary`
- Retourne `bateau_id`, `bateau_name`, `client_name`, `phone`, `email`, `last_port`, `last_amount`
- Limit 100 (suffisant pour un suivi)
- Section ajoutée dans chaque `port-move-set` (Principal vs Secondaire), suivant les transferts
- Header cliquable avec chevron + count + somme N-1 (« 71 bateaux · 87 240 $ »)
- Au clic : table avec Bateau · Client · Port N-1 · Contact (tel cliquable) · Montant N-1
- Click sur une ligne → ouvre le modal du bateau (`/bateaux?open=...`)
- Click sur le téléphone → tel: direct (sans propager au row click)
[1.18.83] — 2026-04-22
UX — Dashboard : graphique revenus mensuels = facturation + paiements (2 lignes)
- Paiements reçus (cash in) — ligne navy `#00355f` solide + aire dégradée + point sur le mois courant
- Facturation émise (revenue earned) — ligne teal `#006970` pointillée (dash 6 4)
- Paiements reçus · 248 420 $
- Facturation émise · 263 100 $
- À recevoir · 14 680 $ (badge ambre conditionnel si factures > paiements)
[1.18.82] — 2026-04-22
Feat — Modal entente : bouton « Recalculer » dans l'onglet Paiements
- Charge l'entente + bateau, lit `width` du bateau pour calculer `widthFt`
- Mappe `e.type` → `type_port` (`principal` / `secondaire_avant` / `secondaire_apres`) et `bateau.type` → `categorie` (peche / plaisance / commercial)
- Résout le tarif d'amarrage via `TarifResolver::amarrage($year, $portId)` (scope-aware : per_port → per_administration → tenant) et applique la formule `base + (widthFt − seuil) × tarif_par_pied`
- Résout les services via `TarifResolver::services()`, ne garde que ceux avec `tarif_saisonnier > 0` et exclut `quai` (gratuit)
- `UPDATE ententes SET tarif_applied, total_amount, dock_services_detail`
- `UPDATE factures SET amount, taxes (14.975%), total` pour toute(s) la(les) facture(s) liée(s)
- Ne touche pas à `payment_amount` (paiement reçu préservé)
- Lance une exception si bateau sans largeur ou aucun tarif trouvé
- Bouton ghost en haut de la section « Montants de l'entente » avec status texte à droite
- Confirm avant l'action (avertit que tarif/total seront remplacés)
- Spinner pendant la requête
- Au succès : pré-remplit les champs `tarif_applied` + `total_amount`, met à jour aside + breakdown, affiche feedback (« tarif 729,70 → 750,00 $ · 1 facture mise à jour · largeur 17 pi »)
[1.18.81] — 2026-04-22
UX — Home mobile : icônes de ports toutes navy/teal (plus de rotation)
[1.18.80] — 2026-04-22
Fix — Assurance (KPI + liste mobile) filtrée sur bateaux avec entente active
[1.18.79] — 2026-04-22
Feat — Système d'alertes + KPIs mobile cliquables
- Table `alerts(id, title, body, level, scope, status, expires_at, created_by, created_at)` créée dans tous les tenants
- Admin `AlertsController` + vue `views/alerts/index.php` : page dédiée sous Opérations dans la sidebar (avec compteur `.ui-sidebar-count` pour les alertes actives). Formulaire modal : titre, message, niveau (Info / Avertissement / Urgent), date d'expiration optionnelle. Actions par alerte : Désactiver/Réactiver, Supprimer.
- Mobile layout : fetch automatique de `/api/mobile/alerts` au chargement de chaque page. Si alertes actives > 0 : l'icône de l'onglet « Alertes » dans la bottom tab bar devient rouge pulsante avec un badge compteur superposé
- Mobile `/app/alerts` : nouvelle vue qui liste les alertes actives triées par sévérité (`danger` en premier), avec card colorée par niveau + icône + date + corps du message + date d'expiration si définie
- Filtre automatique : les alertes `expires_at < NOW()` ne sont plus retournées
- « Assurance » → `/app/insurance` : liste des bateaux expirés ou expirant < 30j, avec status coloré (rouge expiré / ambre expirant / gris manquante), clic → détail bateau
- « Incidents » → `/app/incidents` : liste des incidents `status IN ('open','in_progress')` triés par priorité puis date, avec icône par catégorie, badge de status
- KPI « Assurance » corrigé : inclut maintenant expirées + expirant 30j + manquantes (avant : seulement expirant 30j)
- `GET /alerts`, `POST /api/alerts`, `POST /api/alerts/{id}/toggle`, `POST /api/alerts/{id}/delete`
- `GET /api/mobile/alerts` (consommé par le badge)
- `GET /app/alerts`, `GET /app/insurance`, `GET /app/incidents`
[1.18.78] — 2026-04-22
Fix — OCR sur Sonnet (plus précis) + mic feedback iOS
- Détection user-agent iOS → message explicite « Recherche vocale non disponible sur Safari iOS »
- Détection absence de l'API → message clair
- Feedback en direct dans le hint : « Écoute… » pendant la capture, « Entendu : X » au succès
- Gestion détaillée des erreurs : `not-allowed` (permission), `no-speech` (rien entendu), `audio-capture` (pas de micro), `network` (service indisponible)
[1.18.77] — 2026-04-22
Feat — OCR mobile : accepte aussi le nom du bateau (pas juste l'immatriculation)
- Cherche les deux (immat + nom)
- Choisisse celui dont la lecture est la plus nette
- Préfère l'immat s'il est lisible (plus unique) sinon retourne le nom
- `kind=reg` : alphanumériques + tirets seulement (`[^A-Za-z0-9\-]`)
- `kind=name` : lettres + chiffres + espaces + apostrophes + accents (noms français courants : `L'ODYSSÉE II`, `La Brise du Nord`)
[1.18.76] — 2026-04-22
Fix — OCR : photo > 5 Mo refusée par Claude Vision
[1.18.75] — 2026-04-22
Fix — OCR 500 + CSP bloquait `blob:` (preview images compressées)
[1.18.74] — 2026-04-22
Fix — Upload photo mobile : compression client-side (PHP upload_max_filesize = 2 Mo)
- Charge l'image dans un `<img>`, dessine sur canvas à dimension max 1600px (ratio préservé)
- `canvas.toBlob('image/jpeg', 0.85)` → retourne un `File` nommé avec extension `.jpg`
- Fallback transparent si l'opération échoue
- `/api/mobile/ocr` (search.php) : maxDim 1280, quality 0.82 (image de lecture, pas d'archivage) — résultat typique < 300 Ko
- `/api/infractions` (bateau.php) : maxDim 1600, quality 0.85 — garde la qualité comme preuve
- `/api/incidents` (home.php) : même paramètres que infractions
[1.18.73] — 2026-04-22
Feat — App mobile recherche : micro (voix) + caméra (OCR Claude Vision)
- Tap → écoute en `fr-CA` via `SpeechRecognition` / `webkitSpeechRecognition` (natif iOS/Android récent)
- Transcription automatiquement injectée dans le champ + déclenche `doSearch()`
- Pill rouge pulsante pendant l'écoute
- Aucune dépendance externe, aucune clé API, aucun upload
- Tap → ouvre la caméra arrière (`capture="environment"`)
- Photo envoyée en `multipart/form-data` à `POST /api/mobile/ocr`
- Backend (`MobileController::ocr()`) encode en base64 + appelle `Claude::generateWithImage()` avec prompt système court orienté immatriculations québécoises/canadiennes (formats `QC-1234-AB`, `C-01234XX`, numéros MPO)
- Le modèle renvoie UNIQUEMENT le numéro (ou `AUCUN`), nettoyé côté PHP via regex `[^A-Za-z0-9\-]`
- Texte renvoyé injecté dans la search bar + auto-search
- Feedback dans le hint : spinner pendant l'analyse, ✓ vert sur succès, ✗ rouge sur échec
- `Claude::generateWithImage($system, $user, $imageBase64, $mimeType, $model='claude-haiku-4-5', $maxTokens=256)` — helper générique qui construit le payload `messages[{content:[image+text]}]` pour Messages API
- Route `POST /api/mobile/ocr` — accepte upload 8 Mo max, formats jpeg/png/webp
- CSP : aucun changement requis (browser → /api/mobile/ocr same-origin, le call Anthropic se fait côté serveur)
[1.18.72] — 2026-04-22
UX — App mobile home : bouton doré devient « Signaler un incident »
- Titre (obligatoire)
- Catégorie : Général · Bris/Dommage · Sécurité · Environnement · Infrastructure · Autre
- Port (optionnel) alimenté depuis la liste `$ports`
- Priorité : 2 radio pills (Normale = navy, Urgente = rouge)
- Description et Emplacement en texte libre
- Photo : input file `accept="image/*" capture="environment"` → caméra arrière + preview
- Submit : `FormData` multipart via `fetch('/api/incidents')` avec spinner et feedback « Envoyé ! »
[1.18.71] — 2026-04-22
UX — App mobile : retrait du select « Méthode de notification » (redondant)
[1.18.70] — 2026-04-22
Fix — App mobile : formulaire d'avertissement complet (types DB, photo caméra, frais)
- Type d'infraction : dropdown alimenté depuis `infractions_types` (query dans `MobileController::bateauDetail()`). Fallback input texte si la table est vide
- Type d'avertissement : 3 radio pills (Verbal / Écrit / + Frais) avec style custom
- Montant des frais : apparaît automatiquement si « + Frais » coché, pré-rempli avec `frais_defaut` du type sélectionné
- Description : textarea
- Photo : `<input type="file" accept="image/*" capture="environment">` → ouvre directement la caméra arrière sur mobile. Preview inline de l'image après capture, bouton « Retirer la photo »
- Méthode de notification : select (verbal / écrit / écrit avec frais)
- Envoi : `FormData` multipart via `fetch('/api/infractions')` avec `bateau_id`, `port_id` (résolu dans le controller via le 1er entente active), `issued_by` (user), `status='issued'` — tout ce qu'attend le backend
- Sélection d'un type avec `frais_defaut > 0` → switch auto à « + Frais » et pré-remplit le champ
- Bouton Submit spinner + disabled pendant l'envoi, restauration en cas d'erreur
[1.18.69] — 2026-04-22
Fix — Vue port mobile cassée : `.mx-result` n'avait pas `display:block`
[1.18.68] — 2026-04-22
UX — App mobile : vue port refonte + navigation vers `/app/bateau/:id`
- Nouveau header avec back + titre + count + input filtre live (sticky en haut, même style que home/search)
- Ententes groupées par bateau côté PHP : un bateau avec Principal + Avant-pêche au même port n'apparaît plus deux fois, il est listé une fois avec les 2 badges colorés
- Cards `.mx-result` identiques à la recherche : side bar colorée (vert = actif + assurance OK / ambre = actif + assurance issue / gris = pas d'entente active), avatar initiales, badges types d'ententes + emplacement (`emp.code` si dispo) + badge d'assurance
- Clic sur une card → `/app/bateau/:id` (même détail page que depuis la recherche)
- Divider alphabétique (initiales de nom) conservé avec nouveau look (bandeau navy)
- Filtre client-side : cherche dans nom + bateau + immat + montre/cache aussi les dividers vides
- Suppression du code inline (expand, tableau détails, warning modal, `toggleDetails()`, `openWarning/closeWarning/submitWarning`) — tout migre vers `/app/bateau/:id`
[1.18.67] — 2026-04-22
UX — App mobile : page détail bateau dédiée (au lieu de l'expand inline)
- Charge `bateaux` + client (nom, téléphone, email, ville, urgence)
- Charge les ententes actives (status='active', YEAR=courante) avec port + dock_services
- Charge toutes les infractions du bateau
- Hero dont le gradient = état global du bateau (vert = contrats + assurance OK · ambre = contrats mais assurance à renouveler · gris = pas de contrat)
- Chip « En règle · N contrats actifs » ou « Assurance à vérifier » ou « Pas de contrat actif »
- Dimensions + immat + type
- 3 quick actions en card : Appeler (tel:), Entente (PDF du 1er contrat), Avertir
- Card propriétaire : avatar initiales + nom + ville + téléphone tappable, bouton tel à droite
- Cards contrats : ribbon coloré (vert/bleu/ambre), badge type, port, dates (1 mar → 30 avr), services en pills
- Card assurance : ribbon coloré selon état (vert/ambre/rouge/gris), label dynamique
- Card urgence : style doux rouge si contact d'urgence renseigné
- Section infractions : liste des avertissements (type + statut + date + description)
- FAB sticky en bas (Appeler / Avertir) au-dessus de la bottom tab bar
- Sheet bottom-up pour l'avertissement (réutilise les styles `.mx-sheet` du layout)
[1.18.66] — 2026-04-22
UX — App mobile tenant : redesign complet
- Palette Moorly : navy `#00355f`, teal `#006970`, gold `#c8a45c`
- Sémantique : green `#10b981` · amber `#f59e0b` · red `#ef4444` · blue `#3b82f6`
- Typo : Manrope (display) + Inter (corps), Google Fonts
- Cards radius 14px, ombres légères `0 1px 3px rgba(0,0,0,0.06)`
- Safe-area-inset respecté (iPhone notch)
- Header retiré du layout — chaque view rend son propre `<header class="mx-hdr">` avec le flavor approprié (greet + search sticky pour home, back + title + search pour search, back + hero coloré pour port)
- Nouvelle bottom tab bar sticky `.mx-tabbar` : Accueil · Recherche · Alertes · Sortir (avec `activeTab` passé depuis le controller)
- Variables css + classes compat `mob-*` conservées pour que `port.php` fonctionne (vieux markup)
- Avatar + greet « Bonjour, {Prénom} » + bouton déconnexion
- Search pill cliquable → `/app/search` avec caméra bouton secondaire
- KPI row : Actifs · Assurance · Incidents (couleur ambre/rouge si > 0)
- Liste ports en cards (icône gradient navy/teal/gold alternée, count d'ententes actives)
- Quick action gold « Recherche rapide » en bas
- Requêtes best-effort en try/catch pour ne pas casser si table absente
- Header navy avec back + title + search input + hint live (X résultats / « Minimum 2 caractères »)
- Résultats : cards avec side bar colorée (vert = active + ins OK, ambre = active + ins issue, gris = pas d'entente), avatar initiales coloré par status, badges inline (type entente + port + état assurance)
- Expand au clic : dimensions + assurance (n° + expiration) + contrats (chaque contrat = card `.mx-contract` avec badge coloré + port + svc pills) + contacts + urgence + infractions + bouton rouge « Avertissement »
- Sheet bottom-up `.mx-sheet` pour le formulaire d'avertissement (style iOS native), avec grip handle et animation transform
- Tout le backend préservé : `/api/mobile/search` + `/api/infractions` + logique `currentEntentePhase()`
[1.18.65] — 2026-04-22
UX — App mobile recherche : services listés sous chaque contrat
[1.18.64] — 2026-04-22
UX — App mobile recherche : icônes de services restaurées, basées sur la période courante
- Mars–avril → services de l'entente Avant-pêche
- Mai–juillet → services de l'entente Principal
- Août–novembre → services de l'entente Après-pêche
- Si pas d'entente pour la période, fallback sur Principal puis sur la première entente active
[1.18.63] — 2026-04-22
UX — App mobile recherche : point d'état selon ententes + assurance
- Vert `#10b981` : au moins 1 entente active et assurance valide
- Jaune `#f59e0b` : au moins 1 entente active mais assurance non valide (expirée, <30j, ou manquante)
- Gris `#9ca3af` : aucune entente active (peu importe l'assurance)
[1.18.62] — 2026-04-22
UX — App mobile tenant : recherche bateau liste tous les contrats actifs ordonnés
[1.18.61] — 2026-04-21
Cleanup — Suppression complète du système de synchronisation legacy
- Vue `app/views/dashboard/index.php` : retrait du callout « Synchronisation legacy » (callout jaune avec bouton Synchroniser) + de la fonction JS `runLegacySync()`
- Route `POST /api/legacy-sync` retirée de `app/config/routes.php`
- Controller `DashboardController::legacySync()` supprimé (plumbing HTTP + check tenant slug + appel service)
- Service `DashboardService::legacySync()` supprimé (~140 lignes : fetch API legacy, mapping ports, upsert clients/bateaux/ententes, etc.)
- Scripts : `app/scripts/sync-from-legacy.php` et `app/scripts/import-legacy-2026.php` supprimés (one-off migration helpers)
[1.18.60] — 2026-04-21
UX — Occupation par port : passage de 3 barres à un heatmap grid
- Lignes = ports (nom à gauche), colonnes = phases (Avant-pêche · Principal · Après-pêche · Capacité)
- Cellule = couleur de phase + opacité proportionnelle à l'occupation (vide → opacity 0.10, plein → 1.0)
- Texte dans la cellule : « 78% » en gros + « 47/60 » en petit
- Couleur du texte : blanc dès >55% d'occupation, sinon couleur de la phase
- Tooltip natif au hover avec valeur exacte
- Capacité du port à droite + petit tag « est. » si valeur estimée
[1.18.59] — 2026-04-21
Perf — Dashboard occupation : pattern stale-while-revalidate avec cache mis à jour par /quais
- Lit le cache pour chaque port × scenario (`principal`, `avant_principal`, `apres_principal`)
- Renvoie la capacité cached (précise, du calcul turf.js de /quais) si présente, sinon estimation rapide
- Chaque port a maintenant `cached: true/false` et `computed_at` pour que le client sache si c'est précis ou estimé
- Render instantané avec ce que le serveur renvoie (cached ou estimé)
- Chips « X places · estimation » quand pas cached, banner « Calcul précis en cours… » en haut
- Si au moins un port n'est pas cached OU si > 1h, charge turf.js depuis CDN (lazy) et refait le calcul exact en arrière-plan
- Quand fini : refresh DOM + push résultat à `POST /api/dashboard/occupation/cache`
- Payload `{year, ports: [{port_id, scenarios: {principal/avant_principal/apres_principal: {occupied, capacity}}}]}`
- INSERT … ON DUPLICATE KEY UPDATE
- Permet update partiel (un seul scénario depuis /quais ou tous depuis /dashboard)
- Dashboard reste affiché instantanément (cache), même la 1ère visite (estimation)
- Précision atteinte progressivement par la navigation /quais + le background turf.js du dashboard
- Refresh auto si données > 1h
[1.18.58] — 2026-04-21
Perf — Dashboard occupation : capacité estimée utilise la moyenne réelle des bateaux
[1.18.57] — 2026-04-21
Perf — Dashboard occupation : 12 calls API + turf.js → 1 call serveur (~7ms)
- 1 query agrégée pour les counts d'ententes par `(port_id, type)`
- 1 query pour les longueurs totales de docks + config (largeur moyenne + espacement) par port
- Capacité estimée : `floor(total_dock_len_m / ((widthFt + spacingFt) × FT_TO_M))` — formule simple et rapide
- Renvoie `{ports: [{name, capacity, principal, avant_principal, apres_principal}], total: {occupied, capacity, pct}}`
- Mesuré en CLI : ~7ms
[1.18.56] — 2026-04-21
UX — `/ententes` : tri par bateau ASC + filtre Active par défaut
- Sélecteur Statut : « Active » sélectionné par défaut (au lieu de « Tous statuts »)
- DataTable : `order: [[1, 'asc']]` (colonne Bateau croissant) au lieu de `[[0, 'desc']]` (Année décroissant). Le serveur a déjà `$columns[1] = 'b.name'` — pas de changement back-end requis.
[1.18.55] — 2026-04-21
UX — `/clients` : retrait du filtre type + colonne Ententes actives au lieu de Port principal/ville
- Filtre Type (Commerciaux/Individuels) retiré — pas pertinent pour le tri courant. Backend `$filterType` aussi retiré.
- Colonne « Port principal » (qui montrait le port + ville/province en sub) → renommée « Ententes actives », affiche maintenant la liste des ententes actives du client comme dans `/bateaux` :
- Tri chronologique : Avant-pêche → Principal → Après-pêche
- Badge type coloré (#10b981 vert / #3b82f6 bleu / #f59e0b orange) + nom du port à droite
- Une ligne par entente, compact (line-height 1.2)
- Backend : sous-requête `main_port` remplacée par `JSON_ARRAYAGG(JSON_OBJECT(id,type,port_name,period_start,period_end))` retournant `actives_json`. Largeurs de colonnes ajustées : Client 30 / Bateaux 20 / Ententes 30 / Statut 16 / arrow 4.
[1.18.54] — 2026-04-21
Fix — `/clients` colonne port : prioriser le type Principal au lieu de la date
[1.18.53] — 2026-04-21
UX — `/clients` : par défaut, seulement clients avec entente active
[1.18.52] — 2026-04-21
UX — Modal client, onglet Ententes : pattern actives + historique collapsible (cohérent avec bateaux)
- Filtre par défaut : seules les ententes `status='active'` sont listées (badge compteur reflète ce filtre)
- Tri chronologique : avant-pêche → principal → après-pêche, puis par date desc
- Badge type coloré sur chaque card : Principal vert (#10b981), Avant-pêche bleu (#3b82f6), Après-pêche orange (#f59e0b). La bordure gauche de la card prend aussi cette couleur
- Section « Historique » déployable au clic sous les actives, avec badge compteur des non-actives
- Lazy render : le HTML de l'historique est généré uniquement au premier clic (rows stockés dans `dataset.rows`)
- Refactor : le rendu d'une card est extrait dans `_renderEntenteCard(e)` réutilisé par renderEntentes() et toggleEntentesHistory()
[1.18.51] — 2026-04-21
UX — Tri chronologique des ententes + badge type dans le modal bateau
- Modal bateau, onglet Contrats : nouvelle colonne « Type » avec le badge coloré (Principal vert / Avant-pêche bleu / Après-pêche orange) entre Année et Port. Appliqué aux deux tables (actives et historique).
- Tri uniformisé : ordre chronologique de la saison `avant-pêche → principal → après-pêche` (au lieu de Principal en tête). Appliqué à :
- Liste `/bateaux` (colonne Ententes du DataTable)
- Modal bateau actif + historique
[1.18.50] — 2026-04-21
UX — `/bateaux` : port toujours à droite du badge dans la colonne Ententes
[1.18.49] — 2026-04-21
UX — `/bateaux` : ajout du filtre Port
[1.18.48] — 2026-04-21
UX — `/bateaux` : retrait des dates de la colonne Ententes
[1.18.47] — 2026-04-21
UX — `/bateaux` : remplacement du filtre type bateau par filtre entente, défaut « actives »
- Avec entente active (défaut sélectionné) — montre uniquement les bateaux ayant ≥ 1 entente `status='active'`
- Principal — uniquement les bateaux avec une entente active de type `principal`
- Avant-pêche — type `avant_peche`
- Après-pêche — type `apres_peche`
- Sans entente active — bateaux orphelins (utile pour relance)
- Tous (avec ou sans entente) — vue complète
- `active` → `ea.actives_count > 0`
- `none` → `(ea.actives_count IS NULL OR = 0)`
- type spécifique → sous-requête `EXISTS (SELECT 1 FROM ententes ex WHERE ex.bateau_id = b.id AND ex.status = 'active' AND ex.type = ?)`
- `''` → aucune condition (tous)
[1.18.46] — 2026-04-21
Fix — Indicateur de profondeur bathymétrie : ancré sur la map au lieu du body
[1.18.45] — 2026-04-21
Fix — Couleurs bateaux par type d'entente : `entente_type` était droppé par `array_map` du serializer
[1.18.44] — 2026-04-21
UX — `/quais` : panneau Occupation déplacé sous la sélection de port
Note — Investigation couleurs bateaux par entente_type
[1.18.43] — 2026-04-21
Fix — CSP `connect-src` étendu (unpkg + cdnjs + Stripe)
- `https://unpkg.com` — Leaflet et plugins (sourcemap fetches `.js.map`, ressources liées)
- `https://cdnjs.cloudflare.com` — déjà allowed pour `script-src`/`style-src`/`font-src`, manquait pour `connect-src` (sourcemap, polices `.woff2.map`)
- `https://api.stripe.com` — checkout API si on intègre Stripe Elements côté client (anticipé)
[1.18.42] — 2026-04-21
UX — Couleurs unifiées Principal / Avant-pêche / Après-pêche
- Principal : `#10b981` (vert)
- Avant-pêche : `#3b82f6` (bleu)
- Après-pêche : `#f59e0b` (orange)
- `/bateaux` liste — badges de la colonne « Ententes actives » teintés selon le type (au lieu de `ui-badge-success` uniforme)
- `/ententes` liste — badge de type sous le nom du port remplace le simple label texte
- `/ententes` modal aside — nouveau badge coloré sous le titre (`#asideTypeBadge`) qui rend explicite « Principal / Avant-pêche / Après-pêche » avec icône (anchor / sun / cloud-sun). Mis à jour dynamiquement par `refreshAside()` quand l'utilisateur change le type dans le form
- `/quais` map — bateaux colorés selon `e.type` (au lieu du rouge uniforme `BOAT_REAL_COLOR`). Le popup du bateau inclut le badge de type. La légende en bas-droite montre les 3 couleurs au lieu de « Bateau occupé »
- `MooringMapController::getData()` — ajout de `e.type AS entente_type` au SELECT pour que le map ait l'info
[1.18.41] — 2026-04-21
UX — `/bateaux` : retrait de la longueur dans le sous-titre
[1.18.40] — 2026-04-21
UX — `/bateaux` : rééquilibrage des colonnes pour les ententes multiples
- Propriétaire + Contact fusionnées en une seule colonne : nom en haut, téléphone (ou email en fallback) dessous avec ellipsis. Libère de l'espace horizontal.
- Largeurs rééquilibrées : Bateau 24% · Propriétaire 24% · Ententes actives 32% · Assurance 16% · arrow 4%
- Rendu multi-ententes plus compact : `gap:2px` au lieu de 3px, `line-height:1.2`, padding badge réduit, label sans icône pour les ententes multiples (l'icône reste pour le cas à 1 entente), `white-space:nowrap + overflow:ellipsis` sur le port pour éviter les retours à la ligne
- Server-side `$columns` mis à jour (5 colonnes au lieu de 6) pour matcher les sorts DataTables
[1.18.39] — 2026-04-21
UX — Liste `/bateaux` : colonne Entente affiche toutes les ententes actives
- Subquery `LEFT JOIN` remplacée par une agrégation `JSON_ARRAYAGG(JSON_OBJECT(...))` sur `ententes` filtrée `WHERE status='active'` GROUP BY bateau_id
- Retourne maintenant `actives_json` (array d'objets id/type/port_name/period_start/period_end), `actives_count`, `first_start`, `last_end`
- Colonnes de tri mises à jour : `ea.last_end` pour la colonne Entente, `ea.actives_count` pour le statut
- Colonne « Entente » parse le JSON et rend :
- Aucun contrat actif → badge neutre
- 1 entente → badge success compact avec type (Principal/Avant-pêche/Après-pêche) + port + dates
- 2+ ententes → liste stackée de mini-badges, ordonnée Principal → Avant-pêche → Après-pêche, chaque ligne : badge type + port + année
- Icônes et couleurs cohérentes avec le reste du design system (`ui-badge-success` + `fa-circle-check`)
[1.18.38] — 2026-04-21
UX — Modal bateau : section « Historique » déployable sous les contrats actifs
- Bouton avec chevron + badge compteur sous la liste active
- Au clic : rend la table des ententes non-actives (expirées, annulées, pending) avec leurs badges d'origine
- Lazy render (HTML généré uniquement au premier clic, stocké en `dataset.rows` côté JS)
- Reset propre à chaque ouverture d'un nouveau bateau
- Section masquée si aucune entente historique
[1.18.37] — 2026-04-21
UX — Modal bateau admin : onglet « Contrats » filtre uniquement les ententes actives
- `ententes.filter(e => e.status === 'active')` appliqué avant rendu
- Badge compteur reflète le nombre d'actives uniquement
- Message vide mis à jour : « Aucun contrat actif pour ce navire. »
- La table ne montre plus que le badge « Active » (les autres statuts sont filtrés)
[1.18.36] — 2026-04-20
UX — `/dashboard` client (espace client) : refonte selon `design-preview/client-portal.html`
- Mes bateaux (nombre + nom du premier)
- Mon entente (Active + date de fin / ou empty)
- À payer (somme `factures.total` pour statuts pending/sent/partial/overdue + nombre)
- Documents (Tous valides / Vérifier, selon assurances)
- Gauche :
- Mes bateaux : carte par bateau avec photo (ou icône), registration + longueur + type, badges « Entente active » + « Assurance 2026 » (ou « Expirée »), upload photo inline
- Mes ententes en cours : pour chaque entente, header avec status badge + `ENT-xxxxxxxx` + date signature, grille port/bateau/saison/type/entente/montant dans un `<dl class="portal-entente-grid">`, état des paiements (payé + solde), boutons Signer (si non-signée) + PDF
- Droite :
- Mes factures récentes (5 dernières) : liste `portal-list-row` avec icône, numéro, échéance, badge status, montant, bouton Payer (Stripe)
- Documents : assurances bateaux + ententes signées avec badges Valide/Expirée + boutons download
- Notifications (synthèse côté serveur, 5 dernières) : signatures d'ententes (vert), paiements reçus (bleu), rappels d'assurance à <90 jours (orange)
- Booking modal (nouvelle réservation) + scripts
- Signature modal + pad
- Incidents section (reportage + formulaire + liste)
- Alerts renewal pending + network consent
[1.18.35] — 2026-04-20
Fix — FA Free ne contient presque pas d'icônes `far` (regular) : bascule `far` → `fas`
- `class="far fa-*"` → `class="fas fa-*"` dans : ports.php, regulations.php, services.php, tariffs.php, port.php, home.php, service_detail.php, faq.php, layouts/public.php
- `class="fa-regular fa-*"` → `class="fa-solid fa-*"` dans aide/index.php
[1.18.34] — 2026-04-20
Fix — CSP `frame-src` : iframe Google Maps bloqué par `default-src 'self'`
[1.18.33] — 2026-04-20
Fix — **Root cause** des icônes FA cassées dans les pages CMS : `.mm-cms-content *` forçait `font-family: inherit !important`
[1.18.32] — 2026-04-20
Fix — `/contact` : retour à Google Maps (définitif) + z-index du wrapper + cache-bust FA CSS
- Map `/contact` : retour à l'iframe Google Maps `www.google.com/maps?output=embed` (demande utilisateur). Si bloquée par Firefox/Brave anti-tracking, c'est une décision du navigateur (whitelisting côté user uniquement).
- z-index : le wrapper de la map avait Leaflet qui définit des panes en z-index 200-700, passant au-dessus du navbar. Wrapping now : `position:relative; z-index:0;` pour créer un stacking context qui contient les z-index internes de l'iframe.
- Cache-buster FA CSS : les 4 layouts (public, dashboard, client, mobile) ajoutent `?v=<appVersion()>` à l'URL `/legacy-assets/css/all-fontawesome.min.css` pour forcer un refetch lors des bumps de version.
- Test page : `public/test-fa.html` (debug) avec quelques icônes `.fas` et `.far` pour vérifier que le font s'applique correctement en isolation.
- Font file: `fontTools` confirme que `fa-solid-900.woff2` contient les codepoints `U+F13D` (anchor), `U+F613` (oil-can), `U+F2DC` (snowflake), `U+F085` (cogs), etc.
- CSS file: commence bien par `Font Awesome Free 6.5.1`, se termine par `}` (non tronqué)
- Tous les 185 noms d'icônes utilisés dans le codebase matchent des règles `:before` dans le CSS
[1.18.31] — 2026-04-20
Fix — Icônes FA : remplacement des fichiers locaux par la version officielle FA Free 6.5.1
- `public/legacy-assets/css/all-fontawesome.min.css` — passe de "Font Awesome Pro 6.4.2" (possiblement incomplet) à "Font Awesome Free 6.5.1"
- `public/legacy-assets/fonts/fa-solid-900.{woff2,ttf}` — official Free 156KB
- `public/legacy-assets/fonts/fa-regular-400.{woff2,ttf}` — official Free
- `public/legacy-assets/fonts/fa-brands-400.{woff2,ttf}` — official Free
- `public/legacy-assets/fonts/fa-v4compatibility.{woff2,ttf}` — shim pour anciens alias (ex. `fa-tint`, `fa-address-card`)
[1.18.30] — 2026-04-20
Fix — Icônes FA rendues en carré : bascule sur CDN FA 6.5.1 Free
- ~~Icônes Pro-only~~ : les noms utilisés actuellement (`fa-anchor`, `fa-snowflake`, `fa-plug`, `fa-oil-can`, `fa-ship`, `fa-cogs`, `fa-phone`, etc.) sont tous inclus dans FA Free — aucun impact visible
- Le fichier `all-fontawesome.min.css` local reste en place (ni supprimé ni modifié) pour éviter casser d'éventuelles références externes
Fix — `/contact` : retour à Leaflet+OSM (Google bloqué même avec `www.`)
[1.18.29] — 2026-04-20
Fix — `/contact` : retour à Google Maps (demande utilisateur)
[1.18.28] — 2026-04-20
Fix — CSP bloquait Nominatim + icônes services ne matchaient aucun slug
- Nouvelle fonction `_svcIconKey($slug)` qui fait `basename($slug)` → passe de `services/amarrage` à `amarrage`
- Map d'icônes étendue : ajout de `huiles-usees` → `fa-oil-can`, `eau-electricite` → `fa-plug`, `rampe`, `grue`, `lavage`, `pompage`, `stationnement`, `securite`, `dechets`
- `fa-tint` remplacé par `fa-faucet-drip` pour `eau` (plus expressif, les deux existent dans la lib Pro)
[1.18.27] — 2026-04-20
Fix — `/contact` public : carte Google bloquée, remplacée par Leaflet + OSM
- Remplacement de l'iframe par un `<div id="contactMap">` + Leaflet (déjà utilisé ailleurs dans le projet)
- Tiles OpenStreetMap (sans clé, sans X-Frame-Options)
- Géocodage de `$org['address']` via Nominatim côté client (`countrycodes=ca`, limite 1 résultat), marker + popup avec le nom de l'organisation
- Fallback silencieux sur une vue centrée sur les Îles-de-la-Madeleine si le géocodage échoue
- `scrollWheelZoom: false` pour éviter le hijack du scroll de page
[1.18.26] — 2026-04-20
UX — `/dashboard` (admin) : refonte selon design-preview
- `monthlyRevenue` / `monthlyRevenueLY` : sommes de `paiements.amount` (non annulés) par mois pour `$year` et `$year - 1` (arrays indexés 1..12)
- `totalRevenue` / `totalRevenueLY` / `revenueDeltaPct`
- `activeEntentesCount` / `activeEntentesDelta` (YoY)
- `unpaidInvoicesAmount` / `unpaidInvoicesCount` : somme `factures.total` − paiements non annulés, pour statuts `pending/sent/partial/overdue`
- `renewalsPending` : ententes en `status='pending'` + `renewal_status` IN generated/notified/in_progress
- `openIncidents` / `urgentIncidents` (priority='high')
- `usageDonut` : array unifié (label, count, color) — bascule sur `usageBreakdown` si dispo, sinon répartition des types de contrat
- Header : titre + sous-titre + sélecteur de saison (nav `/dashboard?year=X`) + lien vers rapport financier
- KPI row 6 colonnes : Revenus · Ententes actives · Occupation · Factures impayées · Renouvellements · Incidents
- Grid principal :
- Revenus mensuels (span 2) — SVG natif avec path area gradient + 2 polylines (2026 bleu, LY gris), point marqué sur mois courant, légende avec totaux
- Occupation par port (pilier JS existant, inchangé)
- Donut types d'usage (SVG natif avec `stroke-dasharray` par segment)
- Alertes & actions (span 2, conditionnel si `$alertsCount > 0`)
- Mouvement par port (conservé, avec chips Principal/Secondaire)
- Ententes récentes (conservé)
[1.18.25] — 2026-04-20
UX — Layout : suppression du topbar, user block déplacé en bas de la sidebar (design-preview)
- `app/views/layouts/dashboard.php` : retrait de `<header class="ui-topbar">` complet + JS `globalSearchHandler`, `globalSearchFetch` et le handler de click outside. L'endpoint `/api/search` reste en place côté backend (utilisé nulle part côté UI pour l'instant — à ré-exposer plus tard si besoin, par ex. en palette ⌘K)
- `app/views/partials/sidebar.php` : nouveau bloc `.ui-sidebar-user` en bas de la sidebar (avant le footer version), construit autour d'un `<button>` + Bootstrap dropup. Contient avatar + nom + role, avec chevron qui pivote à l'ouverture
- Menu du dropdown :
- Admin : « Changer le mot de passe » (→ `/change-password`) + « Déconnexion »
- Client : « Changer le mot de passe » (ouvre `#clientPwdModal`) + « Déconnexion »
- `public/css/stitch.css` : `.ui-sidebar-user` restructuré en container + `.ui-sidebar-user-btn` pleinement cliquable avec hover `--s-surface-low`, chevron `.ui-sidebar-user-chev` qui rotate 180° sur `[aria-expanded="true"]`. Dropdown stylé pour coller au design system (border 10px, box-shadow douce, items rounded avec icon aligné)
[1.18.24] — 2026-04-20
UX — Sidebar : badges de comptage sur les items du menu
- Bloc PHP en tête du partial qui calcule `$counts` avec ~17 requêtes `COUNT(*)` ciblées sur le tenant connecté (admin only)
- Chaque requête est isolée dans un `try/catch` pour qu'une table manquante (ex. `permis_commerciaux` sur un tenant sans module) ne casse pas la sidebar
- Helper `_scount($counts, $key)` qui rend `<span class="ui-sidebar-count">N</span>` uniquement si `N > 0` (pas de badge « 0 » bruyant)
- Classe CSS `.ui-sidebar-count` déjà définie dans `stitch.css` : pill rounded, font 11px tabular-nums, `margin-left:auto` → poussée à droite, inversion de couleur sur l'état `.active`
- Navigation générale : Administrations, Ports (actifs), Quais, Bateaux, Clients
- États filtrés : Ententes (actives cette année), Renouvellements (en cours), Demandes (pending), Clients commerciaux (permis actifs), Facturation (factures ouvertes : pending/sent/partial/overdue), Transactions (paiements du mois courant non annulés)
- Conformité : expirées ou < 30 jours, infractions actives, exigences manquantes/expirées
- Opérations : machinerie non complétée, incidents ouverts
- Admin : utilisateurs actifs (hors clients) de la licence
[1.18.23] — 2026-04-20
UX — `/tarifs` : liste récap des exceptions de services par administration
- Nom
- Badges des services modifiés : « Rampe · 150,00 $ vs 75,00 $ » (jaune si différence)
- Bouton « Éditer » qui sélectionne l'admin dans le dropdown et scroll vers l'éditeur
- `loadOverridesSummary()` fetch tous les overrides + les groupe par admin
- Filtre : n'affiche que les admins avec au moins 1 service où prix/actif diffère du tenant
- Rechargement automatique après save via `saveServiceOverrideRow()`
- Map `_tenantServicePrices` (indexée par service_code) + `_adminsMap` (par id) injectés côté JS
[1.18.22] — 2026-04-20
Fix — CSRF token manquant dans les appels `fetch()` natifs (422 Unprocessable Entity)
- Patch global de `window.fetch` pour les méthodes unsafe (POST/PUT/PATCH/DELETE) et URLs same-origin
- Ajoute automatiquement `X-CSRF-Token` si pas déjà présent dans les headers
- Normalise les `Headers` instances en objet plain
- Intercepte les réponses 422 JSON avec `error: 'csrf_invalid'` pour reload automatique (comme jQuery déjà faisait)
[1.18.21] — 2026-04-20
Fix — recompute ententes 2026 avec prix scope-aware (per_administration)
- Résout les prix selon la hiérarchie scope : per_port → per_administration → tenant
- Pour chaque entente : grid amarrage (categorie + type_port + largeur) → `tarif_applied`
- Services : prix scope-aware résolus via `resolveService()`
- `total_amount = tarif + Σ services`
- Met à jour aussi les factures : `amount = total HT`, `taxes = 14.975%`, `total facture = TTC`
- 272 recalculées (2 sans match grille)
- 74 `tarif_applied` modifiés
- 74 `total_amount` modifiés
- Factures associées mises à jour en cascade
- Tarif amarrage base grille : 450 $ (pêche après-pêche)
- Services scope-aware : rampe 150 (L'Étang) + parc 300 + elec 150 = 600
- Total HT : 1050 $ (vs legacy 975 $, grille authoritative maintenant)
- Facture TTC : 1050 + 157.24 = 1207.24 $
[1.18.20] — 2026-04-20
Fix — prix services stales dans `dock_services_detail`
- Pour chaque entente 2026 active/pending, rebuild `dock_services_detail` JSON depuis `dock_services` + prix courants de `tarifs_services`
- Puis re-dérive `tarif_applied = total_amount - SUM(fresh services prices)`
- Warnings si tarif négatif (services > total, donnée corrompue)
- Modes `--dry-run` et `--verbose`
- BRISANT BLANC après-pêche : 375 → 450 ✓
- Les factures associées ne sont pas touchées (leur `amount` était déjà = total_amount HT, inchangé)
[1.18.19] — 2026-04-20
Data — Purge ententes dupliquées + backfill season
- 5 ententes actives 2026 avec duplicate exact (groupe par `bateau_id + type + total_amount + period_start + period_end + signed_at`)
- Garde celle avec l'UUID le plus bas, supprime l'autre
- 5 factures liées supprimées en cascade (DELETE manuel en premier pour respecter la logique)
- Bateaux concernés : L'ELODIE MARCO (2 paires), LA CHOUETTE III (2 paires), MADAME EILEEN (1 paire)
- Résultat : 274 ententes active 2026 (vs 279 avant), 277 factures total (274 pour 2026 + 3 pour 2025 expired)
- 159 ententes 2026 actives avaient `season=NULL` (seul `period_start` renseigné)
- `UPDATE ... SET season = YEAR(period_start) WHERE season IS NULL`
- Le filtre saison dans `/facturation` fonctionne maintenant correctement
[1.18.18] — 2026-04-20
Data + UI — Application des taxes (TPS 5% + TVQ 9.975%) sur les factures
- 162 factures mises à jour (les 144 générées en v1.18.15 + quelques anciennes à 0)
- Résultat : 261/279 factures 2026 ont maintenant des taxes correctes (18 restantes = amount=0, pas applicable)
- Exemple ODYSSEE : amount 1255 + taxes 187.94 = total 1442.94 $
- Nouvelle constante `TAX_RATE = 0.14975`
- `calcTotal()` auto-recompute les taxes sur le montant saisi, SAUF si l'utilisateur a touché manuellement le champ taxes (flag `data-manualOverride`)
- Reset du flag à l'ouverture du modal
- Les labels « TPS+TVQ X $ » et « TPS+TVQ incl. » dans la table réapparaissent automatiquement maintenant que `taxes > 0`
[1.18.17] — 2026-04-20
Fix — `/facturation` : retirer le label "Hors taxes" partout
[1.18.16] — 2026-04-20
Fix — `/facturation` : TypeError dans openModal (élément modalTitle manquant)
[1.18.15] — 2026-04-20
Data — Génération des factures manquantes pour les ententes 2026
- `entente_id = e.id`
- `client_id = COALESCE(e.client_id, b.client_id)` (priorité à l'entente)
- `amount = taxes = 0, total = e.total_amount` — OBNL AP des Îles, pas de breakdown TPS/TVQ en legacy
- `status = 'sent'` (facture émise, non payée — aucun Paiement_Reçu_Date en legacy pour 2026)
- `due_date = period_start + 60 jours`
- UUID v4 généré via `HEX(RANDOM_BYTES(...))`
[1.18.14] — 2026-04-20
Design applied — `/facturation`
- Controller `index()` : nouveaux KPIs `outstandingCount`, `overdueAmount`, `paidPrevMonth` (pour delta %), `paidCount`, `totalCount` ; passe `ports` et `seasons` pour les filtres
- Controller `datatable()` : accepte `filter_port` + `filter_season` en plus de `filter_status`
- Service `FacturationService::list()` :
- Recherche étendue : inclut aussi `client_email`, `client_company`, `registration_number`, `f.id`
- Filtres port (via `COALESCE(e.port_id, q.port_id)`) et saison (via `e.season`)
- SELECT enrichi : `client_email`, `client_phone`, `client_company`, `registration_number`, `entente_season`, `entente_type`
- Page header subtitle dynamique : "X factures · Y en retard · Z payées"
- 4 KPIs avec deltas meta :
- À recevoir : montant + nombre de factures ouvertes
- En retard (variant danger si > 0) : count + montant impayé
- Encaissé ce mois avec delta % vs mois dernier (up/down coloré)
- En attente (variant warning si > 0) : count + meta
- Filtres : search + status + saison + port (dropdowns dynamiques)
- Card header `.ui-card-header` avec count live (`drawCallback`) + bouton actualiser
- Table refondu 8 colonnes :
- N° Facture : icon enveloppe navy + FAC-XXXXXX mono + date d'émission
- Client : nom + company/email/téléphone sub
- Entente / Navire : nom bateau + saison · immat mono
- Sous-total : montant mono + "TPS+TVQ X,XX$" ou "Hors taxes" sub
- Total : montant mono bold + "TPS+TVQ incl." sub
- Échéance : date + "Payée le X" (vert) ou "En retard Xj" (rouge) sub
- Statut : badge avec icône (paid/overdue/partial/sent/pending/cancelled)
- Chevron
- Formatter `fmtMoney()` fr-CA (espace comme séparateur milliers) et `fmtShortDate()` (ex: "12 avr")
[1.18.13] — 2026-04-20
Fix — breakdown tarif ODYSSEE : largeur 16.99pi → 17pi
- `bateaux.width` était `DECIMAL(8,2)` — ODYSSEE 5.1816m était tronqué à 5.18
- 5.18 × 3.28084 = 16.99 pi (au lieu de 17.00)
- Fix : `ALTER TABLE bateaux MODIFY width DECIMAL(10,4), length DECIMAL(10,4)` + ré-import depuis legacy `Largeur_Navire`/`Longueur_Navire` (stockés avec 4 décimales)
- Résultat : ODYSSEE width = 5.1816m → 17pi exactement
- Les tarifs sont facturés par pied entier au-dessus du seuil (pas par fraction). Conséquence pratique : on arrondit `widthFt` à l'entier le plus proche avant l'explication formule.
- Si un bateau fait 16.49 pi → rounds to 16 → tarif base. Si 16.51 pi → rounds to 17 → +30$. Cohérent avec la pratique business.
- Aside : Pêche · Principal · largeur 17 pi
- Formule : 700,00 $ + (17 − 16) pi × 30,00 $/pi = 730,00 $ ✓ (matche le tarif stocké)
[1.18.12] — 2026-04-20
Fix — ententes : import dates legacy + affichage signature papier
- UPDATE cross-DB qui importe `Date_Signature_Contrat`→`signed_at`, `Date_Debut_Contrat`→`period_start`, `Date_Fin_Contrat`→`period_end` pour les ententes 2026 matchées par registration_number + type
- Filtre `> '2000-01-01'` pour skipper les dates `0000-00-00` de legacy
- 268/279 ententes ont maintenant `signed_at` renseigné (vs 153 avant)
- Onglet Signature : 3 états au lieu de 2
- Signée électroniquement (avec `signature_data`) : callout success + tracé + IP
- Signée (sur papier) (avec `signed_at` seulement) : callout info + date de signature
- Non signée : callout warning (état initial)
- Aside : nouvelle ligne « Signée le » qui apparaît uniquement si `signed_at` est renseigné
- `updateAside()` synchronise avec le champ Date de signature du form
[1.18.11] — 2026-04-20
Fix — ententes : montants corrects + breakdown complet du calcul
- Legacy `Paiement_Montant` est la source de vérité (c'est ce qui a été facturé historiquement)
- Conversion m → pi dans Moorly perd de la précision (5.18m stocké, alors que legacy avait 5.1816m = 17 pi exactement). Conséquence : le calcul reconstruit par formule donne 729.70 au lieu de 730.
- Nouvelle approche : `total_amount = legacy.Paiement_Montant` ET `tarif_applied = total - SUM(services)` — garantit math balance parfaite
- `dock_services_detail` aussi normalisé (codes `hivernement`→`parc_hivernal`, `electricite`→`electricite_sans`)
- Résultat : 0 ententes en mismatch sur les 279
- Grille tarifaire 2026 passée en JS via `$tarifGrid` depuis le controller
- `bateauxMap` enrichi avec `width_m` et `bateau_type`
- Nouvelle fonction `computeTarifExplanation()` qui matche categorie + type_port + largeur dans la grille et retourne la formule
- Ligne "Tarif d'amarrage" dans le breakdown affiche 2 sous-lignes en gris :
- « Pêche · Principal · largeur 17 pi »
- « 700,00 $ + (17 − 16) pi × 30,00 $/pi = 730,00 $ » (ou « 700,00 $ (≤ 16 pi, tarif de base) »)
- Warning supplémentaire si le `tarif_applied` enregistré diffère du calcul grille
- `app/scripts/recompute-ententes-2026.php` — recalcule tarif + total depuis la grille tarifaire (approche pure formule). Non utilisé pour les données 2026 (on a préféré dériver depuis legacy), mais disponible pour les futures saisons sans legacy de référence.
[1.18.10] — 2026-04-20
Fix — ententes 2026 : codes services, totaux incorrects, port manquant, détail calcul
- 2 conventions coexistaient en BD : ancienne (`hivernement`, `electricite`) et nouvelle (`parc_hivernal`, `electricite_sans`)
- `UPDATE ... SET dock_services = REPLACE(...)` pour normaliser 62 ententes avec anciens codes → nouveaux codes
- Vue ententes mise à jour : checkbox value/id = nouveaux codes ; compat backward au chargement via codeMap
- Résultat : `["rampe", "quai", "hivernement", "electricite"]` devient `["rampe", "quai", "parc_hivernal", "electricite_sans"]` et coche correctement les 4 toggles
- Requête `UPDATE ... JOIN` qui prend `legacy.APdesIles_Liste.Paiement_Montant` comme source autoritaire pour les ententes 2026 où le montant existe côté legacy
- Fix exemple : JACK'S R BETTR' affichait 1300$ en Moorly alors que legacy a 1225$ (la bonne valeur) — corrigé
- Dans `openModal()`, 120 ententes 2026 ont un `port_id` mais pas d'`emplacement_id` — la fonction `setEntenteEmplacement()` retournait early sans setter le port, d'où le port vide dans l'aside
- Ajout d'un fallback : si pas d'emplacement, set le `port_id` directement + call `onEntentePortChange()`
- Nouvelle section "Détail du calcul" avec breakdown tarif d'amarrage + chaque service additionnel (nom + prix) = total
- `renderBreakdown()` lit `data.dock_services_detail` (snapshot des services avec prix au moment de la création/enrichissement) + `tarif_applied` + `total_amount`
- Warning jaune automatique si `tarif + Σservices ≠ total` (détecte les ajustements manuels ou incohérences)
- Rerender live quand le tarif est modifié dans le form
[1.18.9] — 2026-04-20
Data — Enrichissement des ententes 2026 depuis le dump legacy
- Lit le dump legacy chargé dans la BD `adminportuairedesiles` (tables `APdesIles_Liste`, `APdesIles_Liste_Navire`, `APdesIles_Usage_Type`, `APdesIles_Peche_Type`, `APdesIles_Services_Additionnels`)
- Match avec `moorly.ententes` par `bateau.registration_number + type + année`
- Enrichit sans écraser : `usage_type`, `peche_type`, `total_amount`, `dock_services` (JSON), `dock_services_detail` (snapshot prix), `service_autre`
- Mapping services : Service_Rampe→`rampe`, Service_Quai→`quai`, Service_Parc_Hivernement→`parc_hivernal`, Service_Electricite + Compteur→`electricite_compteur` sinon `electricite_sans`
- Modes `--dry-run` et `--verbose`
- Idempotent
- 291 legacy lus → 277 matchés → 179 updatés
- 145 `usage_type` corrigés (peche → pecheur_resident/saisonnier/occasionnel/plaisance)
- 110 `peche_type` ajoutés (homard/crabe/fletan/buccin/mactre/petoncle)
- 30 `total_amount` corrigés
- 122 `dock_services` JSON ajoutés (avec détails prix)
- État final : 1 usage_type invalide (résiduel), 4 sans services, 11 sans total
- Retiré le hardcode `$usageType = 'peche'` → maintenant `null` (ne crée plus de valeur invalide)
- Ajouté log d'avertissement quand une entente est créée avec détails minimaux
- Commentaires explicitant que `tarif_applied`, `total_amount`, `dock_services`, `peche_type` restent NULL/0 volontairement — à enrichir via le script d'import
- La logique de dé-dup existante (ligne 228-230) empêche déjà d'écraser une entente existante, donc la ré-exécution du sync est sûre
Gitignore
[1.18.8] — 2026-04-20
Fix — `/ententes` modal n'affichait pas les montants de l'entente
- Onglet Paiements scindé en 2 sections claires :
- Montants de l'entente : `tarif_applied` (numeric) + `total_amount` (numeric) — c'est ce qui est dû
- Paiement reçu : Mode + Date + Montant payé — c'est ce qui a été encaissé
- `tarif_applied` et `total_amount` passent en `type="number"` (étaient text avec placeholder trompeur)
- Aside "Montant" affiche maintenant `total_amount` en priorité (fallback → `tarif_applied` → `payment_amount`)
- `openModal()` populate maintenant `total_amount` depuis `data.total_amount` (manquait auparavant)
[1.18.7] — 2026-04-20
Data fix — split concatenated client addresses
- Split intelligent sur les virgules — prend les 3 derniers segments pour city/province/postal et le reste pour address (gère les cas 3, 4 et 5 virgules où la rue contient une virgule elle-même)
- 313 clients mis à jour sur `adminportuairedesiles`, 0 sur les autres tenants (données déjà propres)
[1.18.6] — 2026-04-20
Design applied — `/clients`
- Controller : `index()` charge `portsList` pour le filtre ; `datatable()` accepte `filter_port` (via ententes actives · COALESCE(port_id, quai.port_id)) et `filter_status` (`with_entente` / `without_entente` / `without_boat`)
- Datatable query enrichie avec : `first_bateau_name`, `first_bateau_reg`, `main_port` (port de l'entente active la plus récente)
- Filtres : search + port (dynamique depuis BD) + statut + type
- Table refondu 4 colonnes au lieu de 6 (plus dense, mieux hiérarchisé) :
- Client : avatar gradient initiales (34px) + nom + sub `company · email` ou `email · phone`
- Bateaux : "X bateaux" + nom du premier · immat mono (+N pour surplus)
- Port principal : nom port + ville · province sub (auto-dérivé via ententes actives)
- Statut : badges dynamiques (Entente active success / Sans entente neutral / Sans bateau warning + Commercial info)
- Card header avec `#tableCount` live mis à jour via drawCallback
[1.18.5] — 2026-04-20
Design applied — `/ports`
- Controller `index()` : nouveaux KPIs `seasonal`, `occupiedPlaces` (COUNT ententes actives avec emplacement_id), `adminCount`
- Controller `datatable()` : accepte `filter_admin` + `filter_status`, query enrichie avec `total_places` (SUM quais.total_places), `occupied_places` (COUNT ententes actives), `admin_president`
- 4 KPIs avec deltas meta ("4 administrations", "100% opérationnels", "847 occupées · 68%")
- Filtres search + administration (dropdown dynamique des admins) + statut (actif/saisonnier/inactif)
- Card `.ui-card-header` avec count live + bouton actualiser
- Table refondu :
- Port : icon anchor couleur plaisance + nom + description sub (ou "Port saisonnier")
- Administration : nom + "Président X" sub ; badge warning "Saisonnier" ou "Inactif" si pertinent
- Adresse : rue + coordonnées GPS mono sub
- Quais : count mono + "Q1 à QN" sub
- Places : occupied / total + % occupation sub
- Chevron last col
- Search custom debounced 250ms, drawCallback met à jour le count live
[1.18.4] — 2026-04-20
Design applied — `/administrations`
- Controller `datatable()` étendu : accepte `filter_province` et `filter_ports` (`with`/`without`), sort aussi sur le nom du président, ajoute un champ calculé `membres_count` (COUNT de `membres_ca` actifs par admin)
- Page header avec subtitle dynamique "X administrations · Y ports gérés · Z membres C.A. actifs"
- 4 KPIs avec deltas meta (stable, pourcentage avec ports, etc.)
- Filtres : recherche + province (liste distincte tirée de la BD) + statut ports (avec / sans)
- Card avec header `.ui-card-header` (count + meta "Triées par nom" + bouton actualiser icon-btn)
- Table refondu :
- Nom : icon building + nom + sub (short_name · ville)
- Président : nom + courriel/tel sub ; badge warning "Poste vacant" si NULL
- Ville : ville + province · code postal sub
- Membres C.A. : count mono + "actif(s)" sub (right-aligned)
- Ports gérés : badge info "X ports" avec icône ancre, ou badge neutre "0 port"
- Chevron last column
- Search custom remplace le input DataTables built-in (`dom: 'rtip'`) avec debounce 250ms
- `drawCallback` met à jour le count dans le card header
[1.18.3] — 2026-04-20
Design applied — `/login`, `/register`, `/change-password` (+ `/register-client`)
`layouts/auth.php`
- Chrome refondu : tokens Stitch inlined (`:root`), Manrope + Inter, FontAwesome 6
- Logo réel `moorly_dark.png` 36px centré au-dessus de la card (pas le placeholder "M")
- Card background white, border subtile, radius 14px, shadow ultra-subtile
- 3 largeurs configurables via `$authWidth` : `narrow` 380px / `default` 420px / `wide` 620px
- Footer `© 2026 Moorly · gestion portuaire` sous la card
- i18n FR/EN via `I18n::locale()`
- Bootstrap 5.1.3 retiré (tokens CSS natifs à la place)
- Form components : `.auth-title`, `.auth-subtitle`, `.auth-field`, `.auth-label`, `.auth-input`, `.auth-select`, `.auth-input-group`, `.auth-checkbox`, `.auth-btn-primary`, `.auth-alert-*`, `.auth-grid-2`
`login.php`
- Titre + subtitle centrés
- Remember-me checkbox
- Lien "Oublié ?" à droite du label mot de passe
- Alert success si `?password_changed=1` (retour depuis change-password)
- Lien "Créer un compte" en footnote
`register.php` (inscription organisation)
- Card wide 620px, form organisé avec grid-2 pour email + password
- Type d'organisation avec notice success conditionnelle pour AP PPB ("licence gratuite")
- Input-group pour sous-domaine avec suffix `.moorly.ca`
- Checkbox "J'accepte les conditions" + liens vers politiques CMS
`register-client.php` (inscription client d'un port)
- Card wide, grid-2 Prénom/Nom, form simple
`change-password.php` (reset / forced password change)
- Alert warning si temp password forcé
- Form : nouveau mot de passe + confirmation
- Lien retour dashboard si non forcé
[1.18.2] — 2026-04-20
Design applied — `/aide` (centre d'aide)
- Layout switched from custom `.aide-layout / .aide-sidebar / .aide-nav-link` to canonical `.ui-settings / .ui-settings-nav / .ui-settings-link` (same pattern que Configuration)
- Page header avec search bar inline à droite (plutôt qu'au-dessus du layout)
- Search dropdown restylé `.aide-search-results` avec titre + section + snippet
- Article : h1 Manrope display + meta line (date, temps de lecture) sous le titre
- Section headers avec icônes identiques par page (groupe visuel clair)
- `.aide-step` badges inline pour les instructions étape-par-étape
- Legacy `.tip` et `.warning` classes préservées (compatibilité avec les fichiers HTML existants)
- Nouveau bloc feedback "Cet article vous a-t-il aidé ?" (👍/👎) avec thanks confirmation
- Pagination prev/next au bas avec titres complets
- i18n FR/EN préservée
[1.18.1] — 2026-04-19
Design applied — `/errors/404`
- Logo Moorly compact en haut
- Compass SVG animée (14s spin linéaire, opacity 0.35, respecte `prefers-reduced-motion`)
- Code 404 Manrope 96px navy, letter-spacing tight
- Titre + body descriptif (i18n FR/EN)
- 2 boutons d'action : Retour (ghost, history.back) + Tableau de bord (primary, /dashboard)
- Support mailto footer
- Standalone (pas de dépendance sur layouts/dashboard.php) — tokens Stitch inlined
- FontAwesome 6 + Manrope/Inter via CDN
[1.18.0] — 2026-04-19
Design Preview v2 — 30 nouveaux mockups HTML statiques
9 sections couvertes (44 mockups au total = 14 exemplars + 30 nouveaux)
Approche par mockup
- Chaque vue pensée pour son métier (1 mockup = 1 vue, granularité A)
- Pattern hybride C : `bateaux`-style pour les CRUD purs ; layouts dédiés pour quais (carte), tarifs (settings multi-section), rapport-financier (rapport print), ppb-outils (launcher cards), pages publiques (chrome public)
- Tous les mockups utilisent shared.css (les classes déjà définies). Inline `<style>` minimal, scopé.
- Sidebar full app (7 sections, 30+ liens) sur toutes les vues authentifiées, sauf client-portal (sidebar simplifiée)
- Contenu réaliste Îles-de-la-Madeleine : noms, bateaux, ports, années 2026, montants CAD format français
- Index `index.html` mis à jour : 9 sections claires, processus de review documenté
Aucun changement prod
[1.17.41] — 2026-04-19
Refonte deep — Section Opérations (3 vues)
`/machinerie`
- Controller : 4 KPIs (total / à compléter / cette semaine / complétées ce mois) avec try/catch + filter_type/filter_status pour datatable
- Vue : header + KPIs (variant warning si à compléter > 0) + filtres search/type/status
- Modal `.ui-modal-shell` : aside icône dynamique selon type d'opération + nom du bateau + badge statut + stats port/équipement/planifié/complété + bouton supprimer
- 4 sections : Tâche (type + équipement) / Bateau & port / Planning (planifiée/complétée/opérateur) / Notes
- Live `updateAside()` avec icône + statut dynamiques selon date complétée
`/incidents`
- Controller : 4 KPIs (ouverts & en cours / urgents / reçus ce mois / résolus ce mois) avec variants warning/danger conditionnels
- Vue : header + KPIs + filtres status/category
- Modal `.ui-modal-shell` : aside icône catégorie dynamique + titre + badges statut/priorité + photo dans aside (si fournie) + stats port/loc/signalé par/résolu + bouton supprimer
- 3 tabs : Détails (catégorie/priorité/titre/description) / Photo & localisation / Suivi admin (statut + notes internes, conditionnel)
- Photo preview synchrone aside + tab média
`/pos` (terminal de caisse + sous-vues)
- Terminal `/pos` : déjà polished avec `.ui-pos-*` classes dédiées, pas touché
- `/pos/catalogue` : header reformaté avec `.ui-page-actions`, cards converties en `.ui-card-header` + `.ui-card-title`, tables en `card-body p-0`
- `/pos/rapports` : header `.ui-page-actions`, KPI bootstrap row → `.ui-kpi-row`, cards `.ui-card-header`, filtres `.ui-filters`
[1.17.40] — 2026-04-19
Fix — classes inventées dans facturation, comptabilite, tarifs
`/facturation`
- Modal réécrit selon le pattern canonique (bateaux/ententes) : `.ui-modal-shell` + `.ui-preview-icon` + `<dl class="ui-stats">` + `.ui-modal-aside-actions` + `.ui-modal-content` + `<section class="ui-section">` + `.ui-form-grid-3` + `.ui-field/-label/-input` + `<footer class="ui-modal-footer">`
- `.btn-close` remplacé par `.ui-icon-btn` dans `.ui-modal-close-row`
- Bouton imprimer + supprimer regroupés dans `.ui-modal-aside-actions` (sortis du footer)
`/comptabilite`
- Sed batch : `.ui-form-field` → `.ui-field`, `.ui-form-label` → `.ui-label`, `.ui-form-grid` → `.ui-form-grid-2`, `.ui-callout-info` → `.ui-callout`, `.ui-card-sub` → `.ui-card-meta`, `.ui-btn-block` → `.ui-btn-full`, `.ui-empty-state*` → `.ui-empty/-title/-desc/-illustration`
- Callout "Formats supportés" restructuré avec `<i>` + `<strong>` (pas `.ui-callout-title`)
`/tarifs`
- Mêmes sed batch que comptabilite
[1.17.39] — 2026-04-19
Fix — modals administrations & ports cassés visuellement (regressed in v1.17.38)
- `.modal-content.ui-modal-shell` + `.ui-modal-aside` + `.ui-modal-content`
- `.ui-preview-icon` (pas `.ui-modal-aside-icon`)
- `<dl class="ui-stats">` + `<div class="ui-stat"><dt>...<dd>...` (pas `.ui-modal-aside-stat-label/-value`)
- `.ui-modal-aside-actions` (pas `.ui-modal-aside-foot`)
- `<nav class="ui-modal-tabs">` + `<button class="ui-modal-tab">` + JS show/hide `.tab-pane-ui` (pas Bootstrap nav-tabs)
- `<footer class="ui-modal-footer">` + `.ui-modal-footer-meta` + `.ui-modal-footer-actions` (pas `.ui-modal-foot/-foot-actions`)
- `<section class="ui-section">` + `<header class="ui-section-header"><h3 class="ui-section-title">` + `.ui-section-desc`
- `.ui-form-grid-2` / `.ui-form-grid-3` + `.ui-field` + `.ui-label` + `.ui-input` (pas `.ui-form-grid/-field/-label` ni `.form-control`)
- `.ui-icon-btn` (close + delete row icons)
- `.ui-btn-full` (pas `.ui-btn-block`)
- `.ui-callout` simple flex (pas `.ui-callout-info/-body/-title`)
[1.17.38] — 2026-04-19
Refonte deep — Section Infrastructure (3 vues)
`/administrations`
- Controller : 4 KPIs (total / avec ports / ports gérés / membres C.A. actifs) avec try/catch
- Vue : header + KPIs + callout info + table .ui-table
- Modal `.ui-modal-shell` avec aside icône building + nom + nom court + badge langue + stats ville/province/président/bail/membres + bouton supprimer
- 4 tabs : Identification / Enregistrement & taxes / Officiers (5 cards) / C.A. & bail (dropzone PDF + tableau membres dynamique)
- Live `updateAside()` sur name/short/city/province/president/language
`/ports`
- Controller : 4 KPIs (total / actifs / quais / places totales)
- Vue : header + KPIs (variant success pour actifs) + callout + table avec badges status colorés
- Modal `.ui-modal-shell` aside icône anchor + preview hero image + badge status + stats adresse/coordonnées + lien carte OSM + bouton supprimer
- 3 tabs : Informations / Localisation (avec callout coordonnées GPS) / Image (dropzone hero)
- Live `updateAside()` + preview synchrone hero image dans aside
`/quais` (carte d'amarrage Leaflet)
- Controller : 4 KPIs (ports actifs / lignes / emplacements / sondages bathy)
- Header + KPIs ajoutés au-dessus de la carte (hauteur ajustée à 100vh - 280px)
- 7 panneaux latéraux uniformisés via classe `.quai-panel` (background blanc + border + padding cohérents)
- Toute la logique map JS (Leaflet, Turf, bathymétrie 3D, draw lignes, gestion spots) préservée intacte
[1.17.37] — 2026-04-19
Refonte deep — Section Finance (suite, 4 vues)
`/facturation`
- Controller : 4 KPIs (à recevoir / en retard / encaissé mois / en attente) avec try/catch
- Vue : header + KPIs avec variants warning/danger/success conditionnels + filtres
- Modal `.ui-modal-shell` : aside icône invoice + référence + client + badges status + stats montant/taxes/total/échéance/payée + bouton imprimer
- 3 sections de form : Entente liée / Montants / Échéances & statut — `updateAside()` live sync sur tous les champs
`/comptabilite`
- Controller : 4 KPIs (exports année / lignes exportées / total exports historique / dernier export)
- Vue : header + KPIs + 2 colonnes (config logiciel + plan comptable / export + historique)
- Cards reformatées en `.ui-card` + `.ui-card-header` + `.ui-callout-info` pour formats supportés
- Historique en `.ui-table` avec `.ui-empty-state` quand vide
`/rapport-financier`
- Header reformaté en `<header class="ui-page-header">` avec sous-titre + `.ui-page-actions`
- Contenu rapport (rendu JS dynamique) wrappé dans `.ui-card` pour cohérence visuelle
- JS de rendu détaillé par port/type/saison préservé tel quel
`/tarifs`
- Controller : 4 KPIs (tarifs amarrage / services actifs / tarif base moyen / années configurées)
- Header reformaté + `.ui-page-actions` + KPI row
- 3 cards principales (Portée, Catégories tarifs, Services additionnels) refaites avec `.ui-card-header` + `.ui-card-tools`
- Tables `table-sm` → `.ui-table`, empty states avec `.ui-empty-state`
- Card Périodes refaite en `.ui-form-grid` + `.ui-form-field`
- Modals : header bleu marine remplacé par header neutre standard
- Toute la logique JS (changeScope, edit/save tarif/service, periodes, copy year, overrides) préservée
[1.17.36] — 2026-04-19
Refonte deep — Section Finance (1/5) : `/transactions`
- Controller : 4 KPIs agrégés (paiements + pos_transactions) — encaissé mois/jour, annulées du mois, moyenne par transaction (toutes try/catch pour multi-tenant)
- Vue : header + 4 KPIs (avec variant warning si annulées > 0) + filtres search/type/method/status
- Table `.ui-table` 8 colonnes : date mono, client/port, détail, n° référence (badge POS si applicable), montant (line-through si annulé), méthode avec icônes (cheque/virement/comptant/carte/Stripe), référence externe, action (bouton void ou badge annulée)
- Modal void restylé `.ui-callout` warning + `.ui-btn-danger-ghost`
- JS, IDs, endpoints API et logique TransactionsService inchangés
[1.17.35] — 2026-04-19
Refonte deep — Section Conformité (5 vues)
`/conformite`
- Controller : 4 KPIs (valides / expirent < 30j / expirés / manquants), table `conformites`, all try/catch
- Vue : header + 4 KPIs avec variants warning/danger conditionnels + filtres search/type/status
- Table `.ui-table` avec badges type, dates expiration colorées (rouge si expiré, jaune si < 30j) + jours restants
- Modal `.ui-modal-shell` : aside icône clipboard + type + client + bateau + badges status/expiry + lien doc + delete
- 2 tabs : Informations (client/bateau + type/expiry/status) / Document (URL)
`/compliance` (Suivi documentaire)
- Vue : header avec 2 boutons bulk alert + 4 KPIs (expirées danger + expirent warning + manquantes neutre + alertes envoyées)
- Card avec nav tabs (Expirées / Expirent / Manquantes / Alertes envoyées) en `.ui-modal-tabs` + counters
- 4 tables `.ui-table` chacune avec actions cloche pour notifier le client + bouton résoudre
`/infractions`
- Controller : 4 KPIs (actifs / ce mois / frais à percevoir / émis cette année)
- Vue : header avec actions Types + Signaler + 4 KPIs avec variants warning/danger + filtres
- Table avec badges colorés type d'avis (verbal/écrit/écrit+frais), montants frais en mono, statut
- Modal `.ui-modal-shell` : aside icône gavel rouge + type d'infraction + bateau + badges + stats port/date/frais/émis-par + bouton imprimer
- 3 tabs : Lieu & navire / Avertissement (type/frais/statut/notification) / Description & preuve (textarea + photo upload preview)
- Modal types d'infractions restylé en `.ui-table`
`/legal-docs`
- Controller : 4 KPIs (total / publiés / brouillons / modifiés ce mois) sur `cms_pages WHERE doc_type IS NOT NULL`
- Vue : header + KPIs + callout info + card englobante pour le grid de docs
- Editor modal et AI prompt modal préservés tels quels (riches, complexes — leur fonctionnement reste intact)
`/ppb-outils` (Boîte à outils PPB)
- Header restylé avec sous-titre clarifiant la mission
- 5 cards d'entrée (Résolutions / Loi / Avis / Petites créances / Guides) refaites avec `.ui-product-icon` colorés + titres Manrope + hover lift
- Sections dynamiques (contentCard + docListCard) restructurées en `.ui-card-header` + `.ui-card-tools` avec boutons `.ui-btn`
- Modal éditeur de document préservé (rich text editor PPB)
[1.17.34] — 2026-04-19
Fix — table names section Clients (permis + bookings)
- `permis` → en réalité `permis_commerciaux` (PermisController)
- `bookings` → en réalité `booking_requests` (BookingController)
- `waitlist` → en réalité `cms_waitlist` (BookingController)
Corrigé
- `PermisController::index` : KPIs queries pointent vers `permis_commerciaux` + try/catch sur chaque
- `BookingController::adminIndex` : KPIs queries pointent vers `booking_requests` et `cms_waitlist` + try/catch
[1.17.33] — 2026-04-19
Fix — `/clients` 500 error
Corrigé
- Toutes les queries KPI dans `ClientsController::index` wrappées en `try/catch \Throwable` → robustes face aux schémas tenant variables
- KPI "Nouveaux ce mois" remplacé par "Sans bateau" (plus utile + colonne disponible) avec variant warning si > 0
[1.17.32] — 2026-04-19
Refonte deep — Section Clients (4 vues)
`/clients`
- Controller : 4 KPIs (total / avec entente cette saison / commerciaux / nouveaux ce mois) + filter_type (commercial/individual)
- Vue : header `.ui-page-header` + KPIs + filtres search/type + table `.ui-table` avec avatar gradient initiales + ville/province
- Modal `.ui-modal-shell` XL : aside avec avatar 64px gradient + nom + compagnie + badges Commercial/Individuel + stats email/phone/ville + boutons Exporter/Supprimer + temp password callout
- 5 tabs : Renseignements / Navires / Ententes / Factures / Dossier
- Tab Renseignements : 5 sections `.ui-section` (identification, coordonnées, contact urgence, réseau Moorly avec toggle, notes)
- Tabs Navires/Ententes : cards cliquables redirection vers /bateaux ou /ententes (border accent navy ou success selon statut)
- Tab Factures : 3 KPIs (total facturé/payé/solde dû) + groupes par admin avec batchPay
- Tab Dossier : Signalements + Avertissements
`/permis` (Clients commerciaux)
- Controller : 4 KPIs (actifs / entreprises / non payés / revenus année)
- Vue : header + KPIs + filtres search/year/port/status + table avec avatar violet `.ui-row-icon` + badges type/payment/status
- Modal `.ui-modal-shell` : aside icône violette + nom entreprise + contact + badges + stats type/port-année/période/montant
- 4 tabs : Identification / Permis / Paiement / Notes
`/demandes`
- Controller : 4 KPIs (réservations en attente / approuvées 30j / liste d'attente en attente / total inscriptions)
- Vue : header + KPIs + tabs Bootstrap nav-tabs (Réservations / Liste d'attente) avec compteurs `.ui-tab-badge`
- Tables `.ui-table` avec `.ui-row` pour chaque tab, formats date mono, badges colorés
- Detail modal restylée : sections `.ui-section` avec headers, callouts pour estimation tarifaire, bars `.ui-bar-row` pour disponibilité emplacements
- Boutons footer en `.ui-btn-primary/ghost/danger-ghost`
`/renouvellements`
- Controller : KPIs par renewal_status (pending_review / pending_client / confirmed / declined)
- Vue : header avec actions (Relancer tous + Générer) + 4 KPIs (warnings si pending_review/client > 0) + filtres search/status/season/port + table avec diff tarifaire (+/- ancien tarif)
- Detail modal `.ui-modal-shell` XL : aside avec icône rotate + client + bateau + badge statut + stats port/saison/période
- 2 tabs : Détails (sections client+navire+emplacement) / Tarifs & ajustement (KPI cards comparaison + override input + callout)
- Generate modal restylé en `.ui-section` avec callout explicatif
[1.17.31] — 2026-04-19
Refonte modals admin + danger zone licence
CMS modal — refonte aside+tabs
- Modal `.ui-modal-shell` XL avec aside (icône pen, titre page, slug, badge publié/brouillon, stats port/ordre/document PDF, lien voir page publique, delete)
- 4 tabs `.ui-modal-tabs` : Général (titre, slug avec hint live, port, ordre, toggle publier, image héro) · Contenu (chips FR/EN + Code/Aperçu, textarea HTML) · Document PDF (callout success preview + upload) · Assistant IA (panneau gradient violet avec mode Créer/Modifier, langues, prompt, contexte, bouton génération)
- `updateAside()` sync titre/slug/port/ordre/badges en live · `toggleAiPanel()` redirigé vers click sur tab IA · footer avec meta + actions
- Tous les IDs JS et fonctions préservés (autoSlug, switchLangTab, showContentTab, generateWithAi, updateAiModeUI, syncEditorToHidden)
Utilisateurs modal — refonte aside+tabs
- Modal `.ui-modal-shell` XL avec aside (avatar gradient initiales, nom, email, badges statut+admin, stats rôle/langue/dernière connexion, delete)
- 3 tabs : Identité (username/email + langue) · Rôle & accès (rôle avec descriptions, port_access JSON) · Sécurité (statut, password)
- `updateAside()` live sync nom/email/avatar/badges selon username/role/status
Licence — danger zone restaurée
- Section retirée par erreur lors du refactor v1.17.29 ré-ajoutée
- Carte `.ui-card` "Zone dangereuse" avec callout info conditions de résiliation, 2 boutons danger (résilier abonnement / supprimer données), callout export Loi 25
- Functions `confirmCancelAccount()` / `confirmDeleteData()` réintégrées (POST `/api/account/cancel` et `/api/account/request-deletion`)
[1.17.30] — 2026-04-19
Refonte deep — `/analytiques` (Dashboard analytics)
Modifié
- `views/dashboard/analytiques.php` :
- Header `.ui-page-header` avec subtitle
- Filtres en `.ui-filters` (period select + custom from/to + source select + bouton actualiser primary à droite)
- 4 KPIs `.ui-kpi` mono numbers (visites/uniques/pages-par-visite/taux-rebond) avec deltas
- Chart card `.ui-card` avec header + canvas Chart.js intact
- Grid 2-cols `.ui-grid-2` pour Pages populaires + Referrers (tables avec `.ui-row` + `.ui-mono`)
- Grid 3-cols pour Navigateurs / Appareils / Systèmes (chacun en `.ui-card`)
- `loadTopPages` / `loadReferrers` rendu en `.ui-row` + `.ui-row-name` + `.ui-mono`
- `renderBarList` rendu en `.ui-bars > .ui-bar-row` au lieu d'inline styles
Préservé
- Chart.js inchangé (line chart visites + uniques)
- `getFilters`, `loadAll`, `loadSummary`, `pctChange`, `loadDevices` JS intacts
- API endpoints : `/api/analytics/summary`, `/top-pages`, `/referrers`, `/devices`
[1.17.29] — 2026-04-19
Refonte deep — `/licence` (Plan + abonnement)
Modifié
- `views/licence/index.php` :
- Section "Current Plan + Usage" : layout 2-col `.ui-grid-2`
- Card plan actuel : gradient navy plein avec icône cercle blanc, name Manrope 24px, badges plan + actif, dl info organisation/type/sous-domaine/renouvellement
- Card usage : `.ui-bars` avec `.ui-bar-row` (Ports, Utilisateurs) + valeurs `.ui-mono`. Callout success Stripe + bouton danger annuler si abonnement actif.
- Plans tiers : `.lic-plan-card` (3 cards Free/Pro/Entreprise) avec tag "Plan actuel" / "Recommandé", price `.lic-plan-price` Manrope navy, features list `.lic-plan-features` avec checks verts, toggle Mensuel/Annuel pour Pro en `.ui-chip`
- CTA buttons en `.ui-btn ui-btn-primary/ghost ui-btn-full`
Préservé
- Functions JS : `toggleProCycle`, `cancelSubscription`, `upgradePlan`
- API endpoints : `/api/stripe/cancel-subscription`, `/api/stripe/checkout/plan` inchangés
- Logique conditional : `$isAP`, `$canUpgrade`, `$hasStripe`, `$hasSubscription`
[1.17.28] — 2026-04-19
Refonte deep — `/cms` (Contenu du site)
Modifié
- `CmsController::index` : 5 KPIs (total/published/drafts/pdf/modifiedThisMonth) calculés depuis cms_pages
- `CmsController::datatable` : nouveaux filtres `filter_port` (incluant valeur "common" pour pages sans port) et `filter_status` (published/draft) + `cp.document_url` ajouté au SELECT
- `views/cms/index.php` :
- Header `.ui-page-header` avec subtitle calculé
- 4 KPIs `.ui-kpi` (warning si drafts > 0)
- Filtres search + select port (avec option "Pages communes") + select statut
- Table avec drag-handle préservé, slug en code, indication PDF joint sous le titre, badge statut publié/brouillon, chevron au hover
- `createdRow` ajoute `.ui-row` class
Préservé intact
- Drag-and-drop reorder (DataTables RowReorder + `/api/cms/reorder`)
- Modal édition complète : tabs FR/EN, panneau IA génération (Claude/OpenAI), upload PDF, prévisualisation contenu HTML, AI mode create/edit
- IDs DOM : `dataTable`, `editModal`, `editForm`, `editId`, `title`, `slug`, `port_id`, `sort_order`, `published`, `hero_image`, `document_file`, `docPreview`, `docLink`, `aiPanel`, `aiPrompt`, `aiContext`, `contentEditor`, `contentPreview`, `langTabFr/En`, `tabCode/Preview`, `content_fr/en`, `pwHint`, `deleteZone`, `aiStatus`, `btnGenerate`, `aiLangFr/En`, `aiPromptLabel`, `aiModeCreateLabel/EditLabel`
- Functions JS : `openModal`, `saveRecord`, `deleteRecord`, `reloadTable` (ajoutée), `autoSlug`, `updateAiModeUI`, `toggleAiPanel`, `switchLangTab`, `showContentTab`, `syncEditorToHidden`, `generateWithAi`
- API endpoints inchangés : `/api/dt/cms`, `/api/cms/:id`, `/api/cms/reorder`, `/api/cms/generate`
[1.17.27] — 2026-04-19
Refonte deep — `/utilisateurs` (Admin section)
Modifié
- `UtilisateursController::index` : 4 KPIs scoppés à license (total/actifs ce mois/inactifs >30j/admins) + max plan
- `UtilisateursController::datatable` : nouveaux filtres `filter_role` et `filter_status`
- `views/utilisateurs/index.php` réécrit complet (183 → ~280 lignes) :
- Header `.ui-page-header` avec subtitle calculé
- 4 KPIs `.ui-kpi` avec variantes warning si inactifs >0
- Filtres search + select rôle + select statut
- Table `.ui-table` avec : avatar gradient (initiales) + nom/email, badge rôle (info si admin), langue, badge statut (warning si inactif >30j)
- Modal `.modal-lg` avec 3 sections `.ui-section` : Identité, Rôle/accès, Sécurité
- Footer modal avec delete (gauche) + cancel/save (droite)
Préservé
- IDs DOM : `dataTable`, `editModal`, `editForm`, `editId`, `username`, `email`, `role`, `language_pref`, `status`, `port_access`, `password`, `pwHint`, `deleteZone`
- Functions JS : `openModal`, `saveRecord`, `deleteRecord`, `reloadTable`
- API endpoints : `/api/dt/utilisateurs`, `/api/utilisateurs/:id` inchangés
[1.17.26] — 2026-04-19
Extension migration — Modal headers light theme everywhere
Corrigé
- Inline `.modal-header` gradient retiré → `stitch.css` prend la main → tous les modals legacy (clients, permis, incidents, demandes, legal-docs, utilisateurs, etc.) ont maintenant un header light surface-low avec titre Manrope navy.
- `.btn-close-white` neutralisé sur fond clair (filter:none + opacity assouplie) — visible et harmonieux.
- `.modal-content` style aussi délégué entièrement à stitch.css.
[1.17.25] — 2026-04-19
Extension migration — Final responsive CSS migrated to .ui-*
- Media queries `(max-width: 991px)` et `(max-width: 576px)` : `.s-page-header/title/subtitle`, `.s-stat-card/value/label`, `.s-btn` retirés (HTML déjà migré ailleurs).
- `.ui-kpi/value/label` ajoutés à la place pour les overrides mobile.
- Lien hover selectors `:not(.s-btn)` etc. → `:not(.ui-btn)`, `:not(.ui-sidebar-link)`.
[1.17.24] — 2026-04-19
Extension migration — Form controls + final dead CSS cleanup
- `.form-control` / `.form-select` / `.form-label` globaux (Bootstrap defaults overridés dans dashboard.php inline) : passent du style legacy gris-fond / sans border au style design system (border subtle, fond blanc, focus ring navy, label uppercase 11px). S'applique automatiquement à toutes les vues legacy.
- `.s-alert*` retiré (jamais utilisé en HTML)
- `.s-filters` CSS retiré (toutes refs migrées vers `.ui-filters` — y compris dans `rapports/financier.php` print CSS).
- Media query mobile `.s-filters` → `.ui-filters` aussi swappé.
[1.17.23] — 2026-04-19
Extension migration — Cleanup CSS legacy round 2
- `.s-stat-card` + `.s-stat-card--navy` + variants `.s-stat-label/value/sub/icon` (~25 lignes)
- `.s-page-header`, `.s-page-title`, `.s-page-subtitle` (~10 lignes)
- `.s-btn` + `.s-btn-primary/secondary/outline/danger` + hovers (~30 lignes)
- `.s-form-section` + `.s-step` (déjà retirés en v1.17.22 — confirmé)
- `.s-page-title, .s-stat-value, .s-alert-count` simplifié dans la règle Manrope shared
[1.17.22] — 2026-04-19
Extension migration — Bootstrap modal harmony
- Boutons modal (`.btn-primary`, `.btn-outline-primary`, `.btn-secondary`, `.btn-outline-secondary`, `.btn-danger`, `.btn-outline-danger` dans `.modal`) : adoptent le style `.ui-btn` (navy primary plat, ghost secondary, danger-ghost rouge).
- Form switches/checks (`.ui-section .form-check-input`) : couleur primary navy quand checked, focus ring subtil.
- Modal headers/footers (`.modal-content > .modal-header/.modal-footer`) : background surface-low, padding harmonisé, titre Manrope 16px navy au lieu du gradient legacy. `.btn-close` aussi assoupli.
[1.17.21] — 2026-04-19
Extension migration — Bootstrap nav-tabs styling + form controls
- `.modal-content .nav-tabs` : les tabs Bootstrap des modals (`/clients`, `/permis`, `/incidents`, `/demandes`, `/legal-docs`) ressemblent maintenant à `.ui-modal-tabs` (underline navy active, padding harmonisé, pas de border legacy). JS Bootstrap toggle-tab préservé intact.
- `.ui-section .form-control` / `.form-select` / `.form-label` : Bootstrap form controls dans un wrapper `.ui-section` adoptent visuellement le style `.ui-input` / `.ui-label` (border subtle, focus ring navy, label uppercase 11px).
[1.17.20] — 2026-04-19
Extension migration — Form sections (.s-form-section + .s-step → .ui-section-header)
[1.17.19] — 2026-04-19
Extension migration — Stat cards (.s-stat-* → .ui-kpi*)
- `s-stat-card` → `ui-kpi`
- `s-stat-card--navy` → `ui-kpi` avec gradient inline (variant primary)
- `s-stat-label` → `ui-kpi-label`
- `s-stat-value` → `ui-kpi-value`
- `s-stat-sub` → `ui-kpi-delta ui-kpi-delta-muted`
[1.17.18] — 2026-04-19
Extension migration — Final sweep (variants)
- `card border-0 shadow-sm rounded-4 mb-3/4`, `card border-0 shadow-sm rounded-3 mb-3/4`
- `card border-0 shadow-sm rounded-4 h-100`, `card border-0 shadow-sm rounded-3 h-100`
- `card border-0 shadow-sm rounded-4 overflow-hidden`
- `card border-0 shadow-sm rounded-4 p-4`
- `card border-0 shadow-sm rounded-3 mb-2` (dans du JS)
- `s-filters mb-3`
- `partials/signature_pad.php` bouton submit
[1.17.17] — 2026-04-19
Extension migration — Groupes 7 + 8 (clients views + POS sub)
[1.17.16] — 2026-04-19
Extension migration — Groupe 6 (meta)
[1.17.15] — 2026-04-19
Extension migration — Groupe 5 (admin/secondaire)
[1.17.14] — 2026-04-19
Extension migration — Groupe 4 (finance)
[1.17.13] — 2026-04-19
Extension migration — Groupe 3 (conformité)
[1.17.12] — 2026-04-19
Extension migration — Groupe 2 (infrastructure)
[1.17.11] — 2026-04-19
Extension migration — Groupe 1 (high-traffic)
Swaps appliqués
- `s-page-header/title/subtitle` → `ui-page-header/title/subtitle`
- `s-btn s-btn-primary` → `ui-btn ui-btn-primary`
- `s-btn s-btn-outline/secondary` → `ui-btn ui-btn-ghost`
- `s-btn s-btn-danger` → `ui-btn ui-btn-danger-ghost`
- `s-filters` → `ui-filters`
- `card border-0 shadow-sm rounded-*` → `ui-card`
- `table table-hover` → `ui-table`
[1.17.10] — 2026-04-19
Task 8/8 — Cleanup CSS legacy (partiel)
Retiré
- `.sidebar`, `.sidebar .logo`, `.sidebar .nav-*`, `.sidebar .sidebar-bottom` (~120 lignes) : remplacées par `.ui-sidebar*` dans stitch.css.
- `.topbar`, `.topbar .search-bar`, `.topbar .user-info`, `.topbar .user-avatar` (~40 lignes) : remplacées par `.ui-topbar*`.
- `.main-content` : remplacée par `.ui-main`.
- `.mob-hamburger`, `.mob-overlay` : remplacées par `.ui-mob-*`.
- Media queries correspondantes nettoyées : règles `.sidebar`, `.topbar`, `.main-content`, `.mob-hamburger` retirées des `@media (max-width: 991px)` et `(max-width: 576px)`.
Conservé
- 38 références `.s-*` dans le CSS inline : `.s-btn*`, `.s-stat-card*`, `.s-page-header`, `.s-page-title`, `.s-page-subtitle`, `.s-form-section`, `.s-step`, `.s-badge`, `.s-filters`, `.s-banner`, `.table` overrides, `.modal-content`, `.bg-light`. Ces classes sont encore utilisées par 30+ vues non-migrées (clients, ports, quais, administrations, incidents, infractions, etc.). Cleanup complet possible uniquement après migration de toutes les vues.
Migration UI polish — État final
- 5 vues principales au nouveau design system (`/bateaux`, `/ententes`, `/dashboard`, `/configuration`, `/pos`)
- Shell complet migré (sidebar collapsible + topbar search + mobile hamburger)
- `stitch.css` ~2 500 lignes = single source of truth pour `.ui-*`
- CSS legacy inline réduit au strict nécessaire pour les vues restantes
Migration restante (future)
[1.17.9] — 2026-04-19
Fix — POS renderCart cartEmpty reference
Corrigé
- `#cartEmpty` div supprimé du markup initial — le container `#cartItems` commence vide
- Empty state stocké en constante `CART_EMPTY_HTML` et injecté par `renderCart()` quand `cart.length === 0`
- Render initial au `$(function() { renderCart(); })` pour afficher l'empty state au chargement
[1.17.8] — 2026-04-19
POS — Alignement visuel avec le mockup design-preview
Ajouté
- Barre de recherche produit en haut du catalogue (`.ui-filter-search-lg`) — filtre par nom en live combiné au filtre catégorie
- Compteurs sur chips catégories (ex: « Carburant 3 ») calculés depuis les items
- Section client sélectionné refaite en `.ui-pos-client` avec avatar gradient teal→navy, nom + sub-info (email), bouton Changer
- Cart items rendus avec structure `.ui-pos-item` + `.ui-pos-item-info` + `.ui-pos-qty` (qty input stylé) + `.ui-pos-item-total` (visuellement identique au mockup)
Modifié
- `renderCart()` JS : markup `.cart-line` legacy → `.ui-pos-item` avec classes canoniques
- `selectClient(id, name, sub)` : signature étendue pour sub-info (email/phone) + calcul initiales pour avatar
- `clearClient()` / `setWalkIn()` : alternent correctement entre `#clientSearchZone` (search) et `#selectedClient` (pos-client)
- `newSale()` : reset aussi la recherche produit + filter
Résultat
[1.17.7] — 2026-04-19
Fix — POS onclick attribute parsing
Corrigé
- `views/pos/index.php` : `onclick="addToCart(<?= js($x) ?>, …)"` → `onclick="addToCart(<?= esc(js($x)) ?>, …)"`. `esc()` convertit `"` en `"`, décodé correctement par le browser à la lecture de l'attribut puis évalué par le moteur JS.
[1.17.6] — 2026-04-19
Task 7/8 — Migration `/pos` (point de vente)
Modifié
- `app/views/pos/index.php` réécrit (371 → ~400 lignes) :
- Header `.ui-page-header` avec liens rapides Catalogue + Rapports
- Empty state si catalogue vide : `.ui-empty` avec CTA seed defaults
- Layout `.ui-pos` : catalogue à gauche (fr 1fr) + panier sticky à droite (400px)
- Catalogue : `.ui-pos-categories` (chips filtres) + `.ui-pos-grid` (4 colonnes produits) avec `.ui-product-card` (icône colorée, nom, unité, prix navy)
- Panier : header avec total items + bouton reset, input client search + bouton Passage, client sélectionné badge, liste items avec qty +/- et trash, totaux (sous-total / TPS / TVQ / Total grand), 4 boutons méthode paiement `.ui-payment-btn`, bouton Encaisser sticky
- Receipt modal : icône check verte, numéro transaction, total en grand navy Manrope, boutons Imprimer + Nouvelle vente
Préservé
- Variables JS : `cart`, `selectedClientId`, `paymentMethod`, `lastTxnId`, `TPS_RATE`, `TVQ_RATE`
- Functions : `filterCategory`, `addToCart`, `removeFromCart`, `changeQty`, `renderCart`, `searchClient`, `selectClient`, `clearClient`, `setWalkIn`, `setPayMethod`, `processPayment`, `printLastReceipt`, `newSale`, `seedDefaults`
- IDs : `categoryTabs`, `itemsGrid`, `clientSearch`, `clientResults`, `selectedClient`, `clientName`, `cartItems`, `cartEmpty`, `cartTotals`, `subtotalDisplay`, `tpsDisplay`, `tvqDisplay`, `totalDisplay`, `paymentSection`, `receiptModal`, `receiptNumber`, `receiptTotal`
- Classes hook : `.pos-cat-btn`, `.pos-item-tile`, `.pay-method`, `.cart-line`, `.cart-qty-btn`
- API endpoints : `/api/pos/seed-defaults`, `/api/pos/transaction`, `/api/search`, `/api/print/pos/:id` inchangés
- Data attributes : `data-cat`, `data-method`
- Formatage français des montants (virgule décimale + espace milliers)
Prochaine étape
[1.17.5] — 2026-04-19
Task 6/8 — Migration `/configuration` (10 sections)
Modifié
- `app/views/configuration/index.php` réécrit (632 → ~600 lignes) :
- Layout `.ui-settings` : nav verticale `.ui-settings-nav` (sticky, 220px) + content `.ui-settings-content`
- 10 sections regroupées en 3 groupes nav : Organisation (Général, Tarification, Taxes, Renouvellements) · Intégrations (Stripe, IA, Widget) · Système (Général système, Réseau, Compte)
- Chaque panel : `.ui-section` avec header + form-grid, inputs `.ui-input`, suffix `%/CAD`, toggles `.ui-toggle` pour booléens
- Tarif styles / AI providers : cartes radio stylées inline avec border accent sur active
- Stripe : callout status avec état connecté/non connecté, details expandable pour config manuelle
- Widget : blocks code `<pre>` avec dark theme + boutons copy + preview bouton réservation
- Account/danger zone : callout info + 2 danger zones (résilier / suppression données)
- Save alert `.ui-callout-success` + footer `.ui-section-footer` avec bouton primary
- Color picker avec swatch preview live
Préservé
- Fonction `switchTab(tabId, btn)` : même signature, targete `.ui-settings-link` maintenant. URL hash `#org`, `#tarifs`, etc.
- Tax calculation: `updateTaxFields()`, `calcTaxTotal()`, `taxRates` table intactes
- AI provider switch: `switchAiProvider()` recalcule borders des cartes
- Tarif style highlight: `highlightTarifStyle()` pour radio selection visuelle
- Stripe Connect: `connectStripe()` POST `/api/stripe/connect/onboard`
- Copy to clipboard: `copyToClipboard(elementId)` avec feedback "Copié" temporaire
- Account actions: `confirmCancelAccount()`, `confirmDeleteData()` avec prompts/confirms
- Save: `saveConfig()` collecte FormData (arrays `[]` préservés), POST `/api/configuration`
- All IDs preserved: `panel-*`, form input names, `taxes_regime`, `taxTotal`, `colorPreview`, `aip_card_*`, `aikey_*`, `saveAlert`, `configForm`, `networkSharing`, `stripe_secret_key`, `widgetDirectLink`, `embedButton/Iframe/Script`
Prochaine étape
[1.17.4] — 2026-04-19
Task 5/8 — Migration `/dashboard` admin
Modifié
- `app/views/dashboard/index.php` réécrit :
- Header `.ui-page-header` avec subtitle calculé
- Legacy sync banner → `.ui-callout` jaune (toujours là, à retirer post-migration legacy)
- 4 KPIs en `.ui-kpi` avec variantes warning/danger selon seuils (assurance < 70%/90%, frais en souffrance)
- Grille 2-colonnes `.ui-grid-2` pour Occupation par port + Répartition contrats
- Répartition contrats : `.ui-bar-row` + `.ui-bar-track`/`.ui-bar-fill` + `.ui-badge-info` pour usage breakdown
- Mouvement par port : `.ui-chip-group` (Principal/Secondaire toggle) + tableau stylé
- Alertes : `.ui-activity` + `.ui-activity-item` + `.ui-activity-icon-{warning,danger,info}`, chaque item cliquable vers la section concernée
- Activité récente (recent ententes) : `.ui-table` + `.ui-row` + `.ui-badge` statut
Préservé intact
- Tout le JS Turf.js (~200 lignes) pour le calcul d'occupation par port (polygones bateaux + collisions)
- JS `switchPortMovement()`, `toggleTransferDetail()`, `runLegacySync()`
- IDs DOM : `portOccBars`, `kpiOccRate`, `kpiOccBar`, `kpiOccSub`, `syncStatus`, `btnLegacySync`, `btnMovePrincipal/Secondary`, `portMoveSubtitle`, `transferDetail*`, `transferIcon*`
- API endpoints : `/api/mooring`, `/api/dashboard/transfers`, `/api/legacy-sync` inchangés
- Data flow controller → view identique (toutes les variables extract()-ées)
Prochaine étape
[1.17.3] — 2026-04-19
Task 4/8 — Migration `/ententes` complète
Modifié
- `app/controllers/EntentesController.php` : `index()` calcule 4 KPIs (actives saison / en attente / signées ce mois / valeur totale) scoppés au client si rôle client.
- `app/views/ententes/index.php` réécrit complet (513 → ~600 lignes) :
- Header `.ui-page-header` + subtitle calculé
- 4 KPIs (warning si en attente > 0)
- Filtres `.ui-filters` : search + saison + port + statut
- DataTable `.ui-table` avec columns reformatées : saison mono, bateau+propriétaire avec icône type, port+type entente, période début→fin, badge statut + indicateur signature
- Modal `.ui-modal-shell` XL : aside (260px) avec icône signature + titre + sub + badges + stats (bateau/port/période/montant admin-only) + actions (imprimer + delete). Content : 4 tabs (Détails / Services / Paiements admin-only / Signature)
- Tab Détails : 6 sections (navire+saison, emplacement cascade, type/usage, dates, notes, renouvellement toggle)
- Tab Services : 4 toggles `.ui-toggle` (rampe/quai/hivernement/électricité) + champ autres
- Tab Paiements (admin) : mode, date, montant, tarif appliqué
- Tab Signature : callout status (signée/non-signée) + preview image signature si présente
- Aside live-update sur changements (bateau, port, dates, montant, statut)
Préservé
- IDs DOM : `dataTable`, `editModal`, `editForm`, `editId`, `bateau_id`, `season`, `is_primary`, `type`, `usage_type`, `peche_type`, `status`, `signed_at`, `period_start/end`, `entente_port/quai`, `emplacement_id`, `payment_*`, `tarif_applied`, `service_autre`, `dock_services`, `svc_rampe/quai/hivernement/electricite`, `is_recurring_toggle`, `notes`, `deleteZone`, `printLink`, `signatureStatus`
- Functions JS : `openModal`, `saveRecord`, `deleteRecord`, `reloadTable`, `onEntentePortChange`, `onEntenteQuaiChange`, `setEntenteEmplacement`
- Data passée controller→view : `_allEmplacements` et `_bateauxMap` pour cascade + aside preview
- Services checkboxes collectées en JSON dans hidden `dock_services` au submit
- Role-based : tab Paiements + select Statut cachés pour client
- Auto-open via `?open=ID`, returnClient pattern préservé
Prochaine étape
[1.17.2] — 2026-04-19
Task 3/8 — Migration `/bateaux` complète
Modifié
- `app/controllers/BateauxController.php` :
- `index()` calcule 4 KPIs (total / actifs / sans entente / assurance < 30j) scoppés au client si rôle client
- `index()` charge aussi la liste des ports pour futurs filtres
- `datatable()` accepte nouveaux filtres `type` (peche_commerciale/plaisance/autre) + `insurance` (valid/expiring/expired/missing)
- `app/views/bateaux/index.php` réécrit complet (643 → ~600 lignes) :
- Header `.ui-page-header` avec titre + subtitle calculé + bouton primary
- Row 4 KPIs (warning/danger variants si sans-entente ou assurance expirante présents)
- Filtres `.ui-filters` : search + selects type/assurance hookés à DataTable via POST data
- DataTable `.ui-table` avec columns reformatées : icône type (peche/plaisance/commercial), name+registration+length, propriétaire, email/phone tronqués, badge entente + période, badge assurance avec jours restants, chevron hover
- Modal `.modal-xl + .ui-modal-shell` : aside (260px) avec icône + name + sub + badges assurance + stats (propriétaire/longueur/type) + actions (delete). Content : 5 tabs (Identité / Dimensions / Assurance / Documents / Contrats) avec `.ui-section` + `.ui-form-grid-2/3` + `.ui-input-suffix` (m).
- Aside live update sur changements de champs (name, type, length, registration)
- Toggle langue FR/EN stylé `.ui-toggle`
- Upload PDF, TC lookup dialog, contracts tab : markup restylé, JS intact
- `public/css/stitch.css` : rules `.modal-content.ui-modal-shell` pour intégration Bootstrap modal avec structure flex aside+content. Responsive mobile : aside devient header horizontal.
Préservé
- IDs DOM : `dataTable`, `editModal`, `editForm`, `editId`, `client_id`, `name`, `registration_number`, `official_number`, `type`, `length`, `width`, `draft`, `insurance_*`, `emergency_*`, `photo_url`, `langToggle`, `language_pref`, `documentPDF`, `selectedFilesZone/List`, `existingDocsZone/List`, `contractsList`, `deleteZone`, `tcImportDialog`, `tcDialogBody/Data`, `lengthFt/widthFt/draftFt`, `tcLink`, `tcImportBtn`
- Functions JS : `openModal`, `saveRecord`, `deleteRecord`, `loadDocuments`, `loadContracts`, `fillClientContact`, `convertToFeet`, `updateTcLink`, `importFromTC`, `showTcImportDialog`, `applyTcImport`, `formatSize`, `deleteDocument`, `openEntenteDetail`
- API endpoints : `/api/dt/bateaux`, `/api/bateaux/:id`, `/api/bateaux`, `/api/documents*`, `/api/tc-lookup`, `/api/ententes/by-bateau` inchangés
- CSRF via `/js/csrf.js`, role-based via `$isClient`, search + filters hookés correctement
Prochaine étape
[1.17.1] — 2026-04-19
Task 2/8 — Migration sidebar + topbar shell
Modifié
- `app/views/partials/sidebar.php` — réécrit avec classes `.ui-sidebar-*`. Groupes collapsibles préservés (Administration / Clients / Conformité / Finances / Infrastructure / Opérations). Bilinguisme `$_lang` et visibilité role-based `$isClient` intacts. Changelog link en footer.
- `app/views/layouts/dashboard.php` — shell body restructuré : `<div class="ui-shell">` avec `<aside class="ui-sidebar">` + `<main class="ui-main">`. Topbar restylée en `.ui-topbar` (search + user avatar + logout). Mobile hamburger → `.ui-mob-hamburger`. JS closures ajustées (`.sidebar` → `.ui-sidebar`, `.search-bar` → `.ui-topbar-search`).
- `public/css/stitch.css` — ajouts : `.ui-sidebar-group/.ui-sidebar-group-title/.ui-sidebar-group-items` (collapsibles), `.ui-sidebar-brand-img`, `.ui-sidebar-divider`, `.ui-sidebar-footer`, `.ui-topbar/.ui-topbar-search/.ui-topbar-user*`, `.ui-mob-hamburger/.ui-mob-overlay`, `body.ui-has-banner` pour offset du banner démo. Media query mobile : sidebar devient off-canvas slide-in.
À noter
- CSS inline legacy dans `dashboard.php` (469 lignes de styles `.sidebar`, `.main-content`, `.topbar`) n'est plus référencé mais laissé en place (cleanup task 8). Les `.s-page-header`, `.s-card`, `.s-table` etc. restent actifs puisque les contenus de vue ne sont pas encore migrés.
- JS de toggle des groupes collapsibles (`classList.toggle('open')`) continue de fonctionner : même mécanisme, juste nouvelles classes ciblées.
- Global search comportement inchangé : ID `#globalSearch` + handler JS préservés.
Prochaine étape
[1.17.0] — 2026-04-19
Task 1/8 — Landing stitch.css en prod (invisible)
Ajouté
- `public/css/stitch.css` (2 200 lignes) — design system prod dérivé de `public/design-preview/shared.css`. Tokens Stitch (`--s-*`), composants UI (`.ui-*`), états (hover/focus/active/disabled), responsive breakpoints. Nettoyé des classes hub `.ui-preview-*` (banner, hero, cards, continuity, principles) qui ne servent qu'aux mockups.
- `<link>` dans `layouts/dashboard.php` (head, après Bootstrap + DataTables) — `stitch.css?v=appVersion()` pour invalidation cache correcte.
À noter
Plan complet
[1.16.1] — 2026-04-19
Ajouté — 5 mockups additionnels pour valider la continuité entre pages
- `dashboard.html` — tableau de bord admin : 6 KPIs + graphique revenus mensuels (SVG area chart 2026 vs 2025) + occupation par port (bars) + donut types d'utilisation + fil d'activité récente.
- `ententes.html` — liste ententes : même structure CRUD que `/bateaux` (sidebar + KPIs + filtres + DataTable) mais colonnes adaptées (numéro, bateau/propriétaire, période, tarif, statut). Test d'uniformité sur un 2e CRUD.
- `ententes-modal.html` — modal entente complexe : 6 tabs (Détails/Services/Paiements/Signature/Documents/Historique), breakdown tarif (base + surcharge + rabais + taxes), échéancier installments (paid/upcoming).
- `configuration.html` — settings multi-tabs : layout vertical nav (14 sections: Général/Ports/Équipe/Barèmes/Types/Services/Taxes/Modèles/Stripe/Rappels/Sécurité/API/Exports) + contenu avec formulaires, upload, toggles switches, color picker.
- `pos.html` — point de vente : catalogue produits (grid + chips catégories) + panier avec qty stepper + totaux + 3 méthodes de paiement + bouton d'encaissement. Test radical : est-ce que le système tient hors du pattern CRUD.
- Index enrichi : section "Continuité entre pages" (ce qui reste constant vs ce qui varie par contexte) pour documenter le système design.
shared.css — extensions
À noter
[1.16.0] — 2026-04-19
Ajouté
- Design preview — exemplar `/bateaux` : mockups HTML statiques sous `public/design-preview/` matérialisant les 7 principes design (typographie rigoureuse, hiérarchie par épaisseur, grille 8pt, ombres ultra-subtiles, états complets, densité pro, micro-animations 150-200ms). Registre visuel cible : Stripe Dashboard + Linear.
- 5 vues: `index.html` (hub de navigation), `bateaux.html` (liste normale avec sidebar + KPIs + filtres + DataTable), `bateaux-empty.html` (empty state SVG), `bateaux-loading.html` (skeletons shimmer), `bateaux-modal.html` (modal XL avec aside preview + tabs + form sections).
- `shared.css` (~800 lignes) : tokens Stitch + composants UI réutilisables (`.ui-kpi`, `.ui-card`, `.ui-table`, `.ui-badge`, `.ui-btn`, `.ui-skeleton`, `.ui-modal`, `.ui-field`, etc.).
- URL d'accès : `https://moorly.ca/design-preview/`.
À noter
Spec associée
[1.15.2] — 2026-04-19
Ajouté
- 34 tests Phase B supplémentaires — couverture complète des services testables
- `TransactionsServiceTest` (6 tests) : voidPaiement avec recalcul status facture (sent/partial), exceptions introuvable/déjà annulée, listTransactions UNION paiements+POS avec filtres
- `PosServiceTest` (12 tests) : catalog, taxes config, CRUD categories (avec cascade delete items), CRUD items, processTransaction avec taxes + tax_exempt + numérotation POS-YYYYMMDD-NNN, voidTransaction avec restore stock, dailySummary agrégations, seedDefaults 8 catégories
- `ComplianceDocsServiceTest` (9 tests) : sendAlert création/upsert, sendBulkAlerts (expired/expiring/missing/skip existing), resolveAlert, getDashboard structure + counts, validation type invalide
- `DashboardServiceTest` (7 tests) : getAdminData structure (26 keys), counts bateaux/pending ententes/contract types breakdown, getClientData scoping par client, getPortTransferDetail
Correction mineure
- `PosService::processTransaction` : `$input['client_id'] ?:` → `$input['client_id'] ?? null` (fix PHP 8 warning sur clé absente)
Couverture finale
- ✅ IncidentsService, PaymentInstallmentsService, FacturationService, EntentesService, BookingService
- ✅ TransactionsService, PosService, ComplianceDocsService, DashboardService
- ⏭️ StripeService non couvert (méthodes publiques appellent l'API Stripe — nécessiterait un mock HTTP)
[1.15.1] — 2026-04-19
Ajouté
- 28 tests Phase B supplémentaires (67 tests total, 171 assertions, 1.15s runtime)
- `FacturationServiceTest` (7 tests) : create avec defaults, update, delete, batchPay cascade + skip des factures déjà payées + empty ids, filtrage par status
- `EntentesServiceTest` (11 tests) : dérivation client_id depuis bateau, clientOverride prioritaire, forcePending, validation bateau requis + propriétaire, listByBateau scoping, delete, signature électronique (format invalide → 400, entente introuvable → 404, client non-owner → 403, success flow avec signature_data/ip/user_agent/signed_at/status='active')
- `BookingServiceTest` (10 tests) : submitRequest success + client_id attach + validation champs requis, listRequests filtrage, updateRequestStatus validation, updateWaitlistStatus validation + success, listWaitlist filtrage par status, checkAvailability validation
Couverture service layer
- ✅ `IncidentsService` — CRUD + statut + listForClient
- ✅ `PaymentInstallmentsService` — listByEntente, createForEntente cascade, markPaid cascade
- ✅ `FacturationService` — CRUD + batchPay + list
- ✅ `EntentesService` — CRUD + sign (3 exceptions typées)
- ✅ `BookingService` — request + waitlist workflows
- `ComplianceDocsService` (utilise ANY_VALUE() — requires MySQL 8+ tests fine)
- `DashboardService` (requiert fixtures lourdes pour les KPIs agrégés)
- `PosService`, `StripeService`, `TransactionsService`
[1.15.0] — 2026-04-19
Ajouté
- Suite de tests automatisés Phase B (intégration DB) — 13 tests avec vraie DB MySQL
- Infrastructure : DB `moorly_test` (49 tables : schéma global + tenant combinés), chargée depuis `tests/schema.sql` (dump de `moorly_global` + `moorly_adminportuairedesiles`)
- Pattern transaction-rollback : `DatabaseTestCase` ouvre une transaction en `setUp`, rollback en `tearDown` → DB toujours propre entre tests (~40-50ms par test, 1s total)
- Helpers fixtures : `createClient()`, `createBateau()`, `createEntente()` dans la base class
- `PaymentInstallmentsServiceTest` (5 tests) : listByEntente, createForEntente avec cascade factures + taxes, markPaid avec cascade facture/entente/status
- `IncidentsServiceTest` (8 tests) : CRUD complet, transitions de statut, auto-resolved_at, listForClient scoping, validation statut invalide
- Total : 39 tests PHPUnit (26 unit + 13 integration), 104 assertions, 1s runtime
- `phpunit.xml` : deux test suites (`unit`, `integration`), lancement séparé possible via `--testsuite <name>`
- CI GitHub Actions : `--testsuite unit` (MySQL pas dispo sur runner). Tests integration restent locaux.
[1.14.11] — 2026-04-19
Modifié
- Service layer extraction — 10e et dernier controller (`StripeController` → `StripeService`)
- Controller passe de 555 à 97 LOC (-83%) — le gros gain de la soirée 🎯
- Service : 498 LOC, 6 méthodes publiques + handlers webhook privés
- Public: `checkoutInvoice`, `checkoutPlan`, `cancelSubscription`, `connectOnboard`, `handleWebhook`
- Privés (handlers): `handleCheckoutCompleted`, `markInvoicePaid`, `upgradeLicense`, `handleSubscriptionPaid`, `handleSubscriptionCancelled`
- Helpers: `calculateCommission`, `resolvePriceId`
- Constructor flexible : `forTenant()` pour les flows tenant, `forPlatform()` pour le webhook (pas de tenant context au départ)
- Exceptions typées systématiques : `InvalidArgumentException` (400), `RuntimeException` (500), `DomainException` (400 signature invalide)
- Aucun changement fonctionnel
Bilan service layer (session du 2026-04-19)
[1.14.10] — 2026-04-19
Modifié
- Service layer extraction — 9e controller (`PosController` → `PosService`)
- Controller passe de 457 à 243 LOC (-47%) — le print receipt HTML reste dans le controller (rendu de vue, 50 lignes)
- Service : 359 LOC, 18 méthodes couvrant:
- Catalog (`getActiveCatalog`, `getTaxConfig`, `getTodayTransactionCount`, `countCategories`)
- Categories CRUD (5 méthodes)
- Items CRUD (5 méthodes)
- Transactions (`processTransaction`, `voidTransaction`, `listTransactions`, `findTransaction`)
- Reports (`dailySummary`)
- Seed (`seedDefaults`)
- `processTransaction` : calcul subtotal + taxes + génération numéro POS-YYYYMMDD-NNN, insert + update stock
- `voidTransaction` : restore stock + marque voided
- `seedDefaults` lève `\RuntimeException` si catalogue non vide
- Aucun changement fonctionnel
[1.14.9] — 2026-04-19
Modifié
- Service layer extraction — 8e controller (`DashboardController` → `DashboardService`)
- Controller passe de 454 à 99 LOC (-78%) — le plus gros gain jusqu'ici
- Service : 458 LOC, 4 méthodes publiques + 4 helpers privés
- `getAdminData(year)` : toutes les données du dashboard admin (KPIs, distribution contrats, mouvements ports, transferts, usage, alertes, activité récente) en un seul struct
- `getClientData(clientId)` : données du dashboard client (client, bateaux, ententes, factures, pending renewals, incidents)
- `getPortTransferDetail(from, to, year, type)` : liste détaillée des bateaux transférés entre 2 ports
- `legacySync()` : sync temporaire depuis adminportuairedesiles.com (à retirer après migration)
- Helpers privés: `buildPortLastYearMap`, `countContractsByType`, `fetchPortMovement`, `fetchPortTransfers`
- Aucun changement fonctionnel
[1.14.8] — 2026-04-19
Modifié
- Service layer extraction — 7e controller (`BookingController` → `BookingService`)
- Controller passe de 327 à 134 LOC (-59%)
- Service : 326 LOC, 10 méthodes publiques couvrant les 3 domaines du booking :
- Public: `estimate`, `submitRequest`, `getActiveServices`
- Admin booking: `listRequests`, `findRequest`, `updateRequestStatus`
- Admin waitlist: `listWaitlist`, `findWaitlistEntry`, `updateWaitlistStatus`
- Availability: `checkAvailability` (berth overlap detection + fit)
- Validation centralisée : `submitRequest` et `updateRequestStatus`/`updateWaitlistStatus` lèvent `InvalidArgumentException`
- Helpers privés : `findDailyRate`, `computeTaxes`
- Aucun changement fonctionnel
[1.14.7] — 2026-04-19
Modifié
- Service layer extraction — 6e controller (`EntentesController` → `EntentesService`)
- Controller passe de 322 à 178 LOC (-45%)
- Service : 358 LOC, 7 méthodes publiques + 1 static + 2 helpers privés
- `list`, `listByBateau`, `find`, `create`, `update`, `delete`, `sign`
- `buildServicesSnapshot` (static, catalogue services pour stabilité historique des prix)
- `findBoatOwner`, `normalizeDockServices` (privés)
- Dérivation automatique du `client_id` depuis le bateau (logique v1.13.3 préservée)
- `create/update` lèvent `InvalidArgumentException` si bateau absent/sans propriétaire
- `sign` lève 3 exceptions typées : `InvalidArgumentException` (400), `RuntimeException` (404), `DomainException` (403)
- `EntentesController::buildServicesSnapshot` reste un delegate statique pour backward compat (RenewalsController l'utilise)
- Aucun changement fonctionnel — mêmes routes
[1.14.6] — 2026-04-19
Modifié
- Service layer extraction — 5e controller (`FacturationController` → `FacturationService`)
- Controller passe de 221 à 129 LOC (-42%)
- Service : 212 LOC, 7 méthodes (`list`, `find`, `create`, `update`, `delete`, `batchPay`, `findForPrint`)
- `findForPrint` gère la requête complexe à 8 jointures + résolution du catalogue de services par port/année
- Aucun changement fonctionnel — mêmes routes, même SQL
[1.14.5] — 2026-04-19
Modifié
- Service layer extraction — 4e controller (`TransactionsController` → `TransactionsService`)
- Controller passe de 152 à 59 LOC (-61%)
- Service : 201 LOC, 2 méthodes publiques (`voidPaiement`, `listTransactions`) + 2 helpers privés de construction des subqueries paiements / POS
- `voidPaiement` lève `\InvalidArgumentException` si paiement introuvable/déjà voided (controller map 404/400 selon le message)
- `listTransactions(filters, start, length, orderDir)` retourne `['total', 'filtered', 'rows']` — pagination DataTables supportée côté controller
- Aucun changement fonctionnel — mêmes routes, même SQL
[1.14.4] — 2026-04-19
Modifié
- Service layer extraction — 3e controller (`PaymentInstallmentsController` → `PaymentInstallmentsService`)
- Controller passe de 131 à 40 LOC (-69%)
- Service : 150 LOC, 4 méthodes publiques (`listByEntente`, `listAll`, `markPaid`, `createForEntente`)
- `createForEntente` est maintenant sur le service (avant : static sur le controller, appelé par `ClientRenewalController`)
- `ClientRenewalController` mis à jour pour appeler le service au lieu du controller
- Aucun changement fonctionnel — mêmes routes, même comportement financier
[1.14.3] — 2026-04-19
Modifié
- Service layer extraction — 2e controller (`ComplianceDocsController` → `ComplianceDocsService`)
- Controller passe de 193 à 72 LOC (-63%) — ne contient plus que plumbing HTTP
- Business logic (dashboard 4-lists, sendAlert upsert, sendBulkAlerts, resolveAlert) déplacée dans `app/services/ComplianceDocsService.php`
- `sendBulkAlerts` optimisé au passage : ancien N+1 (1 SELECT existant par bateau) remplacé par 1 SELECT avec `WHERE bateau_id IN (...)` + boucle PHP
- Aucun changement fonctionnel — mêmes routes, même comportement
[1.14.2] — 2026-04-19
Modifié
- Service layer extraction — premier controller (`IncidentsController` → `IncidentsService`)
- Nouveau dossier `app/services/` pour la business logic
- `IncidentsService` utilise le pattern factory + DI : `IncidentsService::forTenant()` pour usage normal, `new IncidentsService($db, $slug)` pour tests futurs
- Controller passe de 262 à 186 LOC (-29%) — ne contient plus que plumbing HTTP
- Business logic (create, update, updateStatus, delete, findOne, listForClient, upload photo) déplacée dans le service, testable isolément en Phase B future
- Aucun changement fonctionnel — même comportement, mêmes routes
[1.14.1] — 2026-04-19
Ajouté
- CI/CD GitHub Actions — workflow `.github/workflows/tests.yml`
- Lance `composer test` automatiquement sur chaque push vers `main` et sur les PRs
- PHP 8.4 (match prod), cache composer pour reruns rapides (~15s après 1er run)
- Suite PHPUnit Phase A (26 tests) tourne à chaque commit — filet de sécurité automatique
[1.14.0] — 2026-04-18
Ajouté
- Suite de tests automatisés PHPUnit — Phase A (logique pure, 26 tests)
- Infrastructure : `composer.json` + dev dep `phpunit/phpunit ^12.0`, `phpunit.xml` config, `tests/bootstrap.php`
- `HelpersTest` (8 tests) : `esc()`, `raw()`, `js()`, `uuid()`
- `TarifCalculatorTest` (8 tests) : `generateInstallments()` — single, monthly_N (clamp 2-12), deposit_balance, passage d'année
- `CsrfTest` (10 tests) : token generation/stability/regenerate, validate, isExempt exact/prefix, enabled
- Lance avec `composer test` — runtime 10ms
- Aucune dépendance DB (Phase B future : `TarifCalculator::calculate()`, `TarifResolver`, `Auth` — si utilité démontrée)
[1.13.3] — 2026-04-18
Performance
- Suppression de 2 N+1 queries dans les controllers les plus visibles
- `DashboardController::index` : boucle de requêtes par port active (N requêtes) remplacée par une seule requête `GROUP BY q.port_id`. Gain: ~20ms par load dashboard avec 8 ports (tenant adminportuairedesiles)
- `FacturationController::batchPay` : boucle `SELECT/INSERT/UPDATE` par facture (3×N requêtes) remplacée par 3 requêtes batch (SELECT ... WHERE id IN, INSERT multi-row, UPDATE ... WHERE id IN). Gain: 150 requêtes → 3 pour un batch de 50 factures
[1.13.2] — 2026-04-18
Sécurité
- Header Content-Security-Policy (CSP) envoyé sur chaque réponse
- Allowlist des 6 CDNs légitimes (jsdelivr, jQuery, DataTables, FontAwesome, Google Fonts)
- Bloque le chargement de scripts/styles/fonts externes non autorisés
- `frame-ancestors 'self'` : anti-clickjacking (seul moorly.ca peut embarquer)
- `form-action 'self'` : anti-hijack des formulaires
- `base-uri 'self'` : anti-injection de `<base>` tag
- `object-src 'none'` : bloque plugins Flash/PDF inline obsolètes
- `upgrade-insecure-requests` : auto-upgrade HTTP → HTTPS
- Kill switch d'urgence : `CSP_ENABLED=false` dans `.env`
- Permissive sur `script-src` et `style-src` (`'unsafe-inline'`, `'unsafe-eval'`) à cause des styles et scripts inline existants — le XSS inline reste couvert par `esc()` de l'audit v1.13.1
- Défense complémentaire contre les attaques XSS (couche 2 au-dessus de l'échappement serveur)
[1.13.1] — 2026-04-18
Ajouté
- Helpers d'output sécurisés dans `app/lib/helpers.php`
- `raw($v)` : marqueur no-op pour HTML intentionnel (contenu CMS, pages d'aide, documents légaux)
- `js($v)` : output sûr pour interpolation dans `<script>` avec flags `JSON_HEX_*` anti-injection
Sécurité
- Audit XSS systématique sur toutes les vues `app/views/` (~23 500 LOC)
- `esc()` ajouté sur ~900 interpolations `<?= ?>` non sécurisées à travers 80 fichiers
- `raw()` appliqué aux contenus HTML intentionnels (CMS `$page['content_*']`, pages d'aide `$contentHtml`, documents légaux, termes d'ententes imprimés)
- `js()` appliqué aux interpolations PHP dans blocs `<script>` et event handlers `onclick` (protection contre `</script>` injection + apostrophes dans noms)
- Découpage en 10 groupes pour commits granulaires :
- Mitigation des attaques Cross-Site Scripting (XSS) persistantes et reflétées
- Correctifs suite à review du diff : remplacement de `esc(addslashes(...))` par `js()` dans les `onclick` (fix de bugs avec noms contenant des apostrophes — ex. « L'Espoir »)
[1.13.0] — 2026-04-17
Ajouté
- Protection CSRF sur tous les formulaires et appels AJAX
- Synchronizer Token Pattern avec token de session PHP (32 bytes)
- Helper `<?= csrf_field() ?>` pour les formulaires HTML
- Setup jQuery global automatique via `/js/csrf.js`
- Page d'erreur dédiée bilingue pour les sessions expirées
- Kill switch d'urgence : `CSRF_ENABLED=false` dans `.env`
- Exemptions : `/api/stripe/webhook` (signature Stripe) et `/api/dt/*` (lecture DataTables)
Sécurité
- Mitigation des attaques Cross-Site Request Forgery
- Régénération automatique du token CSRF au login/logout (anti-session-fixation)
- Status HTTP 422 utilisé pour rejet AJAX (Apache transforme 419 non-RFC en 500)
[1.12.0] — 2026-04-09
Ajouté
- Analytiques tenant — tableau de bord de visites pour chaque port/marina
- Accessible via Administration > Analytiques
- Visites totales, visiteurs uniques, pages par visite, taux de rebond
- Graphique temporel et tableaux détaillés (pages, sources, navigateurs, appareils)
- Filtres par période et source (site public / tableau de bord)
- Changement de mot de passe pour les clients depuis leur espace (sidebar)
- Port d'attache affiché dans l'espace client (coordonnées et dashboard)
[1.11.0] — 2026-04-08
Ajouté
- Analytiques plateforme — tableau de bord de suivi des visites pour l'administrateur Moorly
- Visites par tenant, visiteurs uniques, pages populaires, sources de trafic
- Graphique temporel avec granularité automatique
- Filtres par période, tenant et source
[1.10.0] — 2026-04-07
Ajouté
- Résiliation de compte et suppression de données (Loi 25)
- Annulation d'abonnement avec période de grâce de 30 jours
- Suppression complète des données avec confirmation
- Export de données avant suppression
- Synchronisation legacy — import des données depuis adminportuairedesiles.com
- Notifications par courriel
- Consentement réseau activé par défaut pour les nouveaux utilisateurs
Corrigé
- Limites de taille pour les fichiers téléversés (10 Mo PDF, 5 Mo images)
- Espace client entièrement bilingue (sidebar, dashboard, renouvellement, aide)
- Recherche globale bilingue
- KPI du tableau de bord : ententes actives calculées par statut
- Modals uniformisés visuellement
Modifié
- Page marketing : navigation simplifiée, bouton de connexion plus visible, drapeaux pour le changement de langue
[1.9.0] — 2026-04-07
Ajouté
- Signalements d'incidents
- Les clients peuvent rapporter des problèmes (bris, sécurité, environnement) avec photo
- Interface admin avec filtres, priorités et suivi du cycle de vie
- 6 catégories, 4 niveaux de priorité
- Nouvelles pages d'aide sur les signalements
[1.8.0] — 2026-04-07
Ajouté
- Suivi de conformité documentaire
- Dashboard avec assurances expirées, expirant bientôt et manquantes
- Envoi d'alertes individuelles et en masse aux clients
- Notification dans l'espace client pour les documents à mettre à jour
- Détection automatique par cron quotidien
- Centre d'aide enrichi — 14 nouvelles pages (FR/EN) : signature électronique, renouvellements, versements, réservation, conformité
Modifié
- Page marketing mise à jour avec les nouvelles fonctionnalités
[1.5.0] — 2026-04-07
Ajouté
- Renouvellement récurrent des ententes
- Renouvellement automatique avec calcul du tarif selon la grille de l'année suivante
- Flow client self-service : réviser → mettre à jour → choisir paiement → confirmer
- Notification dans le dashboard client pour les renouvellements en attente
- Versements et plans de paiement
- 6 options : paiement unique, 2 à 6 versements mensuels, dépôt + solde
- Chaque versement génère une facture avec taxes
- 5 méthodes de paiement configurables : carte de crédit, Interac, comptant, chèque, compte client
- Cron quotidien : génération automatique des renouvellements et suivi des versements en retard
- Réservation en ligne depuis le site public du port
- 2 modes : court séjour et saisonnier
- Calcul automatique du prix en temps réel
- Widget multi-étapes intégré à la page d'accueil
- Page admin pour approuver/refuser les demandes
- Signature électronique des ententes
- Pad de signature tactile (mobile et souris)
- Intégrée au flow de renouvellement et visible sur l'entente imprimée
Modifié
- Page Configuration restructurée en 8 onglets
[1.4.0] — 2026-04-07
Ajouté
- Point de vente (POS) pour les plans Pro, Entreprise et AP
- Caisse avec grille d'articles, panier et calcul des taxes automatique
- ~25 articles par défaut répartis en 8 catégories
- 4 méthodes de paiement, impression de reçu, rapports quotidiens
- Historique des transactions avec annulation
- Suivi d'inventaire avec alertes de stock bas
[1.3.0] — 2026-04-07
Ajouté
- Inscription client depuis moorly.ca/inscription avec sélection du port/marina
[1.2.0] — 2026-04-06
Ajouté
- Application mobile pour les gardiens de quai et opérateurs (plans Pro+)
- Données de démonstration réalistes (25 clients, 25 bateaux, 25 ententes)
Modifié
- Pricing restructuré en cascade (Gratuit → Pro → Entreprise)
- Section « Pour les plaisanciers et pêcheurs » sur la page marketing
Corrigé
- Amarrage épaule : les bateaux ne chevauchent plus les quais dans les coins et virages
[1.1.1] — 2026-04-06
Modifié
- Mode démo interactif : les utilisateurs peuvent tester toutes les fonctionnalités non sensibles
[1.1.0] — 2026-04-06
Ajouté
- Numéro de version visible sur toutes les pages
- Page changelog bilingue accessible depuis tous les contextes
[1.0.0] — 2026-04-06
Fonctionnalités principales
Infrastructure
- Gestion multi-ports avec carte interactive (Leaflet)
- Quais avec tracé de lignes et sélection du côté d'eau
- Emplacements désignés avec rotation et redimensionnement
- Placement automatique des bateaux avec détection de collisions
- Gestion des administrations portuaires avec membres du CA
Clients et bateaux
- Base de données clients avec coordonnées et urgence
- Création automatique de compte avec mot de passe temporaire
- Support multi-tenant : un client peut être inscrit sur plusieurs ports
- Registre des bateaux avec immatriculation, dimensions et assurance
- Recherche réseau inter-tenant (avec consentement Loi 25)
Ententes et tarification
- Ententes d'amarrage (principal, avant-pêche, après-pêche, transient)
- 6 styles de tarification (pêche saisonnière, saisonnier, transient, annuel, commercial, hybride)
- Grille tarifaire par catégorie, période et seuil de largeur
- Services additionnels configurables
- Copie de grille tarifaire d'une année à l'autre
Facturation et paiements
- Génération de factures avec calcul automatique des taxes (TPS/TVQ/TVH)
- Configuration fiscale par province
- Impression de factures en PDF
- Paiements en ligne via Stripe
Site web public (CMS)
- Site bilingue par sous-domaine (tenant.moorly.ca)
- CMS intégré avec pages et documents
- Génération de contenu par IA (Claude, ChatGPT ou Gemini)
- Carte des quais, tarifs publics, pages de services et règlements
Dashboard
- Dashboard administrateur avec KPIs et occupation par port
- Dashboard client avec bateaux, ententes, factures et paiement en ligne
Conformité et opérations
- Module de conformité (inspections)
- Module d'infractions avec impression PDF
- Gestion de la machinerie
- Permis commerciaux
Plateforme et abonnements
- Gratuit : 1 port, 50 utilisateurs
- Pro (39$/mois ou 390$/an) : 5 ports, 200 utilisateurs, 0% commission
- Entreprise (890$ à vie) : illimité
- AP PPB : gratuit, illimité, pour Ports pour Petits Bateaux (Pêches et Océans Canada)
- Panneau super-admin avec liste des tenants et revenus
- Démo en ligne sans inscription (demo.moorly.ca)
Conformité légale
- Consentement cookies, politique de confidentialité et conditions d'utilisation bilingues
- Consentement réseau à deux niveaux (tenant + individu)
- Export et suppression de données (Loi 25 / PIPEDA)
Centre d'aide
- 26 pages bilingues (FR/EN)
- Accessible depuis le dashboard et publiquement
- Recherche et navigation par sections