From 64030afef3f49879a883501fb572146d003559c5 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 25 Jun 2026 14:11:55 +0200 Subject: [PATCH] feat: sidebar CTA removal, delete phase/task, public dashboard UX, chat panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. AdminSidebar: removed yellow "Genera preventivo" CTA (duplicated Preventivi tab nav item) 2. Delete phase/task: new deletePhase/deleteTask server actions with FK cascade and phase-status recompute; DeletePhaseTaskButton client component with window.confirm; trash icons in PhasesTab per phase and per task 3. Public dashboard: extend activeOffers query with offer_macros join (offer_name, offer_type); reorder sidebar 1°Offerte 2°Pagamenti 3°Documenti; OffersSection shows macro public_name as heading (never tier letter/internal_name); PaymentStatus gains totalLabel/overrideAmount/hideRows props — retainer=monthly label + no Acconto/Saldo rows 4. Offer editor: Descrizione textarea (nota interna) and Modalità toggle moved directly under title; Tags section now contains only Categoria/Ticket/Tipo/Obiettivo 5. Basecamp chat: API route supports entity_type="phase" with authorization scoping; comments scope in client-view broadened to include phaseIds and client_id (general); CommentsTab updated with phase/general entities; ChatProvider React context; ChatPanel fixed slide-in panel with FAB (bottom-right) and composer tag selector (Generale + phases); PhaseCard gets chat-bubble icon pre-tagging that phase; inline ChatSection block removed from dashboard main column Co-Authored-By: Claude Opus 4.8 --- src/app/admin/clients/[id]/actions.ts | 21 +- src/app/api/client/comment/route.ts | 9 +- src/components/admin/AdminSidebar.tsx | 11 - .../admin/DeletePhaseTaskButton.tsx | 60 ++++ .../admin/offers/OfferEditorClient.tsx | 59 ++-- src/components/admin/tabs/CommentsTab.tsx | 76 ++--- src/components/admin/tabs/PhasesTab.tsx | 117 ++++---- src/components/client-dashboard.tsx | 193 +++++++------ src/components/client/ChatPanel.tsx | 260 ++++++++++++++++++ src/components/client/ChatProvider.tsx | 49 ++++ src/components/client/OffersSection.tsx | 7 +- src/components/client/PhaseCard.tsx | 16 ++ src/components/payment-status.tsx | 93 ++++--- src/lib/client-view.ts | 22 +- 14 files changed, 733 insertions(+), 260 deletions(-) create mode 100644 src/components/admin/DeletePhaseTaskButton.tsx create mode 100644 src/components/client/ChatPanel.tsx create mode 100644 src/components/client/ChatProvider.tsx diff --git a/src/app/admin/clients/[id]/actions.ts b/src/app/admin/clients/[id]/actions.ts index e3b1098..a5b8229 100644 --- a/src/app/admin/clients/[id]/actions.ts +++ b/src/app/admin/clients/[id]/actions.ts @@ -142,6 +142,14 @@ export async function updatePhaseStatus(phaseId: string, id: string, status: str revalidatePath(path); } +export async function deletePhase(phaseId: string, id: string) { + await requireAdmin(); + // FK cascade on tasks and deliverables handles child rows + await db.delete(phases).where(eq(phases.id, phaseId)); + const { path } = await resolveEntity(id); + revalidatePath(path); +} + // ── TASKS ───────────────────────────────────────────────────────────────────── export async function addTask(phaseId: string, id: string, formData: FormData) { @@ -205,6 +213,17 @@ export async function updateTaskStatus(taskId: string, id: string, status: strin revalidatePath(path); } +export async function deleteTask(taskId: string, id: string) { + await requireAdmin(); + // Fetch phase_id before deletion so we can recompute phase status after + const taskRow = await db.select({ phase_id: tasks.phase_id }).from(tasks).where(eq(tasks.id, taskId)).limit(1); + const phaseId = taskRow[0]?.phase_id; + await db.delete(tasks).where(eq(tasks.id, taskId)); + if (phaseId) await recomputePhaseStatus(phaseId); + const { path } = await resolveEntity(id); + revalidatePath(path); +} + // ── DELIVERABLES ────────────────────────────────────────────────────────────── export async function addDeliverable(taskId: string, id: string, formData: FormData) { @@ -337,7 +356,7 @@ export async function postAdminComment(id: string, formData: FormData) { if (!body || !entity) throw new Error("Dati mancanti"); const [entity_type, entity_id] = entity.split(":"); if (!entity_type || !entity_id) throw new Error("Formato entity non valido"); - const allowedTypes = ["task", "deliverable"]; + const allowedTypes = ["task", "deliverable", "phase", "general"]; if (!allowedTypes.includes(entity_type)) throw new Error("entity_type non valido"); await db.insert(comments).values({ entity_type, entity_id, author: "admin", body }); const { path } = await resolveEntity(id); diff --git a/src/app/api/client/comment/route.ts b/src/app/api/client/comment/route.ts index 8fe07c5..1da9984 100644 --- a/src/app/api/client/comment/route.ts +++ b/src/app/api/client/comment/route.ts @@ -7,7 +7,7 @@ import { rateLimit } from "@/lib/rate-limit"; const commentSchema = z.object({ token: z.string().min(1), - entity_type: z.enum(["task", "deliverable", "general"]), + entity_type: z.enum(["task", "deliverable", "general", "phase"]), entity_id: z.string().min(1), body: z.string().min(1, "Il commento non può essere vuoto").max(2000), }); @@ -76,7 +76,12 @@ export async function POST(request: NextRequest) { .from(tasks) .where(inArray(tasks.phase_id, phaseIds)); - if (entity_type === "task") { + if (entity_type === "phase") { + // Phase: entity_id must be one of the client's phases + if (!phasesForClient.find((p) => p.id === entity_id)) { + return NextResponse.json({ error: "Accesso non consentito" }, { status: 403 }); + } + } else if (entity_type === "task") { if (!taskRows.find((r) => r.id === entity_id)) { return NextResponse.json({ error: "Accesso non consentito" }, { status: 403 }); } diff --git a/src/components/admin/AdminSidebar.tsx b/src/components/admin/AdminSidebar.tsx index 49a990d..bac0c67 100644 --- a/src/components/admin/AdminSidebar.tsx +++ b/src/components/admin/AdminSidebar.tsx @@ -39,17 +39,6 @@ export function AdminSidebar() { iamcavalli - {/* CTA globale — genera preventivo */} -
- - - Genera preventivo - -
- {/* Nav links */}