Commit Graph

12 Commits

Author SHA1 Message Date
louispaulb
27263d7766 tickets: distinguer par département + technicien assigné ; réponse courriel = Received
- TicketCard : badge Département (issue_type) + technicien (assigned_staff) → distingue les
  tickets quasi identiques (ex. 3 « Cancellation » = Support/Facturation/Rétention).
- migrate_tickets.py : importe assigned_staff (ticket.assign_to → nom du staff) — le rattrapage
  ne le posait pas. Backfill des ~7,5k tickets assignés en F mais sans technicien côté ERPNext.
- logToLinkedTicket : direction Received si l'expéditeur = adresse destinataire du ticket
  (une réponse depuis un domaine interne @targo n'est plus mal classée « Sent »).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:50:01 -04:00
louispaulb
417ef08f58 ticket: aller-retour courriel (envoi + réponse revient au fil)
Hub (conversation.js) : getOrCreateConvForIssue (lie une Issue à une conv email) + emailTicket
(envoie dans un fil Gmail, sujet [Ticket #ISS-…] + lien direct, journalise Sent Communication).
Route POST /conversations/ticket-email (gaté Authentik). La réponse du client re-thread par le tag
sujet (TICKET_REF_RX) OU le threadId → logToLinkedTicket (déjà en place) → Communication Received
sur l'Issue → visible au panneau. notifyCustomer mémorise désormais conv.threadId au 1er envoi.

UI (IssueDetail) : bouton « Envoyer par courriel » + dialogue (À/Cc/message), « Répondre » scindé
en « Note interne » (Communication) vs courriel. Lien direct = URL du ticket.

Testé : envoi réel à louis@targo.ca sur ISS-0000250013 (channel:email, Sent Communication OK).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 07:42:39 -04:00
louispaulb
0e5c890b74 perf(hub): cache liste techs (30s) + garde anti-chevauchement sync + jeton ingest mémoïsé
Batch 2 de l'audit (perf backend), items sûrs :
- roster.js : fetchTechnicians caché 30 s (appelé par endpoint + generate + materialize +
  tech-positions polling 25 s pour des données quasi statiques). invalidateTechCache() sur
  les 6 écritures Dispatch Technician (coût/effic/skills/pause/traccar/weekly) → pas de stale
  après édition. Vérifié : call1 71 ms → call2 5 ms (caché).
- legacy-dispatch-sync.js : garde _inFlight sur le tick d'import ET la veille → un passage
  long ne se relance plus par-dessus (évitait doublons Dispatch Job + contention ERPNext).
- conversation.js : jeton d'ingestion webhook mémoïsé (mailIngestToken) au lieu d'un
  fs.readFileSync SYNCHRONE à chaque courriel/canal entrant (bloquait la boucle d'événements).

NON fait (par prudence) : paralléliser les erp.list séquentiels de generate() (séquencement
DÉLIBÉRÉ anti-ECONNRESET Frappe, cf. feedback_hub_crash_502) ; batch N+1 Customer du triage
(refactor risqué d'une chaîne de fallback stateful — reporté) ; jitter des pollers (502 boot
déjà mitigé).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:42:18 -04:00
louispaulb
7fa8c6dc74 refactor(priority): unify on 3 levels (ERPNext standard), high = red, jobs + conversations
- shared priorityMeta/PRIORITY_LEVELS → 3 levels: Haute (RED) · Moyenne
  (neutral grey) · Basse. Default/medium = grey outlined_flag. Aliases
  urgent→high, normal→medium for old conversation data.
- conversations: priority endpoint accepts 'medium' (keeps old values for
  back-compat); the flag menu now shows the 3 levels.
- jobs: dropped the parallel job-flags override store — the job flag now
  writes the REAL ERPNext priority (low/medium/high) via patchJob/
  updateJob, same source as the pool sheet (no more two mechanisms).
  setEntryPriority reuses patchJob. Solver: high never dropped + served
  early, low dropped first.

Verified: job flag + conversation flag both grey-by-default, menu Haute/
Moyenne/Basse, high=red.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:01:25 -04:00
louispaulb
512c4a5f1b feat(ops): dispatch auto complet + perf Boîte/rapports + fix session
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).

Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:49:35 -04:00
louispaulb
6628eaaa5b feat(ops+hub): sender picker, per-user notifs, editable rating emails, planif & tickets UX
- Comms: sélecteur d'expéditeur « De: » (défaut groupe Support TARGO) via resolveSendFrom + alias vérifiés
- Notifs: prefs de feeds PAR utilisateur (/conversations/notif-prefs) + cloche à bascules ; boot tooltip-ux (clic prioritaire + anti-empilement)
- Courriel: invitation à évaluer = modèle Unlayer éditable (transactional-rating-invite-*) ; test-send via Gmail + expansion {{rating}} ; logo TARGO auto-hébergé sur le magasin d'actifs du hub
- Planif: bloc « sans déplacement » (damier, début de quart, alerte si pas de quart), quart éditable dans l'éditeur de jour, icônes de compétence en vue jour (TV pour télé), clic cellule → éditeur, clic gauche lane → liste / clic droit → menu quart, lien ↗ ticket par job
- Tickets: défaut « Non fermés » + correction du filtre « Mes tickets » (owner)
- Inbox: poll Gmail 1 min + rafraîchir à la demande (poll-now)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:41:51 -04:00
louispaulb
1b7bad1686 security: harden payments/hub auth + remove leaked ERPNext token from source
Auth hardening:
- payments: per-customer JWT authorization on dual-use /payments routes
  (balance/methods/invoice/checkout/setup/portal/toggle-ppa) via authorizeCustomer
  + PAYMENTS_AUTH=enforce; portal retains+sends magic-link JWT (sessionStorage)
- hub: fail-closed Stripe webhook, /accept/doc-pdf IDOR gate, telephony field-name
  guard (SQLi), modem-bridge private-IP guard (SSRF), ALWAYS_ENFORCE expansion

Leaked-credential cleanup (token already rotated in ERPNext):
- de-hardcode ERPNext API token -> env in bulk_submit.py, import_items.py,
  apps/client/deploy.sh; placeholder in apps/ops/infra/nginx.conf (nginx injects)
- ops prod build no longer bakes VITE_ERP_TOKEN (.env.production empty)
- de-hardcode legacy DB password -> env in import_items.py
- gitignore legacy migration PII exports (tsv/json)

Conversations/UI:
- floating (un-docked) conversation panel; full-width mailbox
- per-message real sender from email headers; unified scroll; header spacing
- campaign pre-send review (subject/from/channel), Gmail send channel, clone-as-draft

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 06:17:17 -04:00
louispaulb
08069ec0dc feat(inbox): show the agent's real name as sender + cleaner Gmail-style thread
Sender identity on outbound:
- Replies/new emails now go out as e.g. "Gilles Drolet" <support@targo.ca> instead
  of the generic alias name "Service TARGO". The name is derived from the agent's SSO
  email (gilles.drolet@ -> "Gilles Drolet"); the ADDRESS stays the monitored alias so
  replies still land in the shared inbox. Verified: Gmail honors the custom display
  name on the send-as alias (sent From header = "Gilles Drolet <support@targo.ca>").
- gmail.sendFrom() exported; conversation.js agentSendFrom() builds the From from the
  stamped msg.agent (replies) / x-authentik-email (new sends via email-new).

UI cleanup (space + clarity, Gmail-style):
- One thin separator line between messages; removed the rounded gray card box around
  emails (.email-card flat), the email-card-head bar, and the blue expanded-head
  highlight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 07:15:23 -04:00
louispaulb
9466645e17 fix(inbox): never resolve our own domains to a customer (wrong sender names)
Root cause: some F customer records carry our own/staff addresses in email_billing,
so matchCustomer's `email_billing LIKE '%addr%'` resolved support@targo.ca -> "Guylaine
Gagnon" and gilles@targointernet.com -> "Sylvie Juteau". Every thread on one of our
addresses then inherited that wrong customer + name.

- OWN_DOMAINS promoted to a single source in lib/helpers.js (was duplicated in
  conversation.js); inbox-triage.matchCustomer() now returns null for any own-domain
  address — a customer is never reachable at targo.ca / targointernet.com / gigafibre.ca.
- conversation.js consumes the shared OWN_DOMAINS (removes the local copy).

Also ran a one-shot repair (temporary endpoint, since removed) that unlinked the 6
already-contaminated threads and reset their display name to the real address.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:51:04 -04:00
louispaulb
19d31025a6 feat(inbox+historique): sender-identity fix, single-source taxonomy, dispatch history & leaderboards
Inbox identity/grouping (fixes mislabeled threads):
- customerName = real From display name, then a customer matched BY EMAIL,
  then the raw address — never the AI's content-guessed name (a gilles@ relay
  showed as "Sylvie Juteau").
- never group a thread by one of our own domains (support@targo.ca,
  *@targointernet.com): re-ingested outbound was collapsing unrelated threads
  into one mislabeled "Guylaine Gagnon" thread.

Refactor: queues/TYPES/QUEUE_OF/CAT centralized in lib/categories.js (were
drifting across conversation.js + inbox-triage.js; telephonie/television had
no matching triage type). Removed dead export findConversationByEmail.

Feat: addMessage stamps msg.agent on outbound replies -> per-agent stats.

Historique (/historique, HistoriquePage.vue): tournées par technicien (ALL
statuses incl. Completed/Cancelled — the board hid them), tech leaderboard,
inbox leaderboard. Hub: /dispatch/history, /dispatch/leaderboard,
/conversations/leaderboard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:11:47 -04:00
louispaulb
32baa409fc refactor(targo-hub): add erp.js wrapper + migrate 7 lib files to it
Replaces hand-rolled `erpFetch` + `encodeURIComponent(JSON.stringify(...))`
URL building with a structured wrapper: erp.get/list/listRaw/create/update/
remove/getMany/hydrateLabels/raw.

Key wins:
- erp.list auto-retries up to 5 times when v16 rejects a fetched/linked
  field with "Field not permitted in query" — the field is dropped and the
  call continues, so callers don't have to know which fields v16 allows.
- erp.hydrateLabels batches link-label resolution (customer_name,
  service_location_name, …) in one query per link field — no N+1, no
  v16 breakage.
- Consistent {ok, error, status} shape for mutations.

Migrated call sites:
- otp.js: Customer email lookup + verifyOTP customer detail fetch +
  Contact email fallback + Service Location listing
- referral.js: Referral Credit fetch / update / generate
- tech-absence-sms.js: lookupTechByPhone, set/clear absence
- conversation.js: Issue archive create
- magic-link.js: Tech lookup for /refresh
- ical.js: Tech lookup + jobs listing for iCal feed
- tech-mobile.js: 13 erpFetch sites → erp wrapper

Remaining erpFetch callers (dispatch.js, acceptance.js, payments.js,
contracts.js, checkout.js, …) deliberately left untouched this pass —
they each have 10+ sites and need individual smoke-tests.

Live-tested against production ERPNext: tech-mobile page renders 54K
bytes, no runtime errors in targo-hub logs post-restart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 23:01:27 -04:00
louispaulb
a8eeb1b77d refactor: major cleanup — remove dead dispatch app, commit all backend code, extract client composables
- Remove apps/dispatch/ (100% replaced by ops dispatch module, unmaintained)
- Commit services/targo-hub/lib/ (24 modules, 6290 lines — was never tracked)
- Commit services/docuseal + services/legacy-db docker-compose configs
- Extract client app composables: useOTP, useAddressSearch, catalog data, format utils
- Refactor CartPage.vue 630→175 lines, CatalogPage.vue 375→95 lines
- Clean hardcoded credentials from config.js fallback values
- Add client portal: catalog, cart, checkout, OTP verification, address search
- Add ops: NetworkPage, AgentFlowsPage, ConversationPanel, UnifiedCreateModal
- Add ops composables: useBestTech, useConversations, usePermissions, useScanner
- Add field app: scanner composable, docker/nginx configs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:38:38 -04:00