From aa5921481b2db9d36f089269e081490ef63b9530 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Wed, 22 Apr 2026 20:40:54 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20contract=20=E2=86=92=20chain=20?= =?UTF-8?q?=E2=86=92=20subscription=20=E2=86=92=20prorated=20invoice=20lif?= =?UTF-8?q?ecycle=20+=20tech=20group=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - contracts.js: built-in install chain fallback when no Flow Template matches on_contract_signed — every accepted contract now creates a master Issue + chained Dispatch Jobs (fiber_install template) so we never lose a signed contract to a missing flow config. - acceptance.js: export createDeferredJobs + propagate assigned_group into Dispatch Job payload (was only in notes, not queryable). - dispatch.js: chain-walk helpers (unblockDependents, _isChainTerminal, setJobStatusWithChain) + terminal-node detection that activates pending Service Subscriptions (En attente → Actif, start_date=tomorrow) and emits a prorated Sales Invoice covering tomorrow → EOM. Courtesy-day billing convention: activation day is free, first period starts next day. - dispatch.js: fix Sales Invoice 417 by resolving company default income account (Ventes - T) and passing company + income_account on each item. - dispatch.js: GET /dispatch/group-jobs + POST /dispatch/claim-job for tech self-assignment from the group queue; enriches with customer_name / service_location via per-job fetches since those fetch_from fields aren't queryable in list API. - TechTasksPage.vue: redesigned mobile-first UI with progress arc, status chips, and new "Tâches du groupe" section showing claimable unassigned jobs with a "Prendre" CTA. Live updates via SSE job-claimed / job-unblocked. - NetworkPage.vue + poller-control.js: poller toggle semantics flipped — green when enabled, red/gray when paused; explicit status chips for clarity. E2E verified end-to-end: CTR-00007 → 4 chained jobs → claim → In Progress → Completed walks chain → SUB-0000100002 activated (start=2026-04-24) → SINV-2026-700012 prorata $9.32 (= 39.95 × 7/30). Co-Authored-By: Claude Opus 4.7 --- apps/ops/quasar.config.js | 2 +- apps/ops/src/css/tech.scss | 363 ++++++++ apps/ops/src/layouts/TechLayout.vue | 87 +- .../src/modules/tech/components/JobCard.vue | 141 +++ .../src/modules/tech/pages/TechDevicePage.vue | 2 +- .../modules/tech/pages/TechDiagnosticPage.vue | 4 +- .../modules/tech/pages/TechJobDetailPage.vue | 682 ++++++++++----- .../src/modules/tech/pages/TechMorePage.vue | 121 ++- .../src/modules/tech/pages/TechScanPage.vue | 2 +- .../src/modules/tech/pages/TechTasksPage.vue | 827 ++++++++++++------ apps/ops/src/pages/NetworkPage.vue | 144 +++ services/targo-hub/lib/acceptance.js | 129 ++- services/targo-hub/lib/checkout.js | 4 +- services/targo-hub/lib/contracts.js | 164 +++- services/targo-hub/lib/devices.js | 7 + services/targo-hub/lib/dispatch.js | 339 ++++++- services/targo-hub/lib/olt-snmp.js | 7 + services/targo-hub/lib/poller-control.js | 108 +++ services/targo-hub/lib/tech-mobile.js | 11 +- services/targo-hub/server.js | 1 + 20 files changed, 2539 insertions(+), 606 deletions(-) create mode 100644 apps/ops/src/css/tech.scss create mode 100644 apps/ops/src/modules/tech/components/JobCard.vue create mode 100644 services/targo-hub/lib/poller-control.js diff --git a/apps/ops/quasar.config.js b/apps/ops/quasar.config.js index 44964ee..8f88588 100644 --- a/apps/ops/quasar.config.js +++ b/apps/ops/quasar.config.js @@ -5,7 +5,7 @@ module.exports = configure(function () { return { boot: ['pinia'], - css: ['app.scss'], + css: ['app.scss', 'tech.scss'], extras: ['material-icons'], diff --git a/apps/ops/src/css/tech.scss b/apps/ops/src/css/tech.scss new file mode 100644 index 0000000..9d67801 --- /dev/null +++ b/apps/ops/src/css/tech.scss @@ -0,0 +1,363 @@ +// TARGO Tech Module — Mobile-first design tokens + base components +// +// The tech surface (`/j/*` routes) targets glove-on-screen readability, +// one-thumb reach, and TARGO brand identity. We deliberately don't reuse +// Quasar's default component chrome here — custom CSS gives us the pill +// radii, shadow layering, and motion curves trending design expects. + +// ── 1. Tokens ──────────────────────────────────────────────────────────────── +// Scoped to `.tech-surface` so the rest of the ops app is unaffected. +.tech-surface { + // TARGO palette — derived from existing #3f3d7a → #5c59a8 gradient + --tg-900: #2a2858; // deep night, pressed/active + --tg-800: #363566; + --tg-700: #3f3d7a; // current header start + --tg-500: #5c59a8; // current header end + --tg-300: #8b88d4; // tints, subtle accents + --tg-100: #dcdaf0; // avatar backgrounds + --tg-50: #f2f1fb; // wash backgrounds + + // Gradients + --tg-hero: linear-gradient(135deg, #3f3d7a 0%, #5c59a8 100%); + --tg-hero-deep: linear-gradient(160deg, #2a2858 0%, #3f3d7a 60%, #5c59a8 100%); + + // Signal — tuned warmer than Quasar defaults, better color-blind legibility + --sig-go: #10b981; // Completed / success + --sig-work: #f59e0b; // In Progress + --sig-stop: #ef4444; // Urgent / error + --sig-wait: #94a3b8; // On Hold (chain-blocked, invisible to tech) + --sig-info: #3b82f6; // Scheduled, neutral + + // Surface + ink (warmer than current #eef1f5) + --surface: #fafbfd; + --surface-elev: #ffffff; + --surface-wash: #f2f1fb; // same as --tg-50 + --surface-over: rgba(255,255,255,0.88); // for floating islands with backdrop blur + + --ink-900: #0f172a; + --ink-700: #334155; + --ink-500: #64748b; + --ink-400: #94a3b8; + --ink-300: #cbd5e1; + + // Elevation system (two-layer lift, not a single fat drop shadow) + --elev-1: 0 1px 2px rgba(15,23,42,0.04), 0 2px 6px rgba(15,23,42,0.03); + --elev-2: 0 2px 8px rgba(15,23,42,0.06), 0 12px 32px rgba(15,23,42,0.04); + --elev-3: 0 4px 14px rgba(15,23,42,0.08), 0 24px 48px rgba(15,23,42,0.06); + --elev-fab: 0 8px 24px rgba(16,185,129,0.35); + --elev-fab-purple: 0 8px 24px rgba(92,89,168,0.35); + + // Radii + --r-sm: 10px; + --r-md: 16px; + --r-lg: 20px; + --r-xl: 28px; + --r-pill: 999px; + + // Motion — Apple spring for page-level, shorter spring for micro + --ease-spring: cubic-bezier(0.32, 0.72, 0, 1); + --ease-micro: cubic-bezier(0.4, 0, 0.2, 1); + --dur-fast: 160ms; + --dur-mid: 240ms; + --dur-slow: 360ms; + + // Type scale + --t-display: 600 2rem/1.1 system-ui, -apple-system, sans-serif; // 32px hero + --t-h1: 700 1.5rem/1.2 system-ui, -apple-system, sans-serif; // 24px + --t-h2: 700 1.25rem/1.25 system-ui, -apple-system, sans-serif; // 20px + --t-body: 500 1rem/1.4 system-ui, -apple-system, sans-serif; // 16px + --t-body-sm: 500 0.9375rem/1.4 system-ui, -apple-system, sans-serif; // 15px + --t-caption: 500 0.8125rem/1.3 system-ui, -apple-system, sans-serif; // 13px + --t-micro: 600 0.6875rem/1.2 system-ui, -apple-system, sans-serif; // 11px + + // Base surface + background: var(--surface); + color: var(--ink-900); + min-height: 100vh; + font-feature-settings: 'cv11', 'ss01'; // nicer tabular digits +} + +// Every tech page needs room below its content for the floating tab island +// (96px pill + 16px margin + safe-area). We do this with a utility class +// `.tg-page` so pages opt in explicitly, and for the legacy Quasar +// `` pages we extend that element with extra padding too. +.tech-surface.q-page { + padding-bottom: calc(96px + env(safe-area-inset-bottom)) !important; +} + +// ── 2. Typography utilities ───────────────────────────────────────────────── +.tech-surface { + .t-display { font: var(--t-display); letter-spacing: -0.02em; } + .t-h1 { font: var(--t-h1); letter-spacing: -0.01em; } + .t-h2 { font: var(--t-h2); letter-spacing: -0.005em; } + .t-body { font: var(--t-body); } + .t-body-sm { font: var(--t-body-sm); } + .t-caption { font: var(--t-caption); color: var(--ink-500); } + .t-micro { font: var(--t-micro); text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-500); } + + .t-ink-900 { color: var(--ink-900); } + .t-ink-700 { color: var(--ink-700); } + .t-ink-500 { color: var(--ink-500); } + .t-ink-400 { color: var(--ink-400); } + .t-ink-on-hero { color: rgba(255,255,255,0.95); } + .t-ink-on-hero-dim { color: rgba(255,255,255,0.7); } +} + +// ── 3. Card — elevated pill-radius, no borders ────────────────────────────── +.tech-surface { + .tg-card { + background: var(--surface-elev); + border-radius: var(--r-lg); + box-shadow: var(--elev-2); + padding: 18px; + transition: transform var(--dur-fast) var(--ease-micro), + box-shadow var(--dur-fast) var(--ease-micro); + + &.tg-card--wash { background: var(--surface-wash); box-shadow: none; } + &.tg-card--flat { box-shadow: var(--elev-1); } + &.tg-card--tap { + cursor: pointer; + -webkit-tap-highlight-color: transparent; + &:active { transform: scale(0.985); box-shadow: var(--elev-1); } + } + &.tg-card--done { opacity: 0.62; } + } +} + +// ── 4. Status chip — pill badge (replaces border-left indicator) ──────────── +.tech-surface { + .tg-chip { + display: inline-flex; align-items: center; gap: 6px; + padding: 4px 10px; + border-radius: var(--r-pill); + font: var(--t-micro); + white-space: nowrap; + + .tg-chip-dot { + width: 6px; height: 6px; border-radius: 50%; + display: inline-block; + } + + &.is-go { background: rgba(16,185,129,0.12); color: #047857; .tg-chip-dot { background: var(--sig-go); } } + &.is-work { background: rgba(245,158,11,0.14); color: #b45309; .tg-chip-dot { background: var(--sig-work); } } + &.is-stop { background: rgba(239,68,68,0.12); color: #b91c1c; .tg-chip-dot { background: var(--sig-stop); } } + &.is-wait { background: rgba(148,163,184,0.16); color: #475569; .tg-chip-dot { background: var(--sig-wait); } } + &.is-info { background: rgba(59,130,246,0.12); color: #1d4ed8; .tg-chip-dot { background: var(--sig-info); } } + &.is-tg { background: rgba(92,89,168,0.12); color: var(--tg-700); .tg-chip-dot { background: var(--tg-500); } } + } +} + +// ── 5. Button system — pill, FAB, ghost ───────────────────────────────────── +.tech-surface { + .tg-btn { + display: inline-flex; align-items: center; justify-content: center; + gap: 8px; + min-height: 52px; + padding: 0 24px; + border: 0; + border-radius: var(--r-pill); + font: 700 1rem/1 system-ui, -apple-system, sans-serif; + letter-spacing: 0.01em; + cursor: pointer; + transition: transform var(--dur-fast) var(--ease-micro), + box-shadow var(--dur-fast) var(--ease-micro), + background var(--dur-fast) var(--ease-micro); + -webkit-tap-highlight-color: transparent; + user-select: none; + + &:disabled { opacity: 0.5; cursor: not-allowed; } + &:active:not(:disabled) { transform: scale(0.97); } + + &.is-block { width: 100%; } + + // Variants + &.is-primary { + background: var(--tg-700); color: #fff; + box-shadow: var(--elev-fab-purple); + &:active { background: var(--tg-900); } + } + &.is-success { + background: var(--sig-go); color: #fff; + box-shadow: var(--elev-fab); + &:active { background: #059669; } + } + &.is-warning { + background: var(--sig-work); color: #fff; + box-shadow: 0 8px 24px rgba(245,158,11,0.35); + } + &.is-ghost { + background: var(--surface-wash); color: var(--tg-700); + box-shadow: none; + &:active { background: var(--tg-100); } + } + &.is-outline { + background: transparent; color: var(--tg-700); + box-shadow: inset 0 0 0 1.5px var(--tg-300); + } + &.is-text { + background: transparent; color: var(--tg-700); + box-shadow: none; min-height: 40px; padding: 0 12px; + } + + &.is-compact { min-height: 40px; padding: 0 16px; font-size: 0.9375rem; } + } + + // Sticky floating CTA pill — sits above the tab bar + .tg-fab-cta { + position: fixed; + left: 16px; right: 16px; + bottom: calc(var(--tab-island-bottom, 88px) + env(safe-area-inset-bottom)); + z-index: 20; + } +} + +// ── 6. Hero header — big gradient banner ──────────────────────────────────── +.tech-surface { + .tg-hero { + background: var(--tg-hero-deep); + color: #fff; + padding: 18px 18px 28px; + border-radius: 0 0 var(--r-xl) var(--r-xl); + position: relative; + overflow: hidden; + + // Subtle radial highlight for depth + &::before { + content: ''; + position: absolute; inset: 0; + background: radial-gradient(1200px 400px at 85% -20%, rgba(255,255,255,0.14), transparent 60%); + pointer-events: none; + } + + .tg-hero-inner { position: relative; z-index: 1; } + } +} + +// ── 7. Floating tab island (bottom nav) ───────────────────────────────────── +.tech-surface .tg-tab-island, +.tg-tab-island { + position: fixed; + left: 16px; right: 16px; + bottom: calc(16px + env(safe-area-inset-bottom)); + height: 64px; + border-radius: var(--r-xl); + background: var(--surface-over); + backdrop-filter: saturate(180%) blur(22px); + -webkit-backdrop-filter: saturate(180%) blur(22px); + box-shadow: var(--elev-3); + border: 1px solid rgba(15,23,42,0.04); + display: flex; align-items: center; justify-content: space-around; + z-index: 30; + padding: 0 8px; + + .tg-tab { + flex: 1; display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 2px; + height: 56px; border-radius: var(--r-md); + cursor: pointer; color: var(--ink-500); + transition: color var(--dur-fast) var(--ease-micro); + -webkit-tap-highlight-color: transparent; + text-decoration: none; + + .tg-tab-icon { font-size: 24px; line-height: 1; } + .tg-tab-label { font: var(--t-micro); } + + &.is-active { + color: var(--tg-700); + .tg-tab-icon { transform: translateY(-1px); } + } + + &:active { background: var(--tg-50); } + } +} + +// ── 8. Page spacing (reserve room for floating tab island) ────────────────── +// Bottom-room is handled at the q-page level (.tech-surface.q-page above), +// so inner content containers don't need to add their own bottom padding — +// they just flow naturally and the tab-island gap is guaranteed. +.tech-surface .tg-page { min-height: 100vh; } + +// ── 9. Peek sheet (bottom card that slides up) ────────────────────────────── +.tech-surface { + .tg-peek-backdrop { + position: fixed; inset: 0; z-index: 40; + background: rgba(15,23,42,0.4); + backdrop-filter: blur(2px); + opacity: 0; pointer-events: none; + transition: opacity var(--dur-mid) var(--ease-micro); + &.is-open { opacity: 1; pointer-events: auto; } + } + + .tg-peek { + position: fixed; left: 0; right: 0; bottom: 0; + z-index: 41; + background: var(--surface-elev); + border-radius: var(--r-xl) var(--r-xl) 0 0; + box-shadow: var(--elev-3); + padding: 8px 0 calc(16px + env(safe-area-inset-bottom)); + transform: translateY(100%); + transition: transform var(--dur-mid) var(--ease-spring); + max-height: 82vh; + overflow-y: auto; + overscroll-behavior: contain; + + &.is-open { transform: translateY(0); } + + .tg-peek-handle { + width: 40px; height: 4px; border-radius: 2px; + background: var(--ink-300); margin: 6px auto 4px; + } + } +} + +// ── 10. Progress arc (SVG ring for hero stat) ─────────────────────────────── +.tech-surface { + .tg-arc { + position: relative; + display: inline-flex; align-items: center; justify-content: center; + + svg { transform: rotate(-90deg); } + + .tg-arc-track { stroke: rgba(255,255,255,0.16); fill: none; } + .tg-arc-fill { stroke: #fff; fill: none; stroke-linecap: round; + transition: stroke-dashoffset var(--dur-slow) var(--ease-spring); } + + .tg-arc-center { + position: absolute; inset: 0; + display: flex; flex-direction: column; + align-items: center; justify-content: center; + text-align: center; + } + } +} + +// ── 11. Offline / system banner ───────────────────────────────────────────── +.tech-surface .tg-banner { + position: fixed; + top: env(safe-area-inset-top); left: 0; right: 0; + z-index: 50; + padding: 6px 16px; + background: var(--sig-stop); color: #fff; + font: var(--t-caption); + text-align: center; + display: flex; align-items: center; justify-content: center; gap: 6px; +} + +// ── 12. Avatar ────────────────────────────────────────────────────────────── +.tech-surface .tg-avatar { + width: 40px; height: 40px; + border-radius: 50%; + background: var(--tg-900); + color: #fff; + display: inline-flex; align-items: center; justify-content: center; + font: 700 0.875rem/1 system-ui; + letter-spacing: 0.02em; +} + +// ── 13. Route transitions (slide-up for card→detail) ──────────────────────── +.tech-slide-enter-active, .tech-slide-leave-active { + transition: transform var(--dur-mid) var(--ease-spring), + opacity var(--dur-mid) var(--ease-micro); +} +.tech-slide-enter-from { transform: translateY(24px); opacity: 0; } +.tech-slide-leave-to { transform: translateY(24px); opacity: 0; } diff --git a/apps/ops/src/layouts/TechLayout.vue b/apps/ops/src/layouts/TechLayout.vue index 889531a..d0e4c5a 100644 --- a/apps/ops/src/layouts/TechLayout.vue +++ b/apps/ops/src/layouts/TechLayout.vue @@ -1,60 +1,57 @@ diff --git a/apps/ops/src/modules/tech/components/JobCard.vue b/apps/ops/src/modules/tech/components/JobCard.vue new file mode 100644 index 0000000..d052ca7 --- /dev/null +++ b/apps/ops/src/modules/tech/components/JobCard.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/apps/ops/src/modules/tech/pages/TechDevicePage.vue b/apps/ops/src/modules/tech/pages/TechDevicePage.vue index eb62047..9d88375 100644 --- a/apps/ops/src/modules/tech/pages/TechDevicePage.vue +++ b/apps/ops/src/modules/tech/pages/TechDevicePage.vue @@ -25,7 +25,7 @@ is a deliberate, visible operation that needs to fail fast. -->