7af917fe80
- Fix button contrast: add all missing shadcn tokens (primary-foreground, ring, input, muted, destructive) aligned to iamcavalli brand - NavBar: #1A463C green bar with white text - Login page: clean brand layout with iamcavalli wordmark - Admin pages: brand colors on headings, borders, links - Admin ClientRow: semantic payment badges (green/yellow/red) - Admin phases tab: Lista ↔ Kanban toggle with @dnd-kit drag & drop between Da fare / In corso / Fatto columns (optimistic updates) - Client dashboard: Timeline ↔ Kanban toggle, expandable task cards with approve button + comment form inline Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
189 lines
5.8 KiB
TypeScript
189 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import type { ClientView } from "@/lib/client-view";
|
|
import type { Comment } from "@/db/schema";
|
|
import { ApproveButton } from "@/components/client/ApproveButton";
|
|
import { CommentList } from "@/components/client/CommentList";
|
|
import { CommentForm } from "@/components/client/CommentForm";
|
|
|
|
type Task = ClientView["phases"][number]["tasks"][number] & {
|
|
phaseTitle: string;
|
|
};
|
|
|
|
const COLUMNS: { id: "todo" | "in_progress" | "done"; label: string; dotClass: string; headerClass: string }[] = [
|
|
{
|
|
id: "todo",
|
|
label: "Da fare",
|
|
dotClass: "bg-[#d4d4d8]",
|
|
headerClass: "text-[#71717a]",
|
|
},
|
|
{
|
|
id: "in_progress",
|
|
label: "In corso",
|
|
dotClass: "bg-[#DEF168]",
|
|
headerClass: "text-[#1A463C]",
|
|
},
|
|
{
|
|
id: "done",
|
|
label: "Fatto",
|
|
dotClass: "bg-[#1A463C]",
|
|
headerClass: "text-[#1A463C]",
|
|
},
|
|
];
|
|
|
|
function TaskCard({
|
|
task,
|
|
token,
|
|
comments,
|
|
}: {
|
|
task: Task;
|
|
token: string;
|
|
comments: Comment[];
|
|
}) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const taskComments = comments.filter((c) => c.entity_id === task.id);
|
|
const hasDeliverables = task.deliverables.length > 0;
|
|
const hasActivity = taskComments.length > 0 || hasDeliverables;
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg border border-[#e5e7eb] overflow-hidden shadow-sm">
|
|
<button
|
|
onClick={() => setExpanded((v) => !v)}
|
|
className="w-full text-left px-3 py-2.5 hover:bg-[#f9f9f9] transition-colors"
|
|
>
|
|
<p className="text-[10px] font-medium text-[#71717a] uppercase tracking-wide mb-1 truncate">
|
|
{task.phaseTitle}
|
|
</p>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p
|
|
className={`text-sm font-medium leading-snug ${
|
|
task.status === "done"
|
|
? "line-through text-[#71717a]"
|
|
: "text-[#1a1a1a]"
|
|
}`}
|
|
>
|
|
{task.title}
|
|
</p>
|
|
{hasActivity && (
|
|
<span className="text-[10px] text-[#71717a] mt-0.5 shrink-0">
|
|
{expanded ? "▲" : "▼"}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{task.description && !expanded && (
|
|
<p className="text-xs text-[#71717a] mt-0.5 leading-snug line-clamp-2">
|
|
{task.description}
|
|
</p>
|
|
)}
|
|
</button>
|
|
|
|
{expanded && (
|
|
<div className="border-t border-[#f4f4f5] px-3 pb-3 pt-2 space-y-3">
|
|
{task.description && (
|
|
<p className="text-xs text-[#71717a] leading-snug">{task.description}</p>
|
|
)}
|
|
|
|
{hasDeliverables && (
|
|
<div className="space-y-2">
|
|
{task.deliverables.map((d) => {
|
|
const delivComments = comments.filter((c) => c.entity_id === d.id);
|
|
return (
|
|
<div
|
|
key={d.id}
|
|
className="bg-[#f9f9f9] rounded-lg px-3 py-2 space-y-2"
|
|
>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="text-xs font-medium text-[#1a1a1a] truncate">
|
|
{d.title}
|
|
</span>
|
|
{(d.status === "pending" ||
|
|
d.status === "submitted" ||
|
|
d.approved_at !== null) && (
|
|
<ApproveButton
|
|
deliverableId={d.id}
|
|
token={token}
|
|
approvedAt={d.approved_at}
|
|
/>
|
|
)}
|
|
</div>
|
|
<CommentList comments={delivComments} />
|
|
<CommentForm
|
|
token={token}
|
|
entityType="deliverable"
|
|
entityId={d.id}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<CommentList comments={taskComments} />
|
|
<CommentForm token={token} entityType="task" entityId={task.id} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ClientKanban({
|
|
phases,
|
|
token,
|
|
comments,
|
|
}: {
|
|
phases: ClientView["phases"];
|
|
token: string;
|
|
comments: Comment[];
|
|
}) {
|
|
const allTasks: Task[] = phases.flatMap((phase) =>
|
|
phase.tasks.map((task) => ({ ...task, phaseTitle: phase.title }))
|
|
);
|
|
|
|
const tasksByStatus = {
|
|
todo: allTasks.filter((t) => t.status === "todo"),
|
|
in_progress: allTasks.filter((t) => t.status === "in_progress"),
|
|
done: allTasks.filter((t) => t.status === "done"),
|
|
};
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
{COLUMNS.map((col) => (
|
|
<div
|
|
key={col.id}
|
|
className="flex flex-col rounded-xl border border-[#e5e7eb] bg-[#f9f9f9] min-h-[200px]"
|
|
>
|
|
<div
|
|
className={`px-4 py-3 flex items-center justify-between ${col.headerClass}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className={`w-2 h-2 rounded-full ${col.dotClass}`} />
|
|
<span className="text-xs font-bold uppercase tracking-wider">
|
|
{col.label}
|
|
</span>
|
|
</div>
|
|
<span className="text-xs font-semibold tabular-nums bg-white rounded-full px-2 py-0.5 border border-[#e5e7eb]">
|
|
{tasksByStatus[col.id].length}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex-1 p-3 space-y-2">
|
|
{tasksByStatus[col.id].map((task) => (
|
|
<TaskCard
|
|
key={task.id}
|
|
task={task}
|
|
token={token}
|
|
comments={comments}
|
|
/>
|
|
))}
|
|
{tasksByStatus[col.id].length === 0 && (
|
|
<p className="text-xs text-[#d4d4d8] italic text-center py-10">
|
|
Nessun task
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
} |