Files
clienthub/src/db/migrations/0018_timer_scope_and_due_date.sql
T
simone a9358da96f feat(timer): il tempo si imputa a fase e task, non solo al progetto
"Quanto e' costata questa fase" non era una domanda che si potesse fare:
time_entries aveva la sola project_id.

Migration 0018 (gia' applicata a prod): phase_id e task_id su time_entries,
piu' due indici, piu' projects.due_date che serve al blocco successivo. Solo
ADD COLUMN e CREATE INDEX.

Due scelte che vale la pena spiegare:

- ON DELETE SET NULL, non CASCADE. Cancellare un task NON deve cancellare le
  ore lavorate su di esso: sono storico fatturabile. L'entry ricade a livello
  progetto e il totale del progetto non cambia mai. Con CASCADE, ripulire una
  fase avrebbe silenziosamente abbassato il fatturato tracciato. Verificato
  sul DB di produzione dentro una transazione con ROLLBACK: cancellato il
  task, l'entry sopravvive con task_id NULL, phase_id intatto e i secondi
  invariati.
- Il timer su un task scrive ENTRAMBE le colonne. Cosi' il totale di una fase
  e' un group-by diretto su phase_id, senza risalire dai task, e comprende
  anche il tempo imputato alla fase ma a nessun task in particolare.

Resta un solo timer attivo in tutto l'hub. Da qui una conseguenza in UI: se
sta girando su un task, il timer del tab "Timer" NON si mostra acceso —
mostrarlo acceso farebbe credere che siano due cronometri diversi. Il tab lo
dice a parole e avvisa che avviarlo fermerebbe l'altro.

Le 8 entry esistenti restano valide con entrambe le colonne a NULL, cioe'
"tempo di progetto": e' esattamente cio' che sono.

PhasesTab passa ai token semantici mentre lo si tocca. Non e' zelo: ci si
infila dentro una TimerCell che i token li usa gia', e in dark mode un badge a
token dentro una card bg-white si vede. Un pezzo di DEBT-01 in meno.

Cade startTimerForClient, senza chiamanti da quando la lista progetti non ha
piu' il timer.

Build pulito. L'avvio/arresto dal browser non e' ancora stato provato: si
verifica in produzione, che e' l'unico posto dove esiste il DB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:38:06 +02:00

41 lines
2.4 KiB
SQL

-- Additive: timer per fase/task e data di consegna attesa sul progetto.
--
-- ── time_entries.phase_id / task_id ──────────────────────────────────────────
-- Il timer nasce a livello di progetto: time_entries aveva la sola project_id,
-- quindi "quanto e' costata questa fase" non era una domanda che si potesse
-- fare. Le due colonne sono NULLABLE e project_id resta obbligatoria: ogni
-- entry e' sempre attribuita a un progetto, il dettaglio e' un di piu'.
-- Le righe esistenti restano valide con entrambe a NULL, cioe' "tempo di
-- progetto, non imputato a una fase" — che e' esattamente cio' che sono.
--
-- ON DELETE SET NULL, non CASCADE, ed e' la scelta che conta qui: cancellare un
-- task NON deve cancellare il tempo tracciato su di esso. Sono ore lavorate, e
-- quindi storico fatturabile; l'entry ricade a livello progetto e il totale del
-- progetto non cambia mai. Con CASCADE, ripulire una fase avrebbe silenziosamente
-- abbassato il fatturato tracciato.
--
-- ── projects.due_date ────────────────────────────────────────────────────────
-- La consegna attesa e' normalmente DERIVATA: project_offers.start_date +
-- offer_micros.duration_months. Questa colonna e' l'override manuale, e vince
-- sulla derivata quando e' valorizzata. Nullable per forza: la stragrande
-- maggioranza dei progetti continuera' a non averla, ed e' giusto cosi' —
-- un campo obbligatorio in piu' su ogni progetto e' un campo che invecchia.
--
-- Nessun DROP, nessun TRUNCATE, nessuna colonna rimossa o modificata.
-- Applicare a prod via SSH+docker exec PRIMA di pushare il codice dipendente.
-- Idempotente: safe to re-run.
ALTER TABLE time_entries
ADD COLUMN IF NOT EXISTS phase_id text REFERENCES phases(id) ON DELETE SET NULL;
ALTER TABLE time_entries
ADD COLUMN IF NOT EXISTS task_id text REFERENCES tasks(id) ON DELETE SET NULL;
-- Il rollup per fase raggruppa su queste due colonne a ogni apertura del
-- progetto: senza indice diventa una scansione piena appena le entry crescono.
CREATE INDEX IF NOT EXISTS time_entries_phase_idx ON time_entries (phase_id);
CREATE INDEX IF NOT EXISTS time_entries_task_idx ON time_entries (task_id);
ALTER TABLE projects
ADD COLUMN IF NOT EXISTS due_date timestamptz;