Files
clienthub/.planning/phases/04-progetti-multi-project/04-03-PLAN.md
T

672 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 04-progetti-multi-project
plan: "03"
type: execute
wave: 2
depends_on:
- 04-01-PLAN.md
files_modified:
- src/app/admin/projects/[id]/page.tsx
- src/app/admin/timer-actions.ts
- src/components/admin/tabs/TimerTab.tsx
- src/components/admin/ProfitabilityCard.tsx
- src/app/admin/impostazioni/page.tsx
autonomous: true
requirements:
- PROJ-01
- PROJ-03
- PROJ-05
must_haves:
truths:
- "La pagina /admin/projects/[id] mostra il workspace con tabs Fasi, Pagamenti, Documenti, Commenti, Preventivo, Timer"
- "Il tab Timer mostra il totale ore lavorate e un bottone play/stop funzionante"
- "Il tab Timer mostra la ProfitabilityCard con €/h reale, costo ideale, delta guadagno/perdita"
- "timer-actions.ts usa project_id invece di client_id per startTimer e stopTimer"
- "La pagina /admin/impostazioni esiste e permette di impostare target_hourly_rate"
artifacts:
- path: "src/app/admin/projects/[id]/page.tsx"
provides: "Workspace progetto con tabs"
contains: "getProjectFullDetail"
- path: "src/app/admin/timer-actions.ts"
provides: "Timer actions con project_id"
contains: "project_id: projectId"
- path: "src/components/admin/tabs/TimerTab.tsx"
provides: "Tab timer con TimerCell + ProfitabilityCard"
contains: "ProfitabilityCard"
- path: "src/components/admin/ProfitabilityCard.tsx"
provides: "Card analytics profittabilità"
contains: "totalTrackedSeconds / 3600"
- path: "src/app/admin/impostazioni/page.tsx"
provides: "Pagina impostazioni admin con target hourly rate"
contains: "target_hourly_rate"
key_links:
- from: "src/app/admin/projects/[id]/page.tsx"
to: "src/lib/admin-queries.ts"
via: "getProjectFullDetail(id)"
pattern: "getProjectFullDetail"
- from: "src/components/admin/tabs/TimerTab.tsx"
to: "src/components/admin/ProfitabilityCard.tsx"
via: "ProfitabilityCard component"
pattern: "ProfitabilityCard"
- from: "src/app/admin/impostazioni/page.tsx"
to: "src/lib/settings.ts"
via: "updateSetting / getTargetHourlyRate"
pattern: "getTargetHourlyRate\|updateSetting"
---
<objective>
Admin project workspace (/admin/projects/[id]) e analytics profittabilità. Clona il workspace di /admin/clients/[id] adattandolo al livello progetto, refactora il timer per usare project_id, crea il TimerTab con ProfitabilityCard, e aggiunge /admin/impostazioni per il target_hourly_rate.
Può girare in PARALLELO con 04-02 perché non tocca nessuno degli stessi file.
Purpose: Consegna il workspace completo per progetto (PROJ-01 e PROJ-03) e le analytics profittabilità (PROJ-05). Dopo questo piano l'admin ha un workspace funzionale per ogni progetto incluso il timer e le analytics.
Output: /admin/projects/[id] funzionale, timer migrato a project_id, analytics card, settings page.
</objective>
<execution_context>
@/Users/simonecavalli/.claude/get-shit-done/workflows/execute-plan.md
@/Users/simonecavalli/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@/Users/simonecavalli/IAMCAVALLI/.planning/PROJECT.md
@/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md
@/Users/simonecavalli/IAMCAVALLI/CLAUDE.md
@/Users/simonecavalli/IAMCAVALLI/.planning/phases/04-progetti-multi-project/04-01-SUMMARY.md
<interfaces>
<!-- Tipi e funzioni disponibili da 04-01. -->
Da src/lib/admin-queries.ts:
```typescript
export type ProjectFullDetail = {
project: Project & { client: { id: string; name: string; brand_name: string; slug: string | null } };
phases: Array<Phase & { tasks: Array<Task & { deliverables: Deliverable[] }> }>;
payments: Payment[];
documents: Document[];
notes: Note[];
comments: Comment[];
quoteItems: QuoteItemWithLabel[];
activeServices: ServiceCatalog[];
activeTimerEntryId: string | null;
activeTimerStartedAt: Date | null;
totalTrackedSeconds: number;
};
export async function getProjectFullDetail(id: string): Promise<ProjectFullDetail | null>;
```
Da src/lib/settings.ts:
```typescript
export const SETTINGS_KEYS: { TARGET_HOURLY_RATE: "target_hourly_rate" };
export async function getSetting(key: string): Promise<string | null>;
export async function updateSetting(key: string, value: string): Promise<void>;
export async function getTargetHourlyRate(): Promise<number>;
```
Template da replicare per workspace (da src/app/admin/clients/[id]/page.tsx — da leggere in read_first):
- Tabs: PhasesTab, PaymentsTab, DocumentsTab, CommentsTab, QuoteTab
- PhasesViewToggle per toggle kanban/list
- ClientActions (ora diventano ProjectActions)
Nota: TimerCell usa il prop `clientId` per la compatibilità con il nome storico — in realtà passiamo il projectId. Il componente TimerCell chiama startTimer(clientId) e stopTimer(entryId) dalle timer-actions.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Refactoring timer-actions.ts (client_id → project_id) + ProfitabilityCard + TimerTab</name>
<files>
src/app/admin/timer-actions.ts
src/components/admin/ProfitabilityCard.tsx
src/components/admin/tabs/TimerTab.tsx
</files>
<read_first>
- src/app/admin/timer-actions.ts — leggere interamente: startTimer, stopTimer, le loro dipendenze da client_id nel DB
- src/components/admin/TimerCell.tsx — leggere le props interface e come chiama startTimer/stopTimer
- src/components/admin/tabs/QuoteTab.tsx — pattern "use client" + server action + useTransition per TimerTab
- src/app/admin/clients/[id]/page.tsx — vedere come TimerCell è attualmente passato (per capire dove compare il timer e cosa props riceve)
</read_first>
<action>
**A. Aggiornare src/app/admin/timer-actions.ts**
Riscrivere startTimer per usare project_id. Leggere prima l'intero file corrente.
Il cambiamento principale:
1. Parametro `clientId: string``projectId: string`
2. `db.insert(time_entries).values({ id, client_id: clientId })``db.insert(time_entries).values({ id, project_id: projectId })`
3. La query "stop any running session" rimane GLOBALE (non per progetto) — D-15: solo un timer attivo alla volta
4. Aggiornare `revalidatePath` per includere `/admin/projects`
```typescript
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/db";
import { time_entries } from "@/db/schema";
import { eq, isNull, and } from "drizzle-orm";
import { nanoid } from "nanoid";
export async function startTimer(projectId: string): Promise<{ entryId: string }> {
// Stop ALL currently running sessions (global: only one timer active at a time — D-15)
const running = await db
.select({ id: time_entries.id, started_at: time_entries.started_at })
.from(time_entries)
.where(isNull(time_entries.ended_at));
for (const r of running) {
const now = new Date();
const secs = Math.round((now.getTime() - new Date(r.started_at).getTime()) / 1000);
await db
.update(time_entries)
.set({ ended_at: now, duration_seconds: secs })
.where(eq(time_entries.id, r.id));
}
// Create new entry scoped to PROJECT (not client) — D-19
const id = nanoid();
await db.insert(time_entries).values({ id, project_id: projectId });
revalidatePath("/admin/projects");
revalidatePath("/admin");
return { entryId: id };
}
export async function stopTimer(entryId: string): Promise<void> {
const rows = await db
.select({ started_at: time_entries.started_at })
.from(time_entries)
.where(eq(time_entries.id, entryId))
.limit(1);
if (!rows[0]) return;
const now = new Date();
const secs = Math.round((now.getTime() - new Date(rows[0].started_at).getTime()) / 1000);
await db
.update(time_entries)
.set({ ended_at: now, duration_seconds: secs })
.where(eq(time_entries.id, entryId));
revalidatePath("/admin/projects");
revalidatePath("/admin");
}
```
**B. Creare src/components/admin/ProfitabilityCard.tsx**
Implementare il componente analytics (D-20). Calcolo:
- ore = totalTrackedSeconds / 3600
- €/h reale = accepted_total ÷ ore (se ore > 0, altrimenti mostrare "—")
- costo ideale = targetHourlyRate × ore
- delta = accepted_total - costo_ideale (positivo = guadagno, negativo = perdita)
```typescript
// src/components/admin/ProfitabilityCard.tsx
// NO "use client" — questo è un componente server-renderable (solo display, no interactivity)
type ProfitabilityCardProps = {
acceptedTotal: string; // e.g., "1500.00"
totalTrackedSeconds: number;
targetHourlyRate: number; // e.g., 50
};
export function ProfitabilityCard({
acceptedTotal,
totalTrackedSeconds,
targetHourlyRate,
}: ProfitabilityCardProps) {
const hours = totalTrackedSeconds / 3600;
const accepted = parseFloat(acceptedTotal || "0");
const realHourlyRate = hours > 0 ? accepted / hours : null;
const idealCost = targetHourlyRate * hours;
const delta = accepted - idealCost;
const deltaIsProfit = delta >= 0;
return (
<div className="bg-white rounded-lg border border-[#e5e7eb] p-4 space-y-3">
<h3 className="font-medium text-[#1a1a1a]">Profittabilità</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<p className="text-[#71717a] text-xs">Ore lavorate</p>
<p className="font-mono font-semibold text-[#1a1a1a]">{hours.toFixed(1)}h</p>
</div>
<div>
<p className="text-[#71717a] text-xs">Importo accettato</p>
<p className="font-mono font-semibold text-[#1a1a1a]">
{accepted > 0 ? `${accepted.toFixed(2)}` : "Non impostato"}
</p>
</div>
</div>
<div className="border-t border-[#f4f4f5] pt-3 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[#71717a]">/h reale</span>
<span className="font-mono font-semibold text-[#1a1a1a]">
{realHourlyRate !== null ? `${realHourlyRate.toFixed(2)}/h` : "—"}
</span>
</div>
<div className="flex justify-between">
<span className="text-[#71717a]">/h target</span>
<span className="font-mono font-semibold text-[#71717a]">{targetHourlyRate.toFixed(2)}/h</span>
</div>
<div className="flex justify-between">
<span className="text-[#71717a]">Costo ideale ({hours.toFixed(1)}h × {targetHourlyRate}/h)</span>
<span className="font-mono font-semibold text-[#1a1a1a]">{idealCost.toFixed(2)}</span>
</div>
</div>
{hours > 0 && accepted > 0 && (
<div className="border-t border-[#f4f4f5] pt-3 flex justify-between items-center">
<span className="text-[#71717a]">Delta (guadagno/perdita)</span>
<span className={`font-mono font-bold ${deltaIsProfit ? "text-green-600" : "text-red-600"}`}>
{deltaIsProfit ? "+" : ""}{delta.toFixed(2)}
</span>
</div>
)}
{hours === 0 && (
<p className="text-xs text-[#71717a] border-t border-[#f4f4f5] pt-3">
Avvia il timer per iniziare a tracciare le ore.
</p>
)}
</div>
);
}
```
**C. Creare src/components/admin/tabs/TimerTab.tsx**
Il TimerTab mostra il timer (TimerCell) e la ProfitabilityCard. È un Client Component perché TimerCell è "use client".
```typescript
"use client";
import { TimerCell } from "@/components/admin/TimerCell";
import { ProfitabilityCard } from "@/components/admin/ProfitabilityCard";
type TimerTabProps = {
projectId: string;
acceptedTotal: string;
activeTimerEntryId: string | null;
activeTimerStartedAt: Date | null;
totalTrackedSeconds: number;
targetHourlyRate: number;
};
export function TimerTab({
projectId,
acceptedTotal,
activeTimerEntryId,
activeTimerStartedAt,
totalTrackedSeconds,
targetHourlyRate,
}: TimerTabProps) {
return (
<div className="space-y-6">
<div className="bg-white rounded-lg border border-[#e5e7eb] p-4">
<h3 className="font-medium text-[#1a1a1a] mb-4">Timer</h3>
<TimerCell
clientId={projectId}
activeEntryId={activeTimerEntryId}
activeStartedAt={activeTimerStartedAt}
totalTrackedSeconds={totalTrackedSeconds}
/>
</div>
<ProfitabilityCard
acceptedTotal={acceptedTotal}
totalTrackedSeconds={totalTrackedSeconds}
targetHourlyRate={targetHourlyRate}
/>
</div>
);
}
```
NOTA: TimerCell usa il prop `clientId` ma nel contesto progetto gli passiamo `projectId`. Questo è intentionale per mantenere la compatibilità con TimerCell senza modificarlo. TimerCell chiamerà `startTimer(projectId)` — che ora è il parametro corretto.
Verificare che TimerCell importi da `@/app/admin/timer-actions` e non da un path relativo. Se usa path relativo, assicurarsi che la risoluzione sia corretta.
</action>
<verify>
<automated>npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- src/app/admin/timer-actions.ts contains `project_id: projectId` nella insert (grep: `grep "project_id: projectId" src/app/admin/timer-actions.ts`)
- src/app/admin/timer-actions.ts does NOT contain `client_id:` nella insert (grep: vecchio pattern rimosso)
- src/components/admin/ProfitabilityCard.tsx exists e contains `totalTrackedSeconds / 3600` (grep)
- src/components/admin/tabs/TimerTab.tsx exists e contains `ProfitabilityCard` (grep)
- src/components/admin/tabs/TimerTab.tsx contains `clientId={projectId}` (passa project id a TimerCell) (grep)
- TypeScript compila senza errori
</acceptance_criteria>
<done>Timer migrato a project_id, ProfitabilityCard e TimerTab creati</done>
</task>
<task type="auto">
<name>Task 2: /admin/projects/[id] workspace + /admin/impostazioni settings page</name>
<files>
src/app/admin/projects/[id]/page.tsx
src/app/admin/impostazioni/page.tsx
</files>
<read_first>
- src/app/admin/clients/[id]/page.tsx — LEGGERE INTERAMENTE: questo è il template esatto che cloniamo per projects/[id]; capire tutti i component imports, i pattern params, la struttura Tabs
- src/lib/settings.ts — import getTargetHourlyRate, updateSetting, SETTINGS_KEYS
- src/components/admin/tabs/TimerTab.tsx — props interface appena creato in Task 1
- src/app/admin/catalog/page.tsx — pattern per la settings page (form con server action inline)
</read_first>
<action>
**A. Creare src/app/admin/projects/[id]/page.tsx**
Clonare src/app/admin/clients/[id]/page.tsx sostituendo:
- `getClientFullDetail(id)``getProjectFullDetail(id)` (import da @/lib/admin-queries)
- Le props dei tab components: sostituire clientId con projectId dove necessario
- Aggiungere il tab Timer (nuovo) usando TimerTab
- Header: mostrare nome progetto + "← Progetti" come breadcrumb, sottotitolo = nome cliente
```typescript
import { notFound } from "next/navigation";
import { getProjectFullDetail } from "@/lib/admin-queries";
import { getTargetHourlyRate } from "@/lib/settings";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { PhasesTab } from "@/components/admin/tabs/PhasesTab";
import { PaymentsTab } from "@/components/admin/tabs/PaymentsTab";
import { DocumentsTab } from "@/components/admin/tabs/DocumentsTab";
import { CommentsTab } from "@/components/admin/tabs/CommentsTab";
import { QuoteTab } from "@/components/admin/tabs/QuoteTab";
import { NotesTab } from "@/components/admin/tabs/NotesTab"; // se esiste
import { TimerTab } from "@/components/admin/tabs/TimerTab";
import { PhasesViewToggle } from "@/components/admin/kanban/PhasesViewToggle";
import Link from "next/link";
export const revalidate = 0;
export default async function ProjectDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const [detail, targetHourlyRate] = await Promise.all([
getProjectFullDetail(id),
getTargetHourlyRate(),
]);
if (!detail) notFound();
const {
project,
phases,
payments,
documents,
notes,
comments,
quoteItems,
activeServices,
activeTimerEntryId,
activeTimerStartedAt,
totalTrackedSeconds,
} = detail;
return (
<div>
<div className="mb-4">
<Link href="/admin/projects" className="text-sm text-[#71717a] hover:text-[#1a1a1a]">
Progetti
</Link>
</div>
<div className="mb-6 flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="text-2xl font-bold text-[#1a1a1a]">{project.name}</h1>
<p className="text-sm text-[#71717a]">
<Link href={`/admin/clients/${project.client.id}`} className="hover:text-[#1a1a1a] hover:underline">
{project.client.name}
</Link>
</p>
</div>
</div>
<Tabs defaultValue="phases" className="w-full">
<TabsList className="mb-6">
<TabsTrigger value="phases">Fasi &amp; Task</TabsTrigger>
<TabsTrigger value="payments">Pagamenti</TabsTrigger>
<TabsTrigger value="documents">Documenti</TabsTrigger>
<TabsTrigger value="notes">Note</TabsTrigger>
<TabsTrigger value="comments">Commenti</TabsTrigger>
<TabsTrigger value="quote">Preventivo</TabsTrigger>
<TabsTrigger value="timer">Timer</TabsTrigger>
</TabsList>
<TabsContent value="phases">
<PhasesViewToggle
listView={<PhasesTab phases={phases} clientId={id} />}
phases={phases}
clientId={id}
/>
</TabsContent>
<TabsContent value="payments">
<PaymentsTab payments={payments} clientId={id} />
</TabsContent>
<TabsContent value="documents">
<DocumentsTab documents={documents} clientId={id} />
</TabsContent>
{/* Render NotesTab solo se il component esiste — altrimenti inline */}
<TabsContent value="notes">
<div className="space-y-4">
{notes.length === 0 && (
<p className="text-sm text-[#71717a]">Nessuna nota ancora.</p>
)}
{notes.map((note) => (
<div key={note.id} className="bg-white rounded-lg border border-[#e5e7eb] p-4">
<p className="text-sm text-[#1a1a1a] whitespace-pre-wrap">{note.body}</p>
<p className="text-xs text-[#71717a] mt-2">
{new Date(note.created_at).toLocaleDateString("it-IT")}
</p>
</div>
))}
</div>
</TabsContent>
<TabsContent value="comments">
<CommentsTab comments={comments} clientId={id} />
</TabsContent>
<TabsContent value="quote">
<QuoteTab
quoteItems={quoteItems}
activeServices={activeServices}
clientId={id}
acceptedTotal={project.accepted_total ?? "0"}
/>
</TabsContent>
<TabsContent value="timer">
<TimerTab
projectId={id}
acceptedTotal={project.accepted_total ?? "0"}
activeTimerEntryId={activeTimerEntryId}
activeTimerStartedAt={activeTimerStartedAt}
totalTrackedSeconds={totalTrackedSeconds}
targetHourlyRate={targetHourlyRate}
/>
</TabsContent>
</Tabs>
</div>
);
}
```
NOTA CRITICA: I tab components (PhasesTab, PaymentsTab, DocumentsTab, CommentsTab, QuoteTab) potrebbero avere prop `clientId` che originariamente si riferivano al client.id. In questo contesto, passiamo il project.id come `clientId` — i tab usano quel valore per le loro server actions (addPhase, addPayment, ecc.). Le server actions di fase/pagamento/documento potrebbero ancora cercare client_id nel DB. VERIFICARE leggendo ogni actions file:
- Se le actions usano ancora `client_id` nel DB, bisogna aggiornare le actions dei tab per usare `project_id`. Questo è parte dello stesso task.
- Leggere src/app/admin/clients/[id]/phase-actions.ts (o simile) e src/app/admin/clients/[id]/payment-actions.ts per capire se fanno insert con client_id.
- Aggiornare TUTTI i file di actions che fanno insert/update con client_id su tabelle che ora usano project_id.
Specificamente, cercare tutti i file di actions:
```bash
grep -r "client_id" src/app/admin/clients/[id]/ --include="*actions*"
```
Per ogni occorrenza che fa insert su phases, payments, documents, notes, quote_items: cambiare il campo da client_id a project_id e aggiornare i revalidatePath da /admin/clients/[id] a /admin/projects/[id].
**B. Creare src/app/admin/impostazioni/page.tsx**
```typescript
import { getTargetHourlyRate, updateSetting, SETTINGS_KEYS } from "@/lib/settings";
import { revalidatePath } from "next/cache";
export const revalidate = 0;
export default async function ImpostazioniPage() {
const targetRate = await getTargetHourlyRate();
async function handleSave(fd: FormData) {
"use server";
const newRate = fd.get("target_hourly_rate");
if (!newRate || isNaN(parseFloat(String(newRate)))) return;
await updateSetting(SETTINGS_KEYS.TARGET_HOURLY_RATE, String(parseFloat(String(newRate)).toFixed(2)));
revalidatePath("/admin/impostazioni");
}
return (
<div>
<h1 className="text-2xl font-bold text-[#1a1a1a] mb-6">Impostazioni</h1>
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6 max-w-md">
<h2 className="text-base font-semibold text-[#1a1a1a] mb-4">Analytics Profittabilità</h2>
<form action={handleSave} className="space-y-4">
<div>
<label
htmlFor="target_hourly_rate"
className="block text-sm font-medium text-[#1a1a1a] mb-1"
>
Tariffa oraria target (/h)
</label>
<p className="text-xs text-[#71717a] mb-2">
Usata per calcolare il costo ideale e il delta profitto/perdita per ogni progetto.
</p>
<div className="flex items-center gap-2">
<span className="text-sm text-[#71717a]"></span>
<input
id="target_hourly_rate"
name="target_hourly_rate"
type="number"
step="0.01"
min="0"
defaultValue={targetRate.toFixed(2)}
className="border border-[#e5e7eb] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20 w-32"
/>
<span className="text-sm text-[#71717a]">/h</span>
</div>
</div>
<button
type="submit"
className="bg-[#1A463C] text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-[#1A463C]/90 transition-colors"
>
Salva
</button>
</form>
</div>
</div>
);
}
```
</action>
<verify>
<automated>npm run build 2>&1 | tail -30</automated>
</verify>
<acceptance_criteria>
- src/app/admin/projects/[id]/page.tsx exists e contains `getProjectFullDetail` (grep)
- src/app/admin/projects/[id]/page.tsx contains `TimerTab` import e usage (grep)
- src/app/admin/projects/[id]/page.tsx contains `getTargetHourlyRate` (grep)
- src/app/admin/impostazioni/page.tsx exists e contains `SETTINGS_KEYS.TARGET_HOURLY_RATE` (grep)
- src/app/admin/impostazioni/page.tsx contains `updateSetting` (grep)
- Tutte le actions di fase/pagamento/documento/note/quote che facevano insert con client_id sono state aggiornate a project_id (grep: `grep -r "client_id" src/app/admin/clients/\[id\]/ --include="*actions*"` non deve avere insert su tabelle migrate)
- `npm run build` completa senza errori TypeScript
- Navigando /admin/projects/[id] (con un progetto esistente) la pagina carica senza 500 errors
- Il tab Timer mostra TimerCell e ProfitabilityCard renderizzati
</acceptance_criteria>
<done>/admin/projects/[id] workspace completo con timer e analytics; /admin/impostazioni funzionale</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Admin session → timer-actions | startTimer e stopTimer non hanno requireAdmin perché chiamati da TimerCell lato client; il guard è il middleware Auth.js su /admin/* che blocca accesso non autenticato |
| Admin session → impostazioni | handleSave inline server action in pagina /admin/impostazioni — il guard Auth.js su /admin/* blocca utenti non autenticati |
| project workspace → quote_items | QuoteTab viene passato quoteItems da getProjectFullDetail — non accessibile via client API (D-02 / CLAUDE.md constraint) |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-04-09 | Information Disclosure | getProjectFullDetail — quoteItems | mitigate | quoteItems inclusi solo nella risposta admin (questo workspace); la funzione client-view (Wave 3, 04-04) non deve includere quote_items — invariante CLAUDE.md |
| T-04-10 | Tampering | timer-actions.ts — startTimer | accept | Auth.js middleware su /admin/* impedisce accesso anonimo; timer actions non espongono dati sensibili, solo time tracking |
| T-04-11 | Information Disclosure | ProfitabilityCard — accepted_total visibile | accept | accepted_total è il totale accettato dal cliente (non il dettaglio dei singoli servizi) — corretto mostrarlo all'admin nel workspace progetto |
| T-04-12 | Tampering | updateSetting — target_hourly_rate | accept | Setting è solo un numero (tariffa oraria); nessun rischio sicurezza; Auth.js middleware blocca accesso non autenticato a /admin/impostazioni |
| T-04-13 | Tampering | phase-actions / payment-actions migrazione project_id | mitigate | Dopo aggiornamento actions: insert usa project_id con FK constraint → DB rifiuta project_id non validi con constraint violation |
</threat_model>
<verification>
```bash
# 1. Timer uses project_id
grep "project_id: projectId" src/app/admin/timer-actions.ts
# 2. No client_id insert in timer
grep -v "project_id" src/app/admin/timer-actions.ts | grep "client_id"
# 3. Analytics card exists
grep "totalTrackedSeconds / 3600" src/components/admin/ProfitabilityCard.tsx
# 4. TimerTab imports ProfitabilityCard
grep "ProfitabilityCard" src/components/admin/tabs/TimerTab.tsx
# 5. Project workspace uses new query
grep "getProjectFullDetail" src/app/admin/projects/\[id\]/page.tsx
# 6. Settings key constant used
grep "SETTINGS_KEYS" src/app/admin/impostazioni/page.tsx
# 7. Build
npm run build
```
</verification>
<success_criteria>
- /admin/projects/[id] carica senza errori e mostra tutti i tab (Fasi, Pagamenti, Documenti, Note, Commenti, Preventivo, Timer)
- Il tab Timer mostra TimerCell (play/stop) e ProfitabilityCard (con ore, €/h reale, costo ideale, delta)
- /admin/impostazioni carica e mostra il form con il valore corrente della tariffa (default 50.00 se non impostata)
- Salvando un nuovo valore in /admin/impostazioni il valore viene persistito e la pagina mostra il nuovo valore
- `npm run build` passa senza errori
</success_criteria>
<output>
After completion, create `.planning/phases/04-progetti-multi-project/04-03-SUMMARY.md` following the template at `@/Users/simonecavalli/.claude/get-shit-done/templates/summary.md`.
Key items to document:
- Quali actions file sono stati aggiornati da client_id a project_id (lista esaustiva)
- Come TimerCell è stato adattato per usare project_id (prop naming)
- Se NotesTab esiste come component o se le note sono state implementate inline
- Valore default inizializzato per target_hourly_rate
</output>