docs(04): create phase plan — 4 plans in 2 waves for multi-project architecture
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
---
|
||||
phase: 04-progetti-multi-project
|
||||
plan: "02"
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 04-01-PLAN.md
|
||||
files_modified:
|
||||
- src/components/admin/NavBar.tsx
|
||||
- src/components/admin/ProjectRow.tsx
|
||||
- src/app/admin/projects/page.tsx
|
||||
- src/app/admin/projects/new/page.tsx
|
||||
- src/app/admin/projects/project-actions.ts
|
||||
- src/app/admin/clients/[id]/page.tsx
|
||||
autonomous: true
|
||||
requirements:
|
||||
- PROJ-01
|
||||
- PROJ-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "La navbar admin mostra i link Progetti e Impostazioni oltre a Clienti e Catalogo"
|
||||
- "La pagina /admin/projects elenca tutti i progetti con colonne Nome (+ cliente), Valore, Acconto, Saldo, Timer, €/h"
|
||||
- "Il bottone '+ Nuovo Progetto' in /admin/projects apre un form che chiede nome e selezione cliente"
|
||||
- "La pagina /admin/clients/[id] mostra cards dei progetti del cliente con bottone '+ Nuovo Progetto'"
|
||||
- "Cliccando una card progetto si naviga a /admin/projects/[id]"
|
||||
- "createProject e archiveProject sono server actions funzionanti"
|
||||
artifacts:
|
||||
- path: "src/components/admin/NavBar.tsx"
|
||||
provides: "NavBar con link Progetti e Impostazioni"
|
||||
contains: "href=\"/admin/projects\""
|
||||
- path: "src/components/admin/ProjectRow.tsx"
|
||||
provides: "Riga progetto per la lista /admin/projects"
|
||||
contains: "ProjectWithPayments"
|
||||
- path: "src/app/admin/projects/page.tsx"
|
||||
provides: "Pagina lista tutti i progetti"
|
||||
contains: "getAllProjectsWithPayments"
|
||||
- path: "src/app/admin/projects/new/page.tsx"
|
||||
provides: "Form creazione progetto con selezione cliente"
|
||||
contains: "createProject"
|
||||
- path: "src/app/admin/projects/project-actions.ts"
|
||||
provides: "Server actions: createProject, archiveProject, updateProjectAcceptedTotal"
|
||||
contains: "export async function createProject"
|
||||
- path: "src/app/admin/clients/[id]/page.tsx"
|
||||
provides: "Pagina cliente modificata per mostrare project cards"
|
||||
contains: "getClientWithProjects"
|
||||
key_links:
|
||||
- from: "src/app/admin/projects/page.tsx"
|
||||
to: "src/lib/admin-queries.ts"
|
||||
via: "getAllProjectsWithPayments()"
|
||||
pattern: "getAllProjectsWithPayments"
|
||||
- from: "src/app/admin/clients/[id]/page.tsx"
|
||||
to: "src/lib/admin-queries.ts"
|
||||
via: "getClientWithProjects(id)"
|
||||
pattern: "getClientWithProjects"
|
||||
- from: "src/components/admin/ProjectRow.tsx"
|
||||
to: "src/app/admin/timer-actions.ts"
|
||||
via: "TimerCell with project_id"
|
||||
pattern: "TimerCell"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Admin projects list e client detail rewrite. Consegna la prima slice verticale visibile: l'admin può vedere tutti i progetti in /admin/projects, creare nuovi progetti da /admin/clients/[id] o dal form globale, e navigare ai workspace progetto.
|
||||
|
||||
Purpose: Rende operativa la struttura multi-project nell'area admin senza ancora richiedere il workspace completo del progetto (quello viene in 04-03). Dopo questo piano l'admin può creare e navigare progetti.
|
||||
|
||||
Output: NavBar aggiornata, /admin/projects funzionale, /admin/clients/[id] mostra project cards.
|
||||
</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 — usare direttamente, no esplorazione codebase necessaria. -->
|
||||
|
||||
Da src/lib/admin-queries.ts (creato in 04-01):
|
||||
```typescript
|
||||
export type ProjectWithPayments = {
|
||||
id: string;
|
||||
name: string;
|
||||
client: { id: string; name: string; slug: string | null };
|
||||
accepted_total: string;
|
||||
archived: boolean;
|
||||
created_at: Date;
|
||||
payments: Array<{ id: string; label: string; status: string; amount: string }>;
|
||||
activeTimerEntryId: string | null;
|
||||
activeTimerStartedAt: Date | null;
|
||||
totalTrackedSeconds: number;
|
||||
};
|
||||
|
||||
export async function getAllProjectsWithPayments(includeArchived?: boolean): Promise<ProjectWithPayments[]>;
|
||||
export async function getClientWithProjects(clientId: string): Promise<ClientWithProjects | null>;
|
||||
|
||||
export type ClientWithProjects = Client & {
|
||||
projects: Array<{ id: string; name: string; accepted_total: string; archived: boolean; created_at: Date }>;
|
||||
};
|
||||
```
|
||||
|
||||
Da src/app/admin/timer-actions.ts (da aggiornare in 04-03, ma TimerCell già usato):
|
||||
```typescript
|
||||
// TimerCell props (da src/components/admin/TimerCell.tsx):
|
||||
// clientId: string ← NOTA: questo è un nome legacy, in ProjectRow passiamo project.id
|
||||
// activeEntryId: string | null
|
||||
// activeStartedAt: Date | null
|
||||
// totalTrackedSeconds: number
|
||||
```
|
||||
|
||||
Pattern ClientRow (da clonare per ProjectRow):
|
||||
- src/components/admin/ClientRow.tsx — usa statusConfig, Badge, TimerCell, Link
|
||||
- Colonne ClientRow: nome, token/link, LTV (accepted_total), acconto badge, saldo badge, timer
|
||||
- Colonne ProjectRow (D-14): Nome+Cliente, Valore, Acconto, Saldo, Timer, €/h
|
||||
|
||||
€/h in lista = accepted_total ÷ (totalTrackedSeconds / 3600). Se ore = 0, mostrare "—".
|
||||
|
||||
Pattern admin page (da src/app/admin/page.tsx):
|
||||
- export const revalidate = 0
|
||||
- Server component asincrono, chiama query, passa a Row component
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: NavBar + ProjectRow + server actions (createProject, archiveProject, updateProjectAcceptedTotal)</name>
|
||||
<files>
|
||||
src/components/admin/NavBar.tsx
|
||||
src/components/admin/ProjectRow.tsx
|
||||
src/app/admin/projects/project-actions.ts
|
||||
</files>
|
||||
|
||||
<read_first>
|
||||
- src/components/admin/NavBar.tsx — leggere struttura attuale (link presenti, stili, imports)
|
||||
- src/components/admin/ClientRow.tsx — leggere interamente: questo è il template ESATTO per ProjectRow
|
||||
- src/components/admin/TimerCell.tsx — leggere per capire la prop interface (clientId, activeEntryId, activeStartedAt, totalTrackedSeconds)
|
||||
- src/app/admin/clients/[id]/quote-actions.ts — pattern server action (requireAdmin, revalidatePath)
|
||||
</read_first>
|
||||
|
||||
<action>
|
||||
**A. Aggiornare src/components/admin/NavBar.tsx**
|
||||
|
||||
Aggiungere i link "Progetti" e "Impostazioni" al NavBar esistente. Leggere il file per trovare dove sono i link esistenti (Clienti, Statistiche, Catalogo) e aggiungere nell'ordine:
|
||||
- Clienti (/admin)
|
||||
- Progetti (/admin/projects) ← NUOVO
|
||||
- Statistiche (/admin/analytics)
|
||||
- Catalogo (/admin/catalog)
|
||||
- Impostazioni (/admin/impostazioni) ← NUOVO
|
||||
|
||||
Ogni link usa il pattern esistente: `<Link href="..." className="text-sm text-white/70 hover:text-white transition-colors">`.
|
||||
|
||||
**B. Creare src/components/admin/ProjectRow.tsx**
|
||||
|
||||
Clonare ClientRow.tsx sostituendo:
|
||||
- `ClientWithPayments` → `ProjectWithPayments` (import da @/lib/admin-queries)
|
||||
- Colonna nome: `project.name` in bold, `project.client.name` in testo secondario xs
|
||||
- Rimuovere colonna token/link cliente (non si mostra il link pubblico nella lista progetti)
|
||||
- Colonna valore: `€{parseFloat(project.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}`
|
||||
- Colonna Acconto: badge per `project.payments.find(p => p.label.toLowerCase().includes("acconto"))`
|
||||
- Colonna Saldo: badge per `project.payments.find(p => p.label.toLowerCase().includes("saldo"))`
|
||||
- Colonna Timer: `<TimerCell clientId={project.id} activeEntryId={project.activeTimerEntryId} activeStartedAt={project.activeTimerStartedAt} totalTrackedSeconds={project.totalTrackedSeconds} />`
|
||||
- Colonna €/h: calcolo inline — `const hours = project.totalTrackedSeconds / 3600; const eurPerHour = hours > 0 ? parseFloat(project.accepted_total) / hours : null;` — mostrare `€{eurPerHour.toFixed(2)}/h` oppure `—` se null
|
||||
|
||||
Link cliccabile sul nome: `<Link href={"/admin/projects/" + project.id}>`.
|
||||
|
||||
Usare gli stessi statusConfig di ClientRow per i badge pagamento.
|
||||
|
||||
**C. Creare src/app/admin/projects/project-actions.ts**
|
||||
|
||||
```typescript
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { requireAdmin } from "@/lib/auth"; // stesso pattern delle altre actions
|
||||
import { db } from "@/db";
|
||||
import { projects, clients } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export async function createProject(fd: FormData): Promise<{ projectId: string }> {
|
||||
await requireAdmin();
|
||||
const name = String(fd.get("name") ?? "").trim();
|
||||
const clientId = String(fd.get("client_id") ?? "").trim();
|
||||
|
||||
if (!name) throw new Error("Nome progetto obbligatorio");
|
||||
if (!clientId) throw new Error("Cliente obbligatorio");
|
||||
|
||||
// Verify client exists
|
||||
const clientRows = await db
|
||||
.select({ id: clients.id })
|
||||
.from(clients)
|
||||
.where(eq(clients.id, clientId))
|
||||
.limit(1);
|
||||
if (clientRows.length === 0) throw new Error("Cliente non trovato");
|
||||
|
||||
const id = nanoid();
|
||||
await db.insert(projects).values({ id, client_id: clientId, name });
|
||||
|
||||
revalidatePath("/admin/projects");
|
||||
revalidatePath(`/admin/clients/${clientId}`);
|
||||
return { projectId: id };
|
||||
}
|
||||
|
||||
export async function archiveProject(projectId: string): Promise<void> {
|
||||
await requireAdmin();
|
||||
await db.update(projects).set({ archived: true }).where(eq(projects.id, projectId));
|
||||
revalidatePath("/admin/projects");
|
||||
}
|
||||
|
||||
export async function unarchiveProject(projectId: string): Promise<void> {
|
||||
await requireAdmin();
|
||||
await db.update(projects).set({ archived: false }).where(eq(projects.id, projectId));
|
||||
revalidatePath("/admin/projects");
|
||||
}
|
||||
|
||||
export async function updateProjectAcceptedTotal(projectId: string, acceptedTotal: string): Promise<void> {
|
||||
await requireAdmin();
|
||||
await db.update(projects).set({ accepted_total: acceptedTotal }).where(eq(projects.id, projectId));
|
||||
revalidatePath(`/admin/projects/${projectId}`);
|
||||
}
|
||||
```
|
||||
|
||||
NOTA: Verificare il path di `requireAdmin` leggendo un altro actions file (es. quote-actions.ts) — usare lo stesso import esatto.
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- src/components/admin/NavBar.tsx contains `href="/admin/projects"` (grep)
|
||||
- src/components/admin/NavBar.tsx contains `href="/admin/impostazioni"` (grep)
|
||||
- src/components/admin/ProjectRow.tsx exists e contains `ProjectWithPayments` (grep)
|
||||
- src/components/admin/ProjectRow.tsx contains `totalTrackedSeconds / 3600` (formula €/h) (grep)
|
||||
- src/app/admin/projects/project-actions.ts exports `createProject`, `archiveProject`, `unarchiveProject`, `updateProjectAcceptedTotal` (grep: `grep "export async function" src/app/admin/projects/project-actions.ts`)
|
||||
- TypeScript compila senza errori
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>NavBar aggiornata, ProjectRow pronto, server actions create</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: /admin/projects list page + /admin/projects/new form + /admin/clients/[id] project cards</name>
|
||||
<files>
|
||||
src/app/admin/projects/page.tsx
|
||||
src/app/admin/projects/new/page.tsx
|
||||
src/app/admin/clients/[id]/page.tsx
|
||||
</files>
|
||||
|
||||
<read_first>
|
||||
- src/app/admin/page.tsx — template esatto per la struttura della lista (revalidate, table, map su rows)
|
||||
- src/app/admin/clients/[id]/page.tsx — leggere INTERO FILE: va riscritto per mostrare project cards invece del workspace tab
|
||||
- src/app/admin/catalog/page.tsx — pattern admin page con form inline (per /admin/projects/new)
|
||||
- src/app/admin/clients/[id]/quote-actions.ts — per capire come il form usa server actions con redirect
|
||||
</read_first>
|
||||
|
||||
<action>
|
||||
**A. Creare src/app/admin/projects/page.tsx**
|
||||
|
||||
```typescript
|
||||
import { getAllProjectsWithPayments } from "@/lib/admin-queries";
|
||||
import { ProjectRow } from "@/components/admin/ProjectRow";
|
||||
import Link from "next/link";
|
||||
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function ProjectsPage() {
|
||||
const projects = await getAllProjectsWithPayments();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-[#1a1a1a]">Progetti</h1>
|
||||
<Link
|
||||
href="/admin/projects/new"
|
||||
className="text-sm bg-[#1A463C] text-white px-4 py-2 rounded-lg hover:bg-[#1A463C]/90 transition-colors"
|
||||
>
|
||||
+ Nuovo Progetto
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-12 text-center">
|
||||
<p className="text-[#71717a]">Nessun progetto ancora. Creane uno dal dettaglio di un cliente.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[#f9f9f9] border-b border-[#e5e7eb]">
|
||||
<tr>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Progetto</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Valore</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Acconto</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Saldo</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Timer</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">€/h</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projects.map((project) => (
|
||||
<ProjectRow key={project.id} project={project} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**B. Creare src/app/admin/projects/new/page.tsx**
|
||||
|
||||
Form che permette di creare un progetto scegliendo il cliente da una select. Il form si sottomette con createProject e redirige al progetto appena creato.
|
||||
|
||||
```typescript
|
||||
import { getAllClientsWithPayments } from "@/lib/admin-queries";
|
||||
import { createProject } from "@/app/admin/projects/project-actions";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function NewProjectPage() {
|
||||
const clients = await getAllClientsWithPayments();
|
||||
const activeClients = clients.filter((c) => !c.archived);
|
||||
|
||||
async function handleCreate(fd: FormData) {
|
||||
"use server";
|
||||
const result = await createProject(fd);
|
||||
redirect(`/admin/projects/${result.projectId}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-[#1a1a1a]">Nuovo Progetto</h1>
|
||||
<p className="text-sm text-[#71717a] mt-1">Crea un nuovo progetto per un cliente esistente.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
|
||||
<form action={handleCreate} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-[#1a1a1a] mb-1">
|
||||
Nome Progetto (Brand)
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="es. Brand Blu"
|
||||
className="w-full border border-[#e5e7eb] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="client_id" className="block text-sm font-medium text-[#1a1a1a] mb-1">
|
||||
Cliente
|
||||
</label>
|
||||
<select
|
||||
id="client_id"
|
||||
name="client_id"
|
||||
required
|
||||
className="w-full border border-[#e5e7eb] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20"
|
||||
>
|
||||
<option value="">Seleziona cliente...</option>
|
||||
{activeClients.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-[#1A463C] text-white py-2 rounded-lg text-sm font-medium hover:bg-[#1A463C]/90 transition-colors"
|
||||
>
|
||||
Crea Progetto
|
||||
</button>
|
||||
<a
|
||||
href="/admin/projects"
|
||||
className="flex-1 text-center border border-[#e5e7eb] py-2 rounded-lg text-sm text-[#71717a] hover:bg-[#f9f9f9] transition-colors"
|
||||
>
|
||||
Annulla
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**C. Riscrivere src/app/admin/clients/[id]/page.tsx**
|
||||
|
||||
Questo file va RISCRITTO per mostrare project cards invece del workspace tab. Leggere il file corrente per capire gli import e adattarli.
|
||||
|
||||
Il nuovo file deve:
|
||||
1. Chiamare `getClientWithProjects(id)` invece di `getClientFullDetail(id)`
|
||||
2. Mostrare le cards dei progetti con link a /admin/projects/[id]
|
||||
3. Mostrare un bottone "+ Nuovo Progetto" che naviga a /admin/projects/new?client_id=[id]
|
||||
4. Mantenere i link di edit e archivio cliente (ClientActions component se esiste, altrimenti link semplici)
|
||||
|
||||
```typescript
|
||||
import { notFound } from "next/navigation";
|
||||
import { getClientWithProjects } from "@/lib/admin-queries";
|
||||
import Link from "next/link";
|
||||
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function ClientDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const data = await getClientWithProjects(id);
|
||||
if (!data) notFound();
|
||||
|
||||
const { projects, ...client } = data;
|
||||
const activeProjects = projects.filter((p) => !p.archived);
|
||||
const archivedProjects = projects.filter((p) => p.archived);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Link href="/admin" className="text-sm text-[#71717a] hover:text-[#1a1a1a]">
|
||||
← Clienti
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex items-start justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[#1a1a1a]">{client.name}</h1>
|
||||
<p className="text-sm text-[#71717a]">{client.brand_name}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
href={`/admin/projects/new?client_id=${id}`}
|
||||
className="text-sm bg-[#1A463C] text-white px-4 py-2 rounded-lg hover:bg-[#1A463C]/90 transition-colors"
|
||||
>
|
||||
+ Nuovo Progetto
|
||||
</Link>
|
||||
<Link
|
||||
href={`/admin/clients/${id}/edit`}
|
||||
className="text-sm border border-[#e5e7eb] px-4 py-2 rounded-lg text-[#71717a] hover:bg-[#f9f9f9] transition-colors"
|
||||
>
|
||||
Modifica Cliente
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeProjects.length === 0 && (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-12 text-center">
|
||||
<p className="text-[#71717a] mb-4">Nessun progetto ancora per questo cliente.</p>
|
||||
<Link
|
||||
href={`/admin/projects/new?client_id=${id}`}
|
||||
className="text-sm text-[#1A463C] hover:underline"
|
||||
>
|
||||
+ Crea il primo progetto
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeProjects.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{activeProjects.map((project) => (
|
||||
<Link
|
||||
key={project.id}
|
||||
href={`/admin/projects/${project.id}`}
|
||||
className="bg-white border border-[#e5e7eb] rounded-xl p-5 hover:shadow-md hover:border-[#1A463C]/20 transition-all"
|
||||
>
|
||||
<h3 className="font-bold text-[#1a1a1a] mb-1">{project.name}</h3>
|
||||
<p className="text-sm text-[#71717a]">
|
||||
{project.accepted_total && parseFloat(project.accepted_total) > 0
|
||||
? `€${parseFloat(project.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}`
|
||||
: "Preventivo non impostato"}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{archivedProjects.length > 0 && (
|
||||
<div className="mt-8">
|
||||
<p className="text-xs text-[#71717a] font-semibold uppercase tracking-wider mb-3">
|
||||
Archiviati ({archivedProjects.length})
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 opacity-60">
|
||||
{archivedProjects.map((project) => (
|
||||
<Link
|
||||
key={project.id}
|
||||
href={`/admin/projects/${project.id}`}
|
||||
className="bg-white border border-[#e5e7eb] rounded-xl p-5 hover:shadow-md transition-all"
|
||||
>
|
||||
<h3 className="font-bold text-[#1a1a1a] mb-1">{project.name}</h3>
|
||||
<p className="text-xs text-[#71717a]">Archiviato</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
NOTA: Se il file corrente ha altri import (ClientActions, tabs, ecc.) che non servono più, rimuoverli per evitare TS errors.
|
||||
|
||||
**D. Aggiornare /admin/projects/new per gestire il query param client_id**
|
||||
|
||||
Il link "+ Nuovo Progetto" da /admin/clients/[id] passa `?client_id=[id]`. Aggiornare la NewProjectPage per pre-selezionare il cliente se il param è presente:
|
||||
|
||||
```typescript
|
||||
// Aggiungere searchParams alle props:
|
||||
export default async function NewProjectPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ client_id?: string }>;
|
||||
}) {
|
||||
const { client_id } = await searchParams;
|
||||
// ...
|
||||
// Nella select, aggiungere defaultValue o usare selected su ogni option:
|
||||
// <option key={c.id} value={c.id} selected={c.id === client_id}>{c.name}</option>
|
||||
}
|
||||
```
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>npm run build 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- src/app/admin/projects/page.tsx exists e contains `getAllProjectsWithPayments` (grep)
|
||||
- src/app/admin/projects/page.tsx contains `ProjectRow` (grep)
|
||||
- src/app/admin/projects/new/page.tsx exists e contains `createProject` (grep)
|
||||
- src/app/admin/clients/[id]/page.tsx contains `getClientWithProjects` (grep)
|
||||
- src/app/admin/clients/[id]/page.tsx contains `href={\`/admin/projects/${` (grep — link alle cards progetto)
|
||||
- src/app/admin/clients/[id]/page.tsx does NOT contain `getClientFullDetail` (grep — vecchia funzione rimossa)
|
||||
- `npm run build` completa senza errori TypeScript
|
||||
- Accedendo a /admin/projects (dopo `npm run dev`) la pagina carica senza 500 error
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>/admin/projects funzionale con lista e form creazione; /admin/clients/[id] mostra project cards</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Admin browser → Server Actions | createProject, archiveProject, updateProjectAcceptedTotal chiamati da form con requireAdmin() |
|
||||
| Admin → /admin/projects/[id] | Link navigazione — il workspace progetto (04-03) avrà il suo guard; questo piano non espone dati sensibili |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-05 | Elevation of Privilege | createProject server action | mitigate | `requireAdmin()` all'inizio di ogni server action — verifica sessione Auth.js prima di qualsiasi DB write |
|
||||
| T-04-06 | Tampering | archiveProject / updateProjectAcceptedTotal | mitigate | `requireAdmin()` guarda entrambe le actions; projectId viene da path param (non da query string non validata) |
|
||||
| T-04-07 | Information Disclosure | /admin/clients/[id] project cards | accept | Dati mostrati sono solo nome progetto e accepted_total — nessun dato sensibile (quote_items mai esposti) |
|
||||
| T-04-08 | Tampering | createProject con client_id da form | mitigate | Action verifica che il client_id esista nel DB prima di inserire — previene inserimento di progetti orfani su client_id inventato |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
# 1. NavBar has new links
|
||||
grep "admin/projects\|admin/impostazioni" src/components/admin/NavBar.tsx
|
||||
|
||||
# 2. ProjectRow exists and has formula
|
||||
grep "totalTrackedSeconds / 3600" src/components/admin/ProjectRow.tsx
|
||||
|
||||
# 3. Server actions have requireAdmin
|
||||
grep "requireAdmin" src/app/admin/projects/project-actions.ts
|
||||
|
||||
# 4. Client detail uses new query
|
||||
grep "getClientWithProjects" src/app/admin/clients/\[id\]/page.tsx
|
||||
|
||||
# 5. Build clean
|
||||
npm run build
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- /admin/projects mostra tabella vuota (o con dati se il seed ha creato progetti) senza errori
|
||||
- /admin/projects/new mostra form con select clienti
|
||||
- /admin/clients/[id] mostra grid cards progetti con bottone "+ Nuovo Progetto"
|
||||
- Cliccando una card naviga a /admin/projects/[id] (che mostra 404 finché 04-03 non crea la pagina)
|
||||
- `npm run build` passa senza errori TypeScript
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-progetti-multi-project/04-02-SUMMARY.md` following the template at `@/Users/simonecavalli/.claude/get-shit-done/templates/summary.md`.
|
||||
|
||||
Key items to document:
|
||||
- Nuovi file creati e loro funzione
|
||||
- Come viene passato il client_id pre-selezionato nel form nuovo progetto
|
||||
- Eventuali componenti legacy rimossi da clients/[id]/page.tsx
|
||||
</output>
|
||||
Reference in New Issue
Block a user