Commit Graph

237 Commits

Author SHA1 Message Date
louispaulb
8d946daf8d Planification: rotation de garde par département (récurrence + rotation)
Dialogue « Garde » : règles par département (tech_group) = {shift de garde, jours, période (toutes
les X sem.), techs en rotation ordonnés}. Indépendantes entre départements (non synchronisées).
« Appliquer à la semaine » génère les gardes : pour chaque jour ciblé, le tech de garde = rotation
(index de semaine / période % liste) ; un tech absent est SAUTÉ au profit du suivant. Règles persistées
(localStorage roster-garde-rules-v1). Les gardes s'affichent en pointillé ambre (on_call), hors heures
travaillées/booking déjà en place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:27:24 -04:00
louispaulb
060ee578c3 Planification: modèle par défaut (★) appliqué en 1 clic
Marquer un modèle de semaine comme défaut (★ dans le menu Modèles, un seul à la fois) →
bouton ★ <nom> dans la barre pour l'appliquer en 1 clic (avec l'intelligence d'absences déjà en place).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:23:40 -04:00
louispaulb
cdd72856a4 Planification: application de modèle consciente des absences (permanent vs vacances)
applyTemplate respecte maintenant les absences : on n'assigne pas un tech absent ce jour-là.
- Absent toute la semaine (≈ congé permanent: maternité/blessure) → signalé « à remplacer » (warning).
- Absent quelques jours (≈ vacances) → ces jours sautés, le reste du patron tient.
Le toast résume: N assignations, absences partielles ignorées, et qui est à remplacer (avec le type).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:16:05 -04:00
louispaulb
9261692c7f Planification: menu de case court (2 raccourcis + slider en haut) — Appliquer toujours atteignable
- Menu réduit: 2 raccourcis (Normal 8–17, Soir 16–20) + slider d'ajustement remontés EN HAUT
  (près du clic) ; Appliquer dans la rangée du slider → reste visible même si la case est en bas
  (max-height 85vh + flip auto Quasar). Retrait de la longue liste des modèles.
- quickShift(min,max) + applyWindow() factorisés. Shifts en place + Copier/Coller/Vider en icônes
  compactes sous le slider. Nettoyage cellCode/cellColor/codeByShift/colorByShift/addFromMenu inutilisés.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:55:32 -04:00
louispaulb
ece0ccb6ff Planification: Suppr/Backspace pour vider les cases sélectionnées (ou la case active)
onKey: garde anti-champ (n'intercepte pas dans input/textarea/select/contenteditable) + empêche
le 'retour arrière' du navigateur (preventDefault). Suppr/⌫ vide la sélection (ou la case cliquée),
avec pushHistory (annulable). Hint de légende mis à jour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:46:04 -04:00
louispaulb
89a366d197 Planification: hachuré = ABSENT (congé/pause), garde = pointillé ambre (sur appel hors heures)
- Hachuré gris = ABSENT (Tech Availability approuvée + En pause). Nouvel endpoint /roster/absences
  (En pause global + congés approuvés par jour) → la cellule d'un tech absent est hachurée (tooltip = type).
  Remplace l'ancien 'P' pause.
- GARDE = nouveau visuel: bande à CONTOUR POINTILLÉ AMBRE + fond ambre léger (sur appel, hors heures
  d'ouverture) — distinct du travail planifié et de l'absent.
- Légende: dispo (matin→soir) · occupation · absent (hachuré) · garde (pointillé). Retrait 'P pause'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:43:47 -04:00
louispaulb
021417f29f Planification: contour 1px foncé autour de la bande de disponibilité (vs fond du timeline)
La bande pâle se confondait avec le fond gris du timeline → contour 1px bleu-violet foncé
(rgba 55,65,120,.5) box-sizing border-box pour ne pas déborder. Garde = contour brun.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:36:07 -04:00
louispaulb
5ad17c0d19 Planification: fix slider d'ajustement (menu) — sélection de texte + resize continu
- user-select:none sur le menu (rendu en portail → n'héritait pas du no-select de la grille)
  → le glissement ne sélectionne plus le texte.
- Retrait des bulles de label de la q-range (8h/18:30h) qui changeaient de largeur → le menu ne
  se redimensionne plus. La valeur reste affichée en direct sous le slider (8h–18:30h).
- Largeur du menu fixée (260px) + @mousedown.stop sur le slider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:29:57 -04:00
louispaulb
94c7566dd3 Planification: barre de temps pâle + barre de statut opaque vert→orange (occupation)
- Barre de TEMPS (dispo) = pâle (bleu très pâle matin → violet pâle soir) : repère discret du quand.
- Barre de STATUT (occupé) = OPAQUE vert (peu) → orange (plein) → rouge (surbooké), positionnée
  aux vraies heures des jobs → ressort nettement sur le fond pâle ; les trous pâles = offrables.
- Légende: dispo (matin→soir) pâle + occupation (vert→orange) + garde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:22:05 -04:00
louispaulb
d7867e2a62 Planification: cellule = barre de dispo seule sur une échelle de temps (règle horaire en-tête)
- En-tête de chaque jour: règle horaire (graduations alignées sur l'axe adaptatif, ex 8/12/16/20).
- Cellule réduite à LA barre de dispo (dégradé matin→soir, occupé assombri, garde hachuré) —
  plus de texte d'intervalle : on lit la position contre la règle, détails au survol.
- Barre un peu plus haute (11px). Retrait cellLabel/cell-int/cell-chips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:18:38 -04:00
louispaulb
738d315785 Planification: barre de dispo dégradée par l'heure (bleu matin → violet soir), fini les lettres
- Cellules: plus de chip-lettre (J/S/M/D). La bande de la timeline est colorée par l'HEURE :
  dégradé bleu pâle le matin → violet le soir (hsl 210→270). On lit 'quand' d'un coup d'œil.
- Occupé = assombrit la bande (clair = libre/offrable, foncé = pris) ; rouge si surbooké.
  (Remplace le feu vert/orange/rouge — plus sobre.) Garde reste hachurée.
- Légende: échantillon dégradé « matin → soir » + « garde » (hachuré) au lieu de la liste de lettres.
- Intervalle (8–16) gardé en texte. occColor retiré.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:12:27 -04:00
louispaulb
142ce45755 Planification: ne montrer que les presets nommés + coller multi-cases fiable + fix Cmd+C/V
- Nettoyage des modèles auto en double fait côté données (8h–16h→Jour, 7h–15h→Matinal,
  9h–17h→Décalé, 8h–17h→Jour, 8h–22h→Soir) — restent 5 presets propres.
- Barre d'assignation + légende = presetTemplates (modèles NOMMÉS seulement) → restent propres
  même si des modèles auto réapparaissent.
- Fix copier-coller : le clic ouvrait le menu ET vidait la sélection → Cmd+C voyait 0 cellule.
  Maintenant on mémorise activeCell (dernière case cliquée) ; Cmd+C/V ferment le menu et marchent
  sans multi-sélection. Coller = vers la sélection si multi, sinon la case active.
- Indicateur « N copié(s) » visible. Coller multi-cases via la barre (déjà) + Cmd+V sur sélection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:04:37 -04:00
louispaulb
7f6d314cc0 Planification: fix menu (régression cellHours) + copier/coller + slider d'ajustement dans le menu
BUG: le menu de case plantait sur les cases occupées (cellHours retiré mais encore appelé l.259)
→ c'est ce qui cassait le copier-coller au clic. Corrigé ({{ a.hours }}h).

- Copier/Coller déplacés dans le MENU de la case (clic simple, plus besoin de Cmd+clic) :
  « Copier cette case » / « Coller ». (Boutons barre + Ctrl/Cmd+C/V conservés.)
- « Ajuster l'horaire (glisser) » : q-range dans le menu → élargir/réduire le créneau de dispo,
  + toggle Garde. Applique = trouve/crée un modèle auto-nommé (8h–18h) et remplace la case.
  → la dispo élargie est aussitôt offerte au booking (modèle standard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:24:07 -04:00
louispaulb
c2f3e4d666 Planification: copier-coller de cases + créneaux custom (slider q-range) + auto-nommage
- Copier-coller pour bâtir l'horaire vite : sélectionne une case → Copier (ses shifts) →
  sélectionne des cibles → Coller (duplique). Boutons dans la barre + raccourcis Ctrl+C / Ctrl+V.
  Copier une case vide puis Coller = vider les cases.
- Créneaux CUSTOM : nouveau modèle créé via slider q-range (2 poignées, pas 0.5 h) → plus besoin
  de prévoir tous les types. Nom AUTO si vide (« 8h–17h » d'après les heures).
- Presets standards semés : 7h–15h, 9h–17h, 8h–17h (+ Jour 8h-16h existant) — triés par usage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:02:15 -04:00
louispaulb
dfefd7822f Planification: cellule sans icônes — juste intervalle + timeline
Retrait des icônes en cellule (☀/🌆/🌙/🛡️). Le libellé = uniquement l'intervalle début–fin
(ex 8–16) ; le timeline (bande pleine vs garde hachurée) porte le reste visuellement.
Tag garde dans l'infobulle: '(garde)' au lieu de l'emoji.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:55:02 -04:00
louispaulb
17d8442b98 Roster: la garde ne compte PAS comme heures travaillées (mise en dispo)
Garde (on_call) exclue partout des heures travaillées + du coût:
- hub statsByDay: heures = somme des shifts NON-garde ; nouveau compteur on_call/jour (techs en dispo).
- Ops: hoursOf (heures/tech + alerte heures supp) et costByDate/weekCost excluent la garde.
- nouvelle ligne de pied '🛡️ Garde' = nb de techs en disponibilité/jour (si applicable).
Cohérent avec l'occupation (déjà hors-garde) : la garde = réserve d'urgence, ni offerte ni facturée.
Vérifié 8 juin: 112 h travaillées (garde 6 h exclue), garde=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:52:01 -04:00
louispaulb
72845e2057 Planification: axe timeline adaptatif + intervalle texte + modèles triés par usage
#1 axe trop large (0-24) → axe ADAPTATIF calé sur l'amplitude réelle des shifts réguliers de la
semaine (garde n'élargit pas) → barres plus larges, position lisible. Intervalle début–fin REMIS
en texte dans la cellule (☀ 8–16 🛡️) = référence sans survol. Infobulle = capacité offrable
(corrige aussi un bug: shiftH→bookableH).
#2 modèles d'assignation TRIÉS PAR USAGE (les plus utilisés en premier) + infobulle nom.
(Rappel: créer des modèles custom = éditeur « Types de shift ».)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:48:47 -04:00
louispaulb
1ab9f64b48 Roster: quart de Garde (on_call) = réserve d'urgence, jamais offert au booking
Modèle: champ on_call (Check) sur Shift Template. Un quart garde:
- N'est JAMAIS offert au booking client (techGaps retourne null) — vérifié: tech Jour+Garde
  n'offre que la fenêtre Jour, aucun créneau dans la plage de garde.
- Est EXCLU du dénominateur d'occupation (heures offrables), affiché à part.
- Timeline: bande HACHURÉE (vs neutre pour l'offrable) + 🛡️ dans le label + tag (garde) en infobulle.
- Éditeur de modèles: bascule '🛡️ Garde' pour créer/marquer un quart de garde.
hub: fetchTemplates expose on_call; create/update template le gèrent. Champ ajouté à
setup_dispatch_custom_fields.py (persistance). Démo: Garde 18h-minuit marquée on_call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:37:34 -04:00
louispaulb
049897e021 Planification: micro-timeline 24h, multi-shift, neutre/coloré, label h utilisées + icône période
Refonte selon retour: axe 24h (00→24). Chaque shift = bande NEUTRE positionnée (multi-shift OK:
Jour + Garde affichés en 2 bandes, gère le passage minuit). Jobs pris = traits COLORÉS (charge:
vert/orange/rouge). Label compact = icône période (☀/🌆/🌙) + heures utilisées/total (ex 4/8).
Intervalles exacts par shift dans l'infobulle. Tick à midi (50%).

Démo 8 juin: TECH-4738 = Jour 8-16 + Garde 18h-minuit (multi-shift), Matinal 7-15 / Décalé 9-17.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:27:32 -04:00
louispaulb
341c8e5a64 Planification: mini-timeline positionnée (fenêtre réelle du shift + blocs pris)
Avant: 'J8' ne distinguait pas 7-15 de 9-17 → mêmes créneaux apparents, dispo réelle différente.
Maintenant chaque cellule affiche: chip (lettre) + intervalle '7–15', et une mini-timeline sur un
axe de journée (06:00→21:00) où la fenêtre du shift est positionnée (donc 7-15 à gauche, 9-17 à
droite = visuellement distinctes) avec les blocs de jobs pris (couleur selon charge) → les TROUS
restants = créneaux offrables. Infobulle = intervalle + h occupées/h (%).

- hub occupancyByTechDay renvoie {h, blocks:[{s,e}]} (heures de début réelles des jobs).
- ops: cellWindow/axisPos/shiftStyle/blockStyle, rendu .tl/.tl-shift/.tl-blk + tick midi.
- démo 8 juin: modèles Matinal 7-15 + Décalé 9-17, techs alignés (7→13.8, 9→18.6 surbooké).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:21:04 -04:00
louispaulb
49795f858b Planification: taux d'occupation par cellule (jobs assignés / heures du shift)
Chaque cellule tech×jour avec un shift affiche, sous le chip (J8), une mini-barre + % colorés
(vert <70, orange 70-99, rouge >=100 surbooké) + infobulle = intervalle du shift + h occupées/h.
Occupation = Σ duration_h des Dispatch Jobs planifiés assignés ce jour ÷ Σ heures du shift.

- hub: occupancyByTechDay(start,days) + GET /roster/occupancy → map 'TECH|date': heures.
- ops api: getOccupancy ; PlanificationPage: occCells (computed), cellOcc/occColor/cellInterval,
  rendu barre + q-tooltip, chargé dans loadStats. Données démo semaine 8 juin (45/85/120%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:13:27 -04:00
louispaulb
88b2702489 Copilote: agit vraiment sur les absences (gerer_absence + IROPS rematch) + gig dispo
Bug: le copilote comprenait l'absence mais n'agissait pas → aucun impact. Causes:
1) prompt « par défaut analyse » → ne déclenchait pas l'action ;
2) marquer une absence n'excluait PAS des créneaux (techGaps ne testait que le statut
   global En pause, pas les Tech Availability approuvées par jour) ;
3) loadBookingData calculait unavail mais ne le retournait pas (oubli) → garde inerte.

Fixes:
- roster.js: loadBookingData inclut unavail (buildUnavailability = En pause + absence_from/until
  + Tech Availability approuvées) ; techGaps exclut le tech absent ce jour-là ; export bookingSlots/fetchTemplates.
- roster-assistant.js: nouvel outil gerer_absence = crée l'absence (par jour) + trouve les RDV
  impactés + RÉASSIGNE auto à un autre tech libre du même créneau (IROPS), renvoie les
  « à reporter ». Nouvel outil ajouter_disponibilite (tech à l'acte ouvre un créneau). Prompt
  orienté ACTION (signaler une absence = instruction d'exécution).
- Validé prod (lab): copilote crée l'absence ✓, booking exclut le tech absent (109→104) ✓,
  rematch DJ-…→Antoine même créneau ✓ ; données de test nettoyées.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:43:03 -04:00
louispaulb
43c67e3a18 Ops RDV+Copilote: vue agent (semaine/jour + hold), file À recontacter, réglages #56
RendezVousPage:
- Vue segmentée À planifier / À recontacter / Tous.
- Créneaux proposés groupés Semaine → Jour (se situer dans le temps, comme /book).
- Hold à la sélection: bookHold(date,start,10min) → bloque les autres; libéré à la confirmation
  ou au changement de job (onBeforeUnmount).
- File À recontacter (jobs À reporter) + actions: Lien client (copie URL self-serve),
  Aviser par SMS (notify-reschedule: désassigne + SMS lien /book).

CopilotePage: carte réglages des créneaux offerts (#56) — lead_hours, plage horaire,
horizon, max/jour, hold, jours offerts (chips) → savePolicy({booking}).

api/roster.js: bookHold, bookLink, jobsToReschedule, notifyReschedule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:28:14 -04:00
louispaulb
7f3ad56188 Booking #56: politique de créneaux offerts + holds temporaires
- getBookingPolicy() (sous-objet 'booking' du fichier policy): lead_hours, day_start/end,
  days_offered, horizon_days, max_per_day, hold_minutes. Appliquée dans bookingSlots →
  cohérent pour /book + vue agent + fit. ignorePolicy au moment de confirmer (slot encore libre).
- Holds en mémoire (Map TTL): /roster/book/hold {date,start,minutes|release} → fenêtre retirée
  des dispos des autres pendant le hold; libérée à la confirmation. Évite le double-booking
  pendant qu'un agent/client choisit.
- roster-assistant: DEFAULT_POLICY.booking + booking_fields/weekdays (descripteurs UI), fusion fine.
- Testé: plage 10-14h filtre bien; hold 10:00 dispo 12→11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:19:42 -04:00
louispaulb
69ad35b9bc Booking /book: grille semaine → jour → Matin/Après-midi (se situer dans le temps)
Avant: liste plate jj/mm peu lisible. Maintenant: bandeau de semaine ('Semaine du 8 – 14 juin'),
en-têtes de jour complets (lundi 8 juin), sections Matin/Après-midi. Sélection 1-3 préférences inchangée.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:04:08 -04:00
louispaulb
42c07d36f2 Fix reschedule: notify-reschedule désassigne (vide date/heure/tech, status open) → /book repropose des créneaux
Avant: le job gardait scheduled_date → page /book court-circuitait en 'déjà confirmé' sans options.
Maintenant un report = vrai retour au pool → le client choisit un nouveau créneau (vérifié: 50 créneaux proposés, SMS livré).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:56:20 -04:00
louispaulb
097e0566ec Reschedule: endpoint aviser-client (lien /book + SMS Twilio + statut 'À reporter') + file 'À reporter' + outil copilote
/roster/job/notify-reschedule (job,phone?) → token /book + SMS + booking_status='À reporter'.
/roster/jobs-to-reschedule → file superviseur. Copilote: outil notifier_client_report.
Testé OK (chemin sans téléphone = sûr). Brique P5 de #55, déclenchable depuis le flux de pause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:28:45 -04:00
louispaulb
5d371a2a8b Ops: authFetch robuste — reconnexion auto sur session Authentik expirée
Avant: ne rechargeait que sur 401/403 stricts → ratait la redirection IdP / page HTML de
login (vrai cas d'expiration) → données vides nécessitant un refresh manuel. Maintenant:
détecte redirection auth.targo.ca + HTML-au-lieu-de-JSON → reload auto (anti-boucle 20s),
+ 1 retry sur coupure réseau transitoire (ex: backend qui redémarre).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:20:01 -04:00
louispaulb
1f47ee4eae Ops cohérence: composants partagés TechSelect (autosuggest) + SkillSelect (chips)
Corrige les manques signalés: champ technicien (congés) → autosuggest typeahead; compétences
(demande de shift + éditeur équipe) → chips au lieu de texte libre. Composants réutilisables
pour une UX cohérente partout (et le copilote/réassignation à venir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:57:58 -04:00
louispaulb
412b6f49a6 Copilote roster: outils d'action (marquer_indisponibilite + reassigner_job)
Le copilote peut désormais CHANGER les dispos en langage naturel ('marque Simon malade
aujourd'hui' → crée une Tech Availability approuvée) et réassigner un Dispatch Job, sur
instruction explicite, avec confirmation. Testé OK end-to-end. (SMS client + escalade = suite.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:57:33 -04:00
louispaulb
79d160b9f1 Ops: page Copilote dispatch (chat + voix + sélecteur de politique)
Nouvelle page /copilote : chat texte/voix (Web Speech API fr-CA) vers le copilote Gemini Flash
(impact d'absence + propositions de réassignation), + sélecteur de politique de reprise
(réassign/SMS/escalade) persistée. Route + nav (icône Sparkles ; ajout CalendarRange/CalendarClock
manquantes dans la map d'icônes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:09:16 -04:00
louispaulb
d1bd268a32 Copilote roster (Gemini Flash, function-calling) + politique de reprise configurable
lib/roster-assistant.js : couche conversationnelle sur le roster (distincte du solveur OR-Tools).
Outils data réels (etat_equipe, jobs_du_technicien) via roster.fetchTechnicians + Dispatch Job.
Ex: 'TECH-4776 malade le 16 juin' → résout le nom, liste les RDV impactés, propose des techs
dispos qualifiés. Routes /roster/assistant + /roster/policy (politique persistée fichier).
Réutilise le moteur geminiChat de lib/agent.js (gemini-2.5-flash). Testé OK avec données réelles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:08:26 -04:00
louispaulb
3a90dafb9f docs: déploiement Karrio (expédition multi-transporteurs self-hosted) + sources
Architecture, accès, Dockerfile.carriers + override compose (sanitisés), transporteurs
(Canada Post/Purolator/Canpar/Nationex/Dicom/BoxKnight + agrégateurs eShipper/Freightcom),
pièges (collision hostname, admin, binaire venv), plan d'intégration hub #54. Sources incluses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:25:36 -04:00
louispaulb
a855e11476 Store: prix barré par produit via champ Item.store_regular_price
Affiché barré (gris) si > prix de vente (Item Price), sur produits simples + variantes
(les bundles gardent barré=somme des composants). Catalogue lit le champ; page rend <s>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 21:55:57 -04:00
louispaulb
5807d58913 Store: catalogue live depuis ERPNext (GET /store/catalog)
- Curation = champ Item.show_in_store (case à cocher) ; catégorie=item_group, prix=Item Price, stock=Bin live
- Bundles via Product Bundle (prix vs somme barrée), variantes mono-attribut (delta prix + stock/valeur)
- Fix filtres erp.list (tableaux, pas objets — listUrl teste .length) + child-tables lues via erp.get parent
- Page: fetch /store/catalog au mount, repli démo, bandeau live/démo, stock simple respecté

Prérequis PG corrigé: retrait ORDER BY NULL (MySQLism) dans erpnext utilities/product.py
(validation de variante) — sinon création de variantes impossible sur PostgreSQL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 21:23:01 -04:00
louispaulb
87949f933d Boutique matériel — page modèle /store (staging, self-contained Vue 3)
Vitrine fluide inspirée des meilleurs carts : grille produits, visualisation de
variantes (swatches couleur + pills longueur/modèle, prix delta live, rupture barrée),
panier optimistic (drawer slide, steppers, badge animé, toast, taxes QC, total).
Données démo (caméras/boosters/câbles/TP-Link/bundles). Branchement ERPNext (catalog
+ Product Bundle + pricing) = étape suivante. Validé live au navigateur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:09:47 -04:00
louispaulb
37f4d5a941 Wizard: section Installation (palier financé) à l'étape Items
- Presets Standard 360→240 (10$/mois) / Simple 180→120 (5$/mois) / Aucune
- 2 cases éditables Prix original / Prix financé + aperçu live (barré, $/mois, crédité→0$)
- Alimente install_fee/install_regular du Service Contract → page d'acceptation affiche le détail
- Placé sous Code de référence dans la vue résidentielle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:20:04 -04:00
louispaulb
e2104c93f2 Wizard: 2 cases génériques Prix marketing / Prix original (barré) sur toutes les lignes + aperçu client WYSIWYG
- Prix original (regular_price) disponible sur lignes récurrentes ET ponctuelles (avant: ponctuelles seulement)
- Si Prix original > Prix marketing → barré affiché (sommaire + récap + aperçu live dans l'éditeur)
- Forfait récurrent barré remonte au contrat (monthly_regular) → page d'acceptation affiche <s>99.95</s> 79.95/mois
- Cohérent avec install (install_regular 360→240). Aucun impact CRTC (rabais go-forward, jamais récupéré)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:31:54 -04:00
louispaulb
4b2e6fb698 Install: prix barré 360$ → 240$ (ancre de valeur) + 'financement crédité chaque mois 24 mois' dans contrat + portail signup
Champ install_regular (Service Contract) = valeur affichée barrée; install_fee reste le montant financé/dû réel. Conforme CRTC: 360 = référence marketing seulement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:22:22 -04:00
louispaulb
7267a27cda Portail self-service abonnement (staging /signup): stepper épuré Forfait→Coordonnées→Récap→Confirmation, catalogue réel, capture Lead
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:16:19 -04:00
louispaulb
6ff4a324ca Récap contrat: détail mensuel (forfait + financement install − rabais nouveau client = net), install offerte tant qu'abonné
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:08:08 -04:00
louispaulb
45c31e555f Contrat résidentiel: conformité CRTC 2026-43 (fin du clawback)
Installation = vraie créance financée (champ install_fee : 240$ std → 10$/mois,
120$ simple → 5$/mois). Résiliation résidentielle ne facture que le solde restant
de l'install (service rendu) ; promotions/carte-cadeau jamais récupérées
(termination_fee_benefits=0). Récap reformulé (financement install, plus de
"promotion non étalée"). Conforme avant l'échéance du 12 juin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:05:56 -04:00
louispaulb
ba76b8f3cc Site: texte Actualités révisé + template courriel gift natif (FR)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:42:52 -04:00
louispaulb
f4138cdd75 Roster AI (planification) + prise de rendez-vous client
Solveur OR-Tools (services/roster-solver) : couverture, compétences,
équité, coût chargé, cadence/efficacité, capacité-par-job ; contraintes
dures/souples façon Timefold.

Hub (lib/roster.js) : génération via solveur, publication par réécriture
de semaine (anti-doublons), demande (effectif ou nb de jobs), cadence/coût/
compétences par tech, pause, congés (Tech Availability + approbation),
booking (slots roster-aware / fit 3-dispos / confirm) + portail public /book.
Réessai sur serialization failures frappe_pg ; appels ERP séquentiels.

Ops : page Planification (grille compacte « J8 », multi-shift, drag-select
+ undo/redo, modèles de semaine, éditeur cadence&coût, congés, SMS opt-in),
page Rendez-vous (répartiteur), jobColor tech en pause → tickets rouges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:42:44 -04:00
louispaulb
d8366ab0be docs: spec module Shifts (intégré dispatch, sans paie)
Spec v1 du module de gestion de shifts + approbations dans le dispatch
(décision build-our-own vs Frappe HR déjà actée: PG + multitenant + géofence-job).
Modèle de données, workflows d'approbation (ShiftRequest/Swap/AttendanceRequest),
géofence vs adresse du job dispatch, auto-attendance, auto-roster Timefold,
mapping validé vs Frappe HR, intégrations (Authentik/dispatch/ERPNext API), phasage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 18:06:19 -04:00
louispaulb
21e2c846bf feat(ops): Email Queue admin page (view/delete/purge ERPNext outbound)
Surfaces ERPNext's Email Queue in Ops (nav « File courriels ») so ops can see
what's queued — important now that mute_emails=1 + scheduler paused mean nothing
flushes — and delete/purge stale entries without the ERPNext desk.

- hub lib/email-queue.js: GET list (by status, recipients read from each row's
  full doc since ERPNext ignores fields on child-doctype REST), DELETE :name,
  POST /purge {status}. Wired in server.js.
- ops: api/emailQueue.js + EmailQueuePage.vue (status filter, recipients,
  reference, error tooltip, per-row delete + « Purger Not Sent »), route + nav.

Verified live: 13 'Not Sent' (old internal test emails, no invoice refs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:29:33 -04:00
louispaulb
c4bf18fdcb fix(legacy-report): treat accounts with a company name as commercial
98 business accounts had commercial=0 but a company name (Ferme X Inc.,
Ville de Farnham, Les Jardins Sorel…), leaking into the residential report.
Rule is now: commercial = (commercial flag) OR (company present). Residential
@90$ drops 739→654 (0 company-bearing rows left); commercial 244→276.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 06:33:25 -04:00
louispaulb
b01cf19db6 chore(legacy): reusable targeted refresh of report tables from live billing DB
scripts/refresh-legacy-report-tables.sh: dumps only the 4 tables the
overpriced-internet report needs (account/delivery/service/product) from the
live legacy DB with --single-transaction (non-locking), verifies the dump is
complete before importing into the local legacy-db copy. ~20MB, seconds.
Creds read from a prod-only .refresh.env (gitignored; .env.example committed).

Used to refresh the copy from the 2026-05-22 snapshot to current
(service 66741→69233, account 15321→15706).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 23:03:21 -04:00
louispaulb
bde7a5ef67 feat(ops): Ville column in overpriced-internet report (sort + filter by city)
Adds a sortable 'Ville' column (field city) to the report. Quasar's default
filter scans all columns, so the existing search box now matches city too.
Street address caption drops the now-redundant city (keeps postal code).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 22:44:34 -04:00
louispaulb
105b0b2a51 feat(ops): per-address competitor column via Québec IHV open data
Replaces the reCAPTCHA-blocked Cogeco scraper with the authoritative Québec
"Accès Internet haute vitesse" open ArcGIS data (providers declared to the
gov by the providers themselves — validated to match Cogeco's own popup).

- hub lib/serviceability.js: address → ADR (Adresse_complete → IdAdresse +
  Etat_hiv, civic+postal match w/ JS street disambiguation) → FRN table
  (IdAdresse → FRN_nom providers + signup URLs). Referer-gated proxy, disk
  cache (90d), polite rate limit. Routes /serviceability/lookup[-batch].
- ops ReportInternetCherPage: "Concurrence (FSI)" column — provider chips
  (Cogeco highlighted), batch-fetched on demand with progress; "Cogeco
  disponible" summary card = churn-risk count; manual Cogeco verify icon kept.

Validated live: 37 Chemin Noël → Cogeco+Targo, 147 Montée Richard → Targo
only, Repentigny → Bell+Cogeco. Endpoints documented in
memory/reference_quebec_ihv.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 22:40:47 -04:00