Initial commit
This commit is contained in:
@@ -0,0 +1,592 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback, useMemo } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
DragStartEvent,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
addDays,
|
||||
startOfWeek,
|
||||
startOfDay,
|
||||
format,
|
||||
differenceInDays,
|
||||
isBefore,
|
||||
parseISO,
|
||||
} from "date-fns";
|
||||
import { scheduleTask, unscheduleTask } from "@/actions/schedule";
|
||||
import { ScheduleTimeline } from "@/components/schedule/ScheduleTimeline";
|
||||
import { BacklogPanel } from "@/components/schedule/BacklogPanel";
|
||||
import { ScheduleFilters } from "@/components/schedule/ScheduleFilters";
|
||||
import { CalendarDays, PanelRightOpen, PanelRightClose, Columns2, Rows } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { TaskStatus, TaskType } from "@prisma/client";
|
||||
|
||||
export const DAY_WIDTH = 216;
|
||||
export const ROW_HEIGHT = 56;
|
||||
export const HEADER_HEIGHT = 44;
|
||||
export const LABEL_WIDTH = 208;
|
||||
export const NUM_DAYS = 35;
|
||||
export const SLOT_HEIGHT = 48; // task height = ROW_HEIGHT - 8
|
||||
|
||||
export interface ScheduleTask {
|
||||
id: string;
|
||||
title: string;
|
||||
type: TaskType;
|
||||
status: TaskStatus;
|
||||
priority: string;
|
||||
dueDate: string | null;
|
||||
estimatedHours: number | null;
|
||||
scheduledStartDate: string | null;
|
||||
scheduledEndDate: string | null;
|
||||
scheduleNotes: string | null;
|
||||
assignedArtistId: string | null;
|
||||
assignedArtist: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
image: string | null;
|
||||
} | null;
|
||||
shot: { id: string; shotCode: string; thumbnailUrl: string | null } | null;
|
||||
asset: { id: string; assetCode: string; name: string } | null;
|
||||
project: { id: string; name: string; code: string };
|
||||
}
|
||||
|
||||
export interface ScheduleArtist {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
image: string | null;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface ActiveDragData {
|
||||
type: "scheduled" | "backlog" | "resize";
|
||||
taskId: string;
|
||||
duration?: number;
|
||||
estimatedHours?: number | null;
|
||||
originalEndDate?: string;
|
||||
}
|
||||
|
||||
interface SchedulePageClientProps {
|
||||
artists: ScheduleArtist[];
|
||||
tasks: ScheduleTask[];
|
||||
backlog: ScheduleTask[];
|
||||
projects: { id: string; name: string; code: string }[];
|
||||
canEdit: boolean;
|
||||
currentUserId: string;
|
||||
activeProject?: string;
|
||||
activeArtist?: string;
|
||||
}
|
||||
|
||||
function toDate(val: string | null | undefined): Date | null {
|
||||
if (!val) return null;
|
||||
try {
|
||||
return parseISO(val);
|
||||
} catch {
|
||||
return new Date(val);
|
||||
}
|
||||
}
|
||||
|
||||
function calcDuration(task: ScheduleTask): number {
|
||||
return Math.max(1, Math.ceil((task.estimatedHours ?? 8) / 8));
|
||||
}
|
||||
|
||||
export function SchedulePageClient({
|
||||
artists,
|
||||
tasks,
|
||||
backlog,
|
||||
projects,
|
||||
canEdit,
|
||||
currentUserId,
|
||||
activeProject,
|
||||
activeArtist,
|
||||
}: SchedulePageClientProps) {
|
||||
const { toast } = useToast();
|
||||
const [localTasks, setLocalTasks] = useState<ScheduleTask[]>(tasks);
|
||||
const [localBacklog, setLocalBacklog] = useState<ScheduleTask[]>(backlog);
|
||||
const [viewStart, setViewStart] = useState<Date>(() =>
|
||||
startOfWeek(new Date(), { weekStartsOn: 1 })
|
||||
);
|
||||
const [showBacklog, setShowBacklog] = useState(true);
|
||||
const [filterProject, setFilterProject] = useState(activeProject ?? "");
|
||||
const [filterArtist, setFilterArtist] = useState(activeArtist ?? "");
|
||||
const [filterStatus, setFilterStatus] = useState("");
|
||||
const [activeDrag, setActiveDrag] = useState<ActiveDragData | null>(null);
|
||||
const [resizePreview, setResizePreview] = useState<{
|
||||
taskId: string;
|
||||
endDate: Date;
|
||||
estimatedHours: number;
|
||||
} | null>(null);
|
||||
const [dayWidth, setDayWidth] = useState(DAY_WIDTH);
|
||||
const [rowHeight, setRowHeight] = useState(ROW_HEIGHT);
|
||||
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 6 },
|
||||
})
|
||||
);
|
||||
|
||||
const days = useMemo(
|
||||
() => Array.from({ length: NUM_DAYS }, (_, i) => addDays(viewStart, i)),
|
||||
[viewStart]
|
||||
);
|
||||
|
||||
const filteredArtists = useMemo(() => {
|
||||
if (!filterArtist) return artists;
|
||||
return artists.filter((a) => a.id === filterArtist);
|
||||
}, [artists, filterArtist]);
|
||||
|
||||
const filteredScheduledTasks = useMemo(() => {
|
||||
return localTasks.filter((t) => {
|
||||
if (filterProject && t.project.id !== filterProject) return false;
|
||||
if (filterArtist && t.assignedArtistId !== filterArtist) return false;
|
||||
if (filterStatus && t.status !== filterStatus) return false;
|
||||
return true;
|
||||
});
|
||||
}, [localTasks, filterProject, filterArtist, filterStatus]);
|
||||
|
||||
const filteredBacklog = useMemo(() => {
|
||||
return localBacklog.filter((t) => {
|
||||
if (filterProject && t.project.id !== filterProject) return false;
|
||||
if (filterArtist && t.assignedArtistId !== filterArtist) return false;
|
||||
if (filterStatus && t.status !== filterStatus) return false;
|
||||
return true;
|
||||
});
|
||||
}, [localBacklog, filterProject, filterArtist, filterStatus]);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(event: DragStartEvent) => {
|
||||
const data = event.active.data.current as ActiveDragData;
|
||||
setActiveDrag(data);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
setActiveDrag(null);
|
||||
const { active, delta, over } = event;
|
||||
if (!active.data.current) return;
|
||||
|
||||
const dragData = active.data.current as ActiveDragData;
|
||||
|
||||
// Resize handle - only deltaX matters
|
||||
if (dragData.type === "resize") {
|
||||
const task = localTasks.find((t) => t.id === dragData.taskId);
|
||||
if (!task?.scheduledStartDate || !dragData.originalEndDate) return;
|
||||
|
||||
const originalEnd = toDate(dragData.originalEndDate)!;
|
||||
const daysDelta = Math.round(delta.x / dayWidth);
|
||||
let newEnd = addDays(originalEnd, daysDelta);
|
||||
|
||||
const startDate = toDate(task.scheduledStartDate)!;
|
||||
if (isBefore(newEnd, startDate)) newEnd = startDate;
|
||||
|
||||
const prevEnd = task.scheduledEndDate;
|
||||
setLocalTasks((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === dragData.taskId
|
||||
? { ...t, scheduledEndDate: newEnd.toISOString() }
|
||||
: t
|
||||
)
|
||||
);
|
||||
|
||||
scheduleTask({
|
||||
taskId: dragData.taskId,
|
||||
scheduledStartDate: task.scheduledStartDate,
|
||||
scheduledEndDate: newEnd.toISOString(),
|
||||
}).catch(() => {
|
||||
setLocalTasks((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === dragData.taskId
|
||||
? { ...t, scheduledEndDate: prevEnd }
|
||||
: t
|
||||
)
|
||||
);
|
||||
toast({ title: "Failed to resize task", variant: "destructive" });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if dropped over the backlog panel (unschedule)
|
||||
if (over?.id === "backlog-drop-zone" && dragData.type === "scheduled") {
|
||||
const task = localTasks.find((t) => t.id === dragData.taskId);
|
||||
if (!task) return;
|
||||
|
||||
setLocalTasks((prev) => prev.filter((t) => t.id !== dragData.taskId));
|
||||
setLocalBacklog((prev) => [
|
||||
{
|
||||
...task,
|
||||
scheduledStartDate: null,
|
||||
scheduledEndDate: null,
|
||||
},
|
||||
...prev,
|
||||
]);
|
||||
|
||||
unscheduleTask(dragData.taskId).catch(() => {
|
||||
setLocalTasks((prev) => [...prev, task]);
|
||||
setLocalBacklog((prev) =>
|
||||
prev.filter((t) => t.id !== dragData.taskId)
|
||||
);
|
||||
toast({ title: "Failed to unschedule task", variant: "destructive" });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop on timeline - calculate position from translated rect
|
||||
const translatedRect = active.rect.current.translated;
|
||||
if (!translatedRect || !timelineRef.current) return;
|
||||
|
||||
const timelineBounds = timelineRef.current.getBoundingClientRect();
|
||||
const scrollLeft = timelineRef.current.scrollLeft;
|
||||
|
||||
const centerX = translatedRect.left + translatedRect.width / 2;
|
||||
const centerY = translatedRect.top + translatedRect.height / 2;
|
||||
|
||||
// Check if drop is within timeline bounds
|
||||
const inTimeline =
|
||||
centerX >= timelineBounds.left &&
|
||||
centerX <= timelineBounds.right &&
|
||||
centerY >= timelineBounds.top + HEADER_HEIGHT &&
|
||||
centerY <= timelineBounds.bottom;
|
||||
|
||||
if (!inTimeline && dragData.type !== "backlog") return;
|
||||
if (!inTimeline && dragData.type === "backlog") return;
|
||||
|
||||
const dayIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
Math.floor((centerX - timelineBounds.left + scrollLeft) / dayWidth),
|
||||
NUM_DAYS - 1
|
||||
)
|
||||
);
|
||||
const artistIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
Math.floor(
|
||||
(centerY - timelineBounds.top - HEADER_HEIGHT) / rowHeight
|
||||
),
|
||||
filteredArtists.length - 1
|
||||
)
|
||||
);
|
||||
|
||||
const newArtist = filteredArtists[artistIndex];
|
||||
if (!newArtist) return;
|
||||
|
||||
const taskId = dragData.taskId;
|
||||
|
||||
// Duration always derived from estimatedHours so width = hours
|
||||
let duration = 1;
|
||||
if (dragData.type === "scheduled") {
|
||||
const task = localTasks.find((t) => t.id === taskId);
|
||||
if (task) duration = calcDuration(task);
|
||||
} else if (dragData.type === "backlog") {
|
||||
const task = localBacklog.find((t) => t.id === taskId);
|
||||
if (task) duration = calcDuration(task);
|
||||
}
|
||||
|
||||
// Pack task within the dropped day using hour offsets.
|
||||
// Sum hours already scheduled for this artist on that day,
|
||||
// and start the new task at that hour (stored in scheduledStartDate's time component).
|
||||
const existingForArtist = localTasks.filter(
|
||||
(t) =>
|
||||
t.assignedArtistId === newArtist.id &&
|
||||
t.id !== taskId &&
|
||||
t.scheduledStartDate
|
||||
);
|
||||
|
||||
const droppedDay = startOfDay(addDays(viewStart, dayIndex));
|
||||
const hoursOnDay = existingForArtist
|
||||
.filter((t) => {
|
||||
const tStart = toDate(t.scheduledStartDate!);
|
||||
return tStart && differenceInDays(startOfDay(tStart), droppedDay) === 0;
|
||||
})
|
||||
.reduce((sum, t) => sum + (t.estimatedHours ?? 8), 0);
|
||||
|
||||
const newStartDate = new Date(droppedDay);
|
||||
newStartDate.setHours(hoursOnDay);
|
||||
const newEndDate = addDays(newStartDate, duration - 1);
|
||||
|
||||
if (dragData.type === "backlog") {
|
||||
const task = localBacklog.find((t) => t.id === taskId);
|
||||
if (!task) return;
|
||||
|
||||
const scheduledTask: ScheduleTask = {
|
||||
...task,
|
||||
scheduledStartDate: newStartDate.toISOString(),
|
||||
scheduledEndDate: newEndDate.toISOString(),
|
||||
assignedArtistId: newArtist.id,
|
||||
assignedArtist: {
|
||||
id: newArtist.id,
|
||||
name: newArtist.name,
|
||||
email: newArtist.email,
|
||||
image: newArtist.image,
|
||||
},
|
||||
};
|
||||
|
||||
setLocalBacklog((prev) => prev.filter((t) => t.id !== taskId));
|
||||
setLocalTasks((prev) => [...prev, scheduledTask]);
|
||||
|
||||
scheduleTask({
|
||||
taskId,
|
||||
scheduledStartDate: newStartDate.toISOString(),
|
||||
scheduledEndDate: newEndDate.toISOString(),
|
||||
assignedArtistId: newArtist.id,
|
||||
}).catch(() => {
|
||||
setLocalTasks((prev) => prev.filter((t) => t.id !== taskId));
|
||||
setLocalBacklog((prev) => [task, ...prev]);
|
||||
toast({ title: "Failed to schedule task", variant: "destructive" });
|
||||
});
|
||||
} else if (dragData.type === "scheduled") {
|
||||
const prevTask = localTasks.find((t) => t.id === taskId);
|
||||
|
||||
setLocalTasks((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === taskId
|
||||
? {
|
||||
...t,
|
||||
scheduledStartDate: newStartDate.toISOString(),
|
||||
scheduledEndDate: newEndDate.toISOString(),
|
||||
assignedArtistId: newArtist.id,
|
||||
assignedArtist: {
|
||||
id: newArtist.id,
|
||||
name: newArtist.name,
|
||||
email: newArtist.email,
|
||||
image: newArtist.image,
|
||||
},
|
||||
}
|
||||
: t
|
||||
)
|
||||
);
|
||||
|
||||
scheduleTask({
|
||||
taskId,
|
||||
scheduledStartDate: newStartDate.toISOString(),
|
||||
scheduledEndDate: newEndDate.toISOString(),
|
||||
assignedArtistId: newArtist.id,
|
||||
}).catch(() => {
|
||||
if (prevTask) {
|
||||
setLocalTasks((prev) =>
|
||||
prev.map((t) => (t.id === taskId ? prevTask : t))
|
||||
);
|
||||
}
|
||||
toast({ title: "Failed to move task", variant: "destructive" });
|
||||
});
|
||||
}
|
||||
},
|
||||
[localTasks, localBacklog, viewStart, filteredArtists, toast, dayWidth, rowHeight]
|
||||
);
|
||||
|
||||
const handleResizeMouseDown = useCallback(
|
||||
(taskId: string, _currentEndDate: string) =>
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const prevTask = localTasks.find((t) => t.id === taskId);
|
||||
if (!prevTask?.scheduledStartDate) return;
|
||||
|
||||
const startX = e.clientX;
|
||||
const startDate = toDate(prevTask.scheduledStartDate)!;
|
||||
const originalHours = prevTask.estimatedHours ?? 8;
|
||||
const HOUR_WIDTH = dayWidth / 8; // px per 1 hour
|
||||
let currentHours = originalHours;
|
||||
|
||||
const onMouseMove = (me: MouseEvent) => {
|
||||
const deltaPx = me.clientX - startX;
|
||||
const hoursDelta = Math.round(deltaPx / HOUR_WIDTH);
|
||||
currentHours = Math.max(1, originalHours + hoursDelta);
|
||||
const newDays = Math.max(1, Math.ceil(currentHours / 8));
|
||||
const newEndDate = addDays(startDate, newDays - 1);
|
||||
setResizePreview({
|
||||
taskId,
|
||||
endDate: newEndDate,
|
||||
estimatedHours: currentHours,
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
|
||||
const newDays = Math.max(1, Math.ceil(currentHours / 8));
|
||||
const finalEnd = addDays(startDate, newDays - 1);
|
||||
|
||||
setResizePreview(null);
|
||||
setLocalTasks((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === taskId
|
||||
? {
|
||||
...t,
|
||||
scheduledEndDate: finalEnd.toISOString(),
|
||||
estimatedHours: currentHours,
|
||||
}
|
||||
: t
|
||||
)
|
||||
);
|
||||
|
||||
scheduleTask({
|
||||
taskId,
|
||||
scheduledStartDate: prevTask.scheduledStartDate,
|
||||
scheduledEndDate: finalEnd.toISOString(),
|
||||
estimatedHours: currentHours,
|
||||
}).catch(() => {
|
||||
setLocalTasks((prev) =>
|
||||
prev.map((t) => (t.id === taskId ? prevTask : t))
|
||||
);
|
||||
toast({ title: "Failed to resize task", variant: "destructive" });
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
},
|
||||
[localTasks, toast, dayWidth]
|
||||
);
|
||||
|
||||
const activeDragTask = activeDrag
|
||||
? localTasks.find((t) => t.id === activeDrag.taskId) ??
|
||||
localBacklog.find((t) => t.id === activeDrag.taskId)
|
||||
: null;
|
||||
|
||||
const handleUnschedule = useCallback(
|
||||
(taskId: string) => {
|
||||
const task = localTasks.find((t) => t.id === taskId);
|
||||
if (!task) return;
|
||||
setLocalTasks((prev) => prev.filter((t) => t.id !== taskId));
|
||||
setLocalBacklog((prev) => [
|
||||
{ ...task, scheduledStartDate: null, scheduledEndDate: null },
|
||||
...prev,
|
||||
]);
|
||||
unscheduleTask(taskId).catch(() => {
|
||||
setLocalTasks((prev) => [...prev, task]);
|
||||
setLocalBacklog((prev) => prev.filter((t) => t.id !== taskId));
|
||||
toast({ title: "Failed to unschedule task", variant: "destructive" });
|
||||
});
|
||||
},
|
||||
[localTasks, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="flex flex-col h-full overflow-hidden bg-zinc-950">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-3 border-b border-zinc-800 bg-zinc-900 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarDays className="h-5 w-5 text-amber-400" />
|
||||
<h1 className="text-lg font-semibold text-white">Schedule</h1>
|
||||
<span className="text-sm text-zinc-500">
|
||||
{format(viewStart, "MMM d")} —{" "}
|
||||
{format(addDays(viewStart, NUM_DAYS - 1), "MMM d, yyyy")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Zoom controls */}
|
||||
<div className="flex items-center gap-3 border border-zinc-700 rounded-md px-2.5 py-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Columns2 className="h-3 w-3 text-zinc-500" />
|
||||
<input
|
||||
type="range"
|
||||
min={60}
|
||||
max={400}
|
||||
step={8}
|
||||
value={dayWidth}
|
||||
onChange={(e) => setDayWidth(Number(e.target.value))}
|
||||
className="w-20 accent-amber-400 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Rows className="h-3 w-3 text-zinc-500" />
|
||||
<input
|
||||
type="range"
|
||||
min={40}
|
||||
max={120}
|
||||
step={4}
|
||||
value={rowHeight}
|
||||
onChange={(e) => setRowHeight(Number(e.target.value))}
|
||||
className="w-20 accent-amber-400 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowBacklog((b) => !b)}
|
||||
className="text-zinc-400 hover:text-white gap-2"
|
||||
>
|
||||
{showBacklog ? (
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
) : (
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
)}
|
||||
{showBacklog ? "Hide" : "Backlog"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<ScheduleFilters
|
||||
projects={projects}
|
||||
artists={artists}
|
||||
filterProject={filterProject}
|
||||
filterArtist={filterArtist}
|
||||
filterStatus={filterStatus}
|
||||
viewStart={viewStart}
|
||||
onProjectChange={setFilterProject}
|
||||
onArtistChange={setFilterArtist}
|
||||
onStatusChange={setFilterStatus}
|
||||
onViewStartChange={setViewStart}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<ScheduleTimeline
|
||||
artists={filteredArtists}
|
||||
tasks={filteredScheduledTasks}
|
||||
days={days}
|
||||
viewStart={viewStart}
|
||||
canEdit={canEdit}
|
||||
timelineRef={timelineRef}
|
||||
resizePreview={resizePreview}
|
||||
onResizeMouseDown={handleResizeMouseDown}
|
||||
activeDragId={activeDrag?.taskId ?? null}
|
||||
onUnschedule={handleUnschedule}
|
||||
dayWidth={dayWidth}
|
||||
rowHeight={rowHeight}
|
||||
/>
|
||||
|
||||
{showBacklog && (
|
||||
<BacklogPanel
|
||||
tasks={filteredBacklog}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Drag overlay */}
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{activeDragTask && (
|
||||
<div className="rounded-md border border-amber-500/60 bg-amber-500/20 px-2 py-1.5 text-xs font-medium text-amber-200 shadow-xl opacity-90 max-w-[200px] truncate pointer-events-none">
|
||||
{activeDragTask.shot?.shotCode ??
|
||||
activeDragTask.asset?.assetCode ??
|
||||
activeDragTask.title}
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</div>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { redirect } from "next/navigation";
|
||||
import { SchedulePageClient } from "./SchedulePageClient";
|
||||
|
||||
export const metadata = { title: "Schedule" };
|
||||
|
||||
export default async function SchedulePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ project?: string; artist?: string; status?: string }>;
|
||||
}) {
|
||||
const session = await auth();
|
||||
if (!session?.user) redirect("/login");
|
||||
if (session.user.role === "CLIENT") redirect("/dashboard");
|
||||
|
||||
const { project, artist, status } = await searchParams;
|
||||
|
||||
const [artists, scheduledTasks, backlogTasks, projects] = await Promise.all([
|
||||
db.user.findMany({
|
||||
where: { isActive: true, role: { not: "CLIENT" } },
|
||||
select: { id: true, name: true, email: true, image: true, role: true },
|
||||
orderBy: [{ role: "asc" }, { name: "asc" }],
|
||||
}),
|
||||
|
||||
db.task.findMany({
|
||||
where: {
|
||||
scheduledStartDate: { not: null },
|
||||
status: { not: "DONE" },
|
||||
...(project ? { projectId: project } : {}),
|
||||
...(artist ? { assignedArtistId: artist } : {}),
|
||||
},
|
||||
include: {
|
||||
shot: { select: { id: true, shotCode: true, thumbnailUrl: true } },
|
||||
asset: { select: { id: true, assetCode: true, name: true } },
|
||||
project: { select: { id: true, name: true, code: true } },
|
||||
assignedArtist: {
|
||||
select: { id: true, name: true, email: true, image: true },
|
||||
},
|
||||
},
|
||||
orderBy: { scheduledStartDate: "asc" },
|
||||
}),
|
||||
|
||||
db.task.findMany({
|
||||
where: {
|
||||
scheduledStartDate: null,
|
||||
status: { not: "DONE" },
|
||||
...(project ? { projectId: project } : {}),
|
||||
...(artist ? { assignedArtistId: artist } : {}),
|
||||
},
|
||||
include: {
|
||||
shot: { select: { id: true, shotCode: true, thumbnailUrl: true } },
|
||||
asset: { select: { id: true, assetCode: true, name: true } },
|
||||
project: { select: { id: true, name: true, code: true } },
|
||||
assignedArtist: {
|
||||
select: { id: true, name: true, email: true, image: true },
|
||||
},
|
||||
},
|
||||
orderBy: [{ dueDate: "asc" }, { priority: "desc" }],
|
||||
}),
|
||||
|
||||
db.project.findMany({
|
||||
where: { status: "ACTIVE" },
|
||||
select: { id: true, name: true, code: true },
|
||||
orderBy: { name: "asc" },
|
||||
}),
|
||||
]);
|
||||
|
||||
const canEdit = ["ADMIN", "PRODUCER", "SUPERVISOR"].includes(
|
||||
session.user.role
|
||||
);
|
||||
|
||||
const serializeTask = (t: (typeof scheduledTasks)[number]) => ({
|
||||
...t,
|
||||
dueDate: t.dueDate ? t.dueDate.toISOString() : null,
|
||||
scheduledStartDate: t.scheduledStartDate ? t.scheduledStartDate.toISOString() : null,
|
||||
scheduledEndDate: t.scheduledEndDate ? t.scheduledEndDate.toISOString() : null,
|
||||
});
|
||||
|
||||
return (
|
||||
<SchedulePageClient
|
||||
artists={artists}
|
||||
tasks={scheduledTasks.map(serializeTask)}
|
||||
backlog={backlogTasks.map(serializeTask)}
|
||||
projects={projects}
|
||||
canEdit={canEdit}
|
||||
currentUserId={session.user.id}
|
||||
activeProject={project}
|
||||
activeArtist={artist}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user