"use client"; import Link from "next/link"; import { format, addDays, startOfWeek, differenceInDays, parseISO } from "date-fns"; import { CalendarDays, AlertTriangle, Clock, Users, ExternalLink } from "lucide-react"; import { cn } from "@/lib/utils"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { getInitials } from "@/lib/utils"; import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard"; import { TaskStatus, TaskType } from "@prisma/client"; function toDate(val: string | null | undefined): Date | null { if (!val) return null; try { return parseISO(val); } catch { return new Date(val); } } interface ScheduleTask { id: string; title: string; type: string; status: string; priority: string; dueDate: string | null; estimatedHours: number | null; scheduledStartDate: string | null; scheduledEndDate: string | null; assignedArtistId: string | null; assignedArtist: { id: string; name: string | null; email: string; image: string | null; } | null; shot: { shotCode: string } | null; asset: { assetCode: string } | null; project: { id: string; name: string; code: string }; } interface ScheduleWidgetsProps { scheduledTasks: ScheduleTask[]; artists: { id: string; name: string | null; email: string; image: string | null; role: string; }[]; } function calcArtistLoad( tasks: ScheduleTask[], artistId: string, weekStart: Date ): number { const weekEnd = addDays(weekStart, 6); const artistTasks = tasks.filter((t) => t.assignedArtistId === artistId); let totalHours = 0; for (const task of artistTasks) { const start = toDate(task.scheduledStartDate); const end = toDate(task.scheduledEndDate) ?? start; if (!start || !end) continue; // Check overlap with week if (start > weekEnd || end < weekStart) continue; const dur = Math.max(1, differenceInDays(end, start) + 1); const hoursPerDay = (task.estimatedHours ?? 8) / dur; // Count days in this week let daysInWeek = 0; for (let i = 0; i < 7; i++) { const day = addDays(weekStart, i); if (day >= start && day <= end) daysInWeek++; } totalHours += hoursPerDay * daysInWeek; } return totalHours; } export function ScheduleWidgets({ scheduledTasks, artists, }: ScheduleWidgetsProps) { const today = new Date(); const weekStart = startOfWeek(today, { weekStartsOn: 1 }); const weekEnd = addDays(weekStart, 6); // Tasks due this week const dueThisWeek = scheduledTasks.filter((t) => { const due = toDate(t.dueDate); return due && due >= today && due <= weekEnd && t.status !== "DONE"; }); // Overloaded artists (> 40h this week = > 8h/day avg) const artistLoads = artists.map((a) => ({ artist: a, hours: calcArtistLoad(scheduledTasks, a.id, weekStart), })); const overloaded = artistLoads.filter((al) => al.hours > 40); // Upcoming reviews const upcomingReviews = scheduledTasks.filter( (t) => t.status === "INTERNAL_REVIEW" || t.status === "CLIENT_REVIEW" ); return (
{/* Due This Week */}
Due This Week
{dueThisWeek.length}
{dueThisWeek.length === 0 ? (
No tasks due this week
) : ( dueThisWeek.slice(0, 6).map((task) => { const code = task.shot?.shotCode ?? task.asset?.assetCode; const cfg = TASK_STATUS_CONFIG[task.status as TaskStatus]; const StatusIcon = cfg.icon; return (
{code && ( {code} )} {task.title}
c.startsWith("text-")) )} > {cfg.label} {task.dueDate && ( {format(toDate(task.dueDate)!, "EEE, MMM d")} )}
{task.assignedArtist && ( {getInitials( task.assignedArtist.name ?? task.assignedArtist.email )} )} ); }) )}
{/* Artist Utilization */}
This Week
Schedule
{artistLoads.length === 0 ? (
No scheduled work this week
) : ( artistLoads .filter((al) => al.hours > 0) .sort((a, b) => b.hours - a.hours) .slice(0, 6) .map(({ artist, hours }) => { const pct = Math.min(100, (hours / 40) * 100); const isOver = hours > 40; return (
{getInitials(artist.name ?? artist.email)} {artist.name ?? artist.email.split("@")[0]} {Math.round(hours)}h {isOver && " ⚠"}
); }) )}
{/* Upcoming Reviews */}
In Review
{upcomingReviews.length}
{upcomingReviews.length === 0 ? (
Nothing in review
) : ( upcomingReviews.slice(0, 6).map((task) => { const code = task.shot?.shotCode ?? task.asset?.assetCode; const isClient = task.status === "CLIENT_REVIEW"; return (
{code && ( {code} )} {task.title}
{isClient ? "Client Review" : "Internal Review"}
); }) )}
); }