feat: sidebar CTA removal, delete phase/task, public dashboard UX, chat panel
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user