Initial commit
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import { useDroppable, useDraggable } from "@dnd-kit/core";
|
||||
import { format, parseISO, formatDistanceToNow } from "date-fns";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
CalendarDays,
|
||||
Clock,
|
||||
GripVertical,
|
||||
Layers,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { TaskStatus, TaskType } from "@prisma/client";
|
||||
import {
|
||||
ScheduleTask,
|
||||
ActiveDragData,
|
||||
} from "@/app/(dashboard)/schedule/SchedulePageClient";
|
||||
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
|
||||
|
||||
const PRIORITY_DOT: Record<string, string> = {
|
||||
LOW: "bg-zinc-500",
|
||||
NORMAL: "bg-blue-500",
|
||||
HIGH: "bg-amber-500",
|
||||
URGENT: "bg-red-500",
|
||||
};
|
||||
|
||||
function toDate(val: string | null | undefined): Date | null {
|
||||
if (!val) return null;
|
||||
try {
|
||||
return parseISO(val);
|
||||
} catch {
|
||||
return new Date(val);
|
||||
}
|
||||
}
|
||||
|
||||
function BacklogItem({
|
||||
task,
|
||||
canEdit,
|
||||
}: {
|
||||
task: ScheduleTask;
|
||||
canEdit: boolean;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
||||
useDraggable({
|
||||
id: task.id,
|
||||
disabled: !canEdit,
|
||||
data: {
|
||||
type: "backlog",
|
||||
taskId: task.id,
|
||||
estimatedHours: task.estimatedHours,
|
||||
} as ActiveDragData,
|
||||
});
|
||||
|
||||
const cfg = TASK_STATUS_CONFIG[task.status];
|
||||
const StatusIcon = cfg.icon;
|
||||
const contextCode = task.shot?.shotCode ?? task.asset?.assetCode;
|
||||
const dueDate = toDate(task.dueDate);
|
||||
const isOverdue =
|
||||
dueDate && dueDate < new Date() && task.status !== "DONE";
|
||||
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
zIndex: 50,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
"group flex items-start gap-2 px-3 py-2.5 rounded-lg border transition-colors",
|
||||
"border-zinc-800 bg-zinc-900 hover:border-zinc-700 hover:bg-zinc-800/80",
|
||||
isDragging && "opacity-30 border-amber-500/40",
|
||||
canEdit && "cursor-grab active:cursor-grabbing"
|
||||
)}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
>
|
||||
{/* Grip */}
|
||||
{canEdit && (
|
||||
<GripVertical className="h-3.5 w-3.5 text-zinc-600 shrink-0 mt-0.5 group-hover:text-zinc-400" />
|
||||
)}
|
||||
|
||||
{/* Priority dot */}
|
||||
<div
|
||||
className={cn(
|
||||
"w-1.5 h-1.5 rounded-full shrink-0 mt-1.5",
|
||||
PRIORITY_DOT[task.priority] ?? "bg-zinc-500"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0 space-y-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{contextCode && (
|
||||
<span className="text-[10px] font-mono bg-zinc-800 text-zinc-400 px-1.5 py-0.5 rounded shrink-0">
|
||||
{contextCode}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs font-medium text-zinc-200 truncate">
|
||||
{task.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-[10px] text-zinc-500">
|
||||
{TASK_TYPE_LABELS[task.type as TaskType]} · {task.project.code}
|
||||
</span>
|
||||
{task.estimatedHours && (
|
||||
<span className="flex items-center gap-0.5 text-[10px] text-zinc-600">
|
||||
<Clock className="h-2.5 w-2.5" />
|
||||
{task.estimatedHours}h
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
className={cn(
|
||||
"text-[9px] border px-1 py-0 h-3.5 gap-0.5 shrink-0",
|
||||
cfg.color
|
||||
)}
|
||||
>
|
||||
<StatusIcon className="h-2 w-2" />
|
||||
{cfg.label}
|
||||
</Badge>
|
||||
{dueDate && (
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center gap-0.5 text-[10px]",
|
||||
isOverdue ? "text-red-400" : "text-zinc-600"
|
||||
)}
|
||||
>
|
||||
<CalendarDays className="h-2.5 w-2.5" />
|
||||
{formatDistanceToNow(dueDate, { addSuffix: true })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface BacklogPanelProps {
|
||||
tasks: ScheduleTask[];
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
export function BacklogPanel({ tasks, canEdit }: BacklogPanelProps) {
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: "backlog-drop-zone",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col border-l border-zinc-800 bg-zinc-900 transition-colors",
|
||||
"w-72 shrink-0"
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2.5 border-b border-zinc-800 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-zinc-400" />
|
||||
<span className="text-sm font-medium text-zinc-200">Backlog</span>
|
||||
<span className="text-xs text-zinc-500 bg-zinc-800 px-1.5 py-0.5 rounded-full">
|
||||
{tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<span className="text-[10px] text-zinc-600">
|
||||
Drag onto timeline
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Drop zone + list */}
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
"flex-1 transition-colors",
|
||||
isOver && "bg-amber-500/5 ring-1 ring-inset ring-amber-500/20"
|
||||
)}
|
||||
>
|
||||
{tasks.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-zinc-600">
|
||||
<Layers className="h-8 w-8 mb-2 opacity-30" />
|
||||
<p className="text-xs">All tasks scheduled</p>
|
||||
{canEdit && (
|
||||
<p className="text-[10px] mt-1 text-zinc-700">
|
||||
Drop here to unschedule
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-2 space-y-1.5">
|
||||
{canEdit && isOver && (
|
||||
<div className="flex items-center justify-center py-3 rounded-lg border border-dashed border-amber-500/40 text-amber-400 text-xs">
|
||||
Drop to unschedule
|
||||
</div>
|
||||
)}
|
||||
{tasks.map((task) => (
|
||||
<BacklogItem key={task.id} task={task} canEdit={canEdit} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user