gigafibre-fsm/apps/ops/src/css/tech.scss
louispaulb 69fc971c04 feat: contract → chain → subscription → prorated invoice lifecycle + tech group claim
- 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 <noreply@anthropic.com>
2026-04-22 20:40:54 -04:00

364 lines
14 KiB
SCSS

// 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
// `<q-page padding>` 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; }