From b010dd4ff1925294a1704270c0901ad7fd16c57b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Dec 2025 11:48:35 +0000 Subject: [PATCH] Improve capacity matrix performance with lazy loading and better UX - Add beautiful skeleton loading placeholders for initial page load - Add individual cell skeletons during data refresh - Create click-to-manage dialog for easy schedule adjustment per day - Add day navigation (prev/next) within the schedule dialog - Memoize components and data lookups for better performance - Replace full page reload with React Query cache invalidation - Add staleTime to queries for better caching - Use conditional query fetching (operations only loaded as fallback) - Add hover effects and click hints for better UX --- .../capacity/CapacityMatrixSkeleton.tsx | 103 +++++ .../capacity/CellScheduleDialog.tsx | 382 ++++++++++++++++++ .../scheduler/AutoScheduleButton.tsx | 12 +- src/pages/admin/CapacityMatrix.tsx | 355 ++++++++++------ 4 files changed, 724 insertions(+), 128 deletions(-) create mode 100644 src/components/capacity/CapacityMatrixSkeleton.tsx create mode 100644 src/components/capacity/CellScheduleDialog.tsx diff --git a/src/components/capacity/CapacityMatrixSkeleton.tsx b/src/components/capacity/CapacityMatrixSkeleton.tsx new file mode 100644 index 00000000..7f74cc9c --- /dev/null +++ b/src/components/capacity/CapacityMatrixSkeleton.tsx @@ -0,0 +1,103 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; + +interface CapacityMatrixSkeletonProps { + rowCount?: number; + columnCount?: number; +} + +export function CapacityMatrixSkeleton({ + rowCount = 6, + columnCount = 14 +}: CapacityMatrixSkeletonProps) { + return ( +
+ {/* Header skeleton */} +
+
+ + +
+
+ +
+ + + +
+
+
+ + {/* Legend skeleton */} +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ + {/* Table skeleton */} + + + + + + +
+ {/* Header row */} +
+
+ +
+ {Array.from({ length: columnCount }).map((_, i) => ( +
+ + +
+ ))} +
+ + {/* Body rows */} + {Array.from({ length: rowCount }).map((_, rowIndex) => ( +
+
+
+ + +
+ +
+ {Array.from({ length: columnCount }).map((_, colIndex) => ( +
+ +
+ ))} +
+ ))} +
+
+
+
+ ); +} + +// Inline skeleton for individual cells when updating +export function CellSkeleton() { + return ( +
+ + +
+ ); +} diff --git a/src/components/capacity/CellScheduleDialog.tsx b/src/components/capacity/CellScheduleDialog.tsx new file mode 100644 index 00000000..52063b5e --- /dev/null +++ b/src/components/capacity/CellScheduleDialog.tsx @@ -0,0 +1,382 @@ +import { useState, useEffect } from "react"; +import { format, addDays, subDays } from "date-fns"; +import { useQueryClient, useMutation } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useToast } from "@/hooks/use-toast"; +import { useTranslation } from "react-i18next"; +import { + ChevronLeft, + ChevronRight, + Clock, + Trash2, + Loader2, + Plus, + CalendarOff, +} from "lucide-react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface Allocation { + id: string; + operation_id: string; + cell_id: string; + date: string; + hours_allocated: number; + operation?: { + id: string; + operation_name: string | null; + part?: { + part_number: string; + job?: { + job_number: string; + customer: string | null; + }; + }; + }; +} + +interface CellScheduleDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + cell: { + id: string; + name: string; + color: string; + capacity_hours_per_day: number; + } | null; + date: Date | null; + allocations: Allocation[]; + dayInfo: { + type: string; + label: string | null; + multiplier: number; + }; + capacity: number; + totalHours: number; + onDateChange?: (newDate: Date) => void; + startDate: Date; +} + +export function CellScheduleDialog({ + open, + onOpenChange, + cell, + date, + allocations, + dayInfo, + capacity, + totalHours, + onDateChange, + startDate, +}: CellScheduleDialogProps) { + const { t } = useTranslation(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [editingId, setEditingId] = useState(null); + const [editValue, setEditValue] = useState(0); + + // Mutation for updating allocation hours + const updateAllocation = useMutation({ + mutationFn: async ({ id, hours }: { id: string; hours: number }) => { + const { error } = await supabase + .from("operation_day_allocations") + .update({ hours_allocated: hours }) + .eq("id", id); + if (error) throw error; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["day-allocations"] }); + setEditingId(null); + toast({ + title: t("capacity.updated", "Updated"), + description: t("capacity.allocationUpdated", "Allocation hours updated successfully"), + }); + }, + onError: (error: any) => { + toast({ + title: t("common.error", "Error"), + description: error.message, + variant: "destructive", + }); + }, + }); + + // Mutation for deleting allocation + const deleteAllocation = useMutation({ + mutationFn: async (id: string) => { + const { error } = await supabase + .from("operation_day_allocations") + .delete() + .eq("id", id); + if (error) throw error; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["day-allocations"] }); + toast({ + title: t("capacity.deleted", "Deleted"), + description: t("capacity.allocationDeleted", "Allocation removed from this day"), + }); + }, + onError: (error: any) => { + toast({ + title: t("common.error", "Error"), + description: error.message, + variant: "destructive", + }); + }, + }); + + const handlePrevDay = () => { + if (date && onDateChange) { + onDateChange(subDays(date, 1)); + } + }; + + const handleNextDay = () => { + if (date && onDateChange) { + onDateChange(addDays(date, 1)); + } + }; + + const startEdit = (allocation: Allocation) => { + setEditingId(allocation.id); + setEditValue(allocation.hours_allocated); + }; + + const saveEdit = () => { + if (editingId) { + updateAllocation.mutate({ id: editingId, hours: editValue }); + } + }; + + const cancelEdit = () => { + setEditingId(null); + setEditValue(0); + }; + + const isNonWorking = dayInfo.type === "holiday" || dayInfo.type === "closure" || dayInfo.type === "weekend"; + const utilizationPercent = capacity > 0 ? (totalHours / capacity) * 100 : 0; + + const getUtilizationColor = () => { + if (utilizationPercent <= 50) return "bg-green-500"; + if (utilizationPercent <= 80) return "bg-yellow-500"; + if (utilizationPercent <= 100) return "bg-orange-500"; + return "bg-red-500"; + }; + + if (!cell || !date) return null; + + return ( + + + +
+
+
+ {cell.name} +
+ {/* Day navigation */} +
+ + + {format(date, "EEE, MMM d")} + + +
+
+ + {isNonWorking ? ( + + + {dayInfo.label || (dayInfo.type === "weekend" ? "Weekend" : "Non-working day")} + + ) : ( + + + {totalHours.toFixed(1)}h / {capacity}h scheduled + + )} + + + + {!isNonWorking && ( + <> + {/* Capacity bar */} +
+
+ Utilization + {Math.round(utilizationPercent)}% +
+
+
+
+ {utilizationPercent > 100 && ( +

+ Over capacity by {(totalHours - capacity).toFixed(1)}h +

+ )} +
+ + {/* Allocations list */} +
+
+

Scheduled Operations

+ + {allocations.length} operation{allocations.length !== 1 ? "s" : ""} + +
+ + {allocations.length === 0 ? ( +
+ +

No operations scheduled for this day

+

+ Use Auto Schedule or manually assign operations +

+
+ ) : ( + +
+ {allocations.map((allocation) => ( +
+
+
+ + {allocation.operation?.part?.job?.job_number || "—"} + + + {allocation.operation?.operation_name || "Operation"} + +
+
+ {allocation.operation?.part?.part_number || "—"} + {allocation.operation?.part?.job?.customer && ( + <> • {allocation.operation.part.job.customer} + )} +
+
+ +
+ {editingId === allocation.id ? ( +
+ setEditValue(parseFloat(e.target.value) || 0)} + className="w-16 h-8 text-center" + min="0" + step="0.5" + autoFocus + /> + h + + +
+ ) : ( + <> + + + + )} +
+
+ ))} +
+
+ )} +
+ + )} + + {isNonWorking && ( +
+ +

{dayInfo.label || "Non-working Day"}

+

+ No operations can be scheduled on this day +

+
+ )} + +
+ ); +} diff --git a/src/components/scheduler/AutoScheduleButton.tsx b/src/components/scheduler/AutoScheduleButton.tsx index 192ca190..03230827 100644 --- a/src/components/scheduler/AutoScheduleButton.tsx +++ b/src/components/scheduler/AutoScheduleButton.tsx @@ -6,6 +6,7 @@ import { supabase } from "@/integrations/supabase/client"; import { SchedulerService, CalendarDay } from "@/lib/scheduler"; import { useTranslation } from "react-i18next"; import { useAuth } from "@/contexts/AuthContext"; +import { useQueryClient } from "@tanstack/react-query"; import { addMonths, format } from "date-fns"; export function AutoScheduleButton() { @@ -13,6 +14,7 @@ export function AutoScheduleButton() { const { toast } = useToast(); const { t } = useTranslation(); const { tenant } = useAuth(); + const queryClient = useQueryClient(); const handleSchedule = async () => { setLoading(true); @@ -125,8 +127,14 @@ export function AutoScheduleButton() { description: t("capacity.operationsScheduled", { count: updatedCount }), }); - // Reload to show updated data - window.location.reload(); + // Invalidate relevant queries to refresh data without full page reload + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["day-allocations"] }), + queryClient.invalidateQueries({ queryKey: ["operations-capacity"] }), + queryClient.invalidateQueries({ queryKey: ["factory-calendar"] }), + queryClient.invalidateQueries({ queryKey: ["operations"] }), + queryClient.invalidateQueries({ queryKey: ["jobs"] }), + ]); } catch (error: any) { console.error("Scheduling error:", error); diff --git a/src/pages/admin/CapacityMatrix.tsx b/src/pages/admin/CapacityMatrix.tsx index fba720d1..1d24c4a7 100644 --- a/src/pages/admin/CapacityMatrix.tsx +++ b/src/pages/admin/CapacityMatrix.tsx @@ -1,11 +1,11 @@ -import { useState } from "react"; +import { useState, useMemo, useCallback, memo } from "react"; import { useQuery } from "@tanstack/react-query"; import { supabase } from "@/integrations/supabase/client"; import { format, addDays, startOfWeek, isSameDay, parseISO, getDay, isWithinInterval } from "date-fns"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { ChevronLeft, ChevronRight, Loader2, CalendarOff, Sun } from "lucide-react"; +import { ChevronLeft, ChevronRight, CalendarOff } from "lucide-react"; import { AutoScheduleButton } from "@/components/scheduler/AutoScheduleButton"; import { useTranslation } from "react-i18next"; import { useAuth } from "@/contexts/AuthContext"; @@ -15,6 +15,8 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { CapacityMatrixSkeleton, CellSkeleton } from "@/components/capacity/CapacityMatrixSkeleton"; +import { CellScheduleDialog } from "@/components/capacity/CellScheduleDialog"; interface CalendarDay { date: string; @@ -31,25 +33,136 @@ interface DayAllocation { hours_allocated: number; operation?: { id: string; - name: string | null; + operation_name: string | null; part?: { - name: string; + part_number: string; job?: { job_number: string; - customer_name: string | null; + customer: string | null; }; }; }; } +// Memoized cell component for better performance +const CapacityCell = memo(function CapacityCell({ + cellId, + cellName, + date, + hours, + percent, + capacity, + dayInfo, + allocations, + operations, + onClick, +}: { + cellId: string; + cellName: string; + date: Date; + hours: number; + percent: number; + capacity: number; + dayInfo: { type: string; label: string | null; multiplier: number }; + allocations: DayAllocation[]; + operations: any[]; + onClick: () => void; +}) { + const items = allocations.length > 0 ? allocations : operations; + + const getLoadColor = (percent: number, dayType: string) => { + if (dayType === 'holiday' || dayType === 'closure') { + return "bg-gray-200 text-gray-500 border-gray-300"; + } + if (dayType === 'weekend') { + return "bg-gray-100 text-gray-400 border-gray-200"; + } + if (percent === 0) return "bg-gray-50 text-gray-400 border-gray-200"; + if (percent <= 50) return "bg-green-100 text-green-700 border-green-200"; + if (percent <= 80) return "bg-yellow-100 text-yellow-700 border-yellow-200"; + if (percent <= 100) return "bg-orange-100 text-orange-700 border-orange-200"; + return "bg-red-100 text-red-700 border-red-200"; + }; + + return ( + + + +
+ {dayInfo.type === 'holiday' || dayInfo.type === 'closure' ? ( +
+ + Closed +
+ ) : dayInfo.type === 'weekend' ? ( +
+ Weekend +
+ ) : ( + <> + {Math.round(percent)}% +
+ {hours.toFixed(1)}h / {capacity}h +
+ + )} +
+
+ +
+
+ {cellName} - {format(date, "MMM d")} +
+ {dayInfo.label && ( +
{dayInfo.label}
+ )} + {capacity > 0 && ( +
+ {hours.toFixed(1)}h scheduled / {capacity}h capacity +
+ )} + {items.length > 0 && ( +
+
Operations:
+ {items.slice(0, 3).map((item: any, idx: number) => ( +
+ • {item.operation?.part?.job?.job_number || item.part?.job?.job_number || 'Job'}: + {' '}{item.operation?.operation_name || item.name || 'Operation'} + {item.hours_allocated ? ` (${item.hours_allocated}h)` : ''} +
+ ))} + {items.length > 3 && ( +
+ +{items.length - 3} more +
+ )} +
+ )} +
+ Click to manage schedule → +
+
+
+
+ + ); +}); + export default function CapacityMatrix() { const { t } = useTranslation(); const { tenant } = useAuth(); const [startDate, setStartDate] = useState(startOfWeek(new Date(), { weekStartsOn: 1 })); + const [selectedCell, setSelectedCell] = useState(null); + const [selectedDate, setSelectedDate] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); // Working days mask from tenant (default Mon-Fri = 31) const workingDaysMask = (tenant as any)?.working_days_mask ?? 31; + // Fetch cells first (usually cached, fast) const { data: cells, isLoading: cellsLoading } = useQuery({ queryKey: ["cells-capacity"], queryFn: async () => { @@ -60,10 +173,12 @@ export default function CapacityMatrix() { if (error) throw error; return data as any[]; }, + staleTime: 5 * 60 * 1000, // 5 minutes }); + // Fetch calendar data const { data: calendarDays, isLoading: calendarLoading } = useQuery({ - queryKey: ["factory-calendar", startDate], + queryKey: ["factory-calendar", format(startDate, 'yyyy-MM-dd')], queryFn: async () => { const endDate = addDays(startDate, 14); const { data, error } = await supabase @@ -74,10 +189,12 @@ export default function CapacityMatrix() { if (error) throw error; return (data || []) as CalendarDay[]; }, + staleTime: 60 * 1000, // 1 minute }); - const { data: dayAllocations, isLoading: allocationsLoading } = useQuery({ - queryKey: ["day-allocations", startDate], + // Fetch day allocations (main data source) + const { data: dayAllocations, isLoading: allocationsLoading, isFetching: allocationsFetching } = useQuery({ + queryKey: ["day-allocations", format(startDate, 'yyyy-MM-dd')], queryFn: async () => { const endDate = addDays(startDate, 14); const { data, error } = await supabase @@ -96,15 +213,15 @@ export default function CapacityMatrix() { .gte("date", format(startDate, 'yyyy-MM-dd')) .lte("date", format(endDate, 'yyyy-MM-dd')); if (error) throw error; - return (data || []) as any; + return (data || []) as DayAllocation[]; }, + staleTime: 30 * 1000, // 30 seconds }); // Fallback: fetch operations with planned dates if no allocations const { data: operations, isLoading: opsLoading } = useQuery({ - queryKey: ["operations-capacity", startDate], + queryKey: ["operations-capacity", format(startDate, 'yyyy-MM-dd')], queryFn: async () => { - const endDate = addDays(startDate, 14); const { data, error } = await supabase .from("operations") .select(` @@ -115,42 +232,43 @@ export default function CapacityMatrix() { job:jobs(job_number, customer_name) ) `) - .not("status", "eq", "completed") + .neq("status", "completed") .not("planned_start", "is", null); if (error) throw error; return data as any[]; }, + staleTime: 60 * 1000, // 1 minute + enabled: !dayAllocations || dayAllocations.length === 0, // Only fetch if no allocations }); const daysToShow = 14; - const dates = Array.from({ length: daysToShow }, (_, i) => addDays(startDate, i)); + const dates = useMemo( + () => Array.from({ length: daysToShow }, (_, i) => addDays(startDate, i)), + [startDate] + ); // Check if a date is a default working day - const isDefaultWorkingDay = (date: Date): boolean => { + const isDefaultWorkingDay = useCallback((date: Date): boolean => { const jsDay = getDay(date); const maskBits = [64, 1, 2, 4, 8, 16, 32]; return (workingDaysMask & maskBits[jsDay]) !== 0; - }; + }, [workingDaysMask]); - // Get calendar entry for a date - const getCalendarEntry = (date: Date): CalendarDay | null => { - if (!calendarDays) return null; - const dateStr = format(date, 'yyyy-MM-dd'); - return calendarDays.find(d => d.date === dateStr) || null; - }; + // Memoized calendar lookup map + const calendarMap = useMemo(() => { + const map = new Map(); + calendarDays?.forEach(day => map.set(day.date, day)); + return map; + }, [calendarDays]); - // Check if it's a working day - const isWorkingDay = (date: Date): boolean => { - const entry = getCalendarEntry(date); - if (entry) { - return entry.day_type === 'working' || entry.day_type === 'half_day'; - } - return isDefaultWorkingDay(date); - }; + // Get calendar entry for a date + const getCalendarEntry = useCallback((date: Date): CalendarDay | null => { + return calendarMap.get(format(date, 'yyyy-MM-dd')) || null; + }, [calendarMap]); // Get day type and label - const getDayInfo = (date: Date): { type: string; label: string | null; multiplier: number } => { + const getDayInfo = useCallback((date: Date): { type: string; label: string | null; multiplier: number } => { const entry = getCalendarEntry(date); if (entry) { return { @@ -163,17 +281,26 @@ export default function CapacityMatrix() { return { type: 'weekend', label: null, multiplier: 0 }; } return { type: 'working', label: null, multiplier: 1 }; - }; + }, [getCalendarEntry, isDefaultWorkingDay]); + + // Memoized allocations by cell and date + const allocationsByCellDate = useMemo(() => { + const map = new Map(); + dayAllocations?.forEach(a => { + const key = `${a.cell_id}-${a.date}`; + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(a); + }); + return map; + }, [dayAllocations]); // Get allocations for a cell on a date - const getAllocationsForCellDate = (cellId: string, date: Date): DayAllocation[] => { - if (!dayAllocations) return []; - const dateStr = format(date, 'yyyy-MM-dd'); - return dayAllocations.filter(a => a.cell_id === cellId && a.date === dateStr); - }; + const getAllocationsForCellDate = useCallback((cellId: string, date: Date): DayAllocation[] => { + return allocationsByCellDate.get(`${cellId}-${format(date, 'yyyy-MM-dd')}`) || []; + }, [allocationsByCellDate]); // Get operations for a cell on a date (fallback when no allocations) - const getOperationsForCellDate = (cellId: string, date: Date): any[] => { + const getOperationsForCellDate = useCallback((cellId: string, date: Date): any[] => { if (!operations) return []; return operations.filter(op => { if (op.cell_id !== cellId) return false; @@ -186,13 +313,10 @@ export default function CapacityMatrix() { isSameDay(opStart, date) || isSameDay(opEnd, date); }); - }; + }, [operations]); // Calculate load for a cell on a date - const getCellLoad = (cellId: string, date: Date) => { - if (!cells) return { hours: 0, percent: 0, capacity: 0 }; - - const cell = cells.find(c => c.id === cellId); + const getCellLoad = useCallback((cellId: string, date: Date, cell: any) => { if (!cell) return { hours: 0, percent: 0, capacity: 0 }; const dayInfo = getDayInfo(date); @@ -215,32 +339,34 @@ export default function CapacityMatrix() { const percent = capacity > 0 ? (totalHours / capacity) * 100 : 0; return { hours: totalHours, percent, capacity }; - }; + }, [getDayInfo, getAllocationsForCellDate, getOperationsForCellDate]); - const getLoadColor = (percent: number, dayType: string) => { - if (dayType === 'holiday' || dayType === 'closure') { - return "bg-gray-200 text-gray-500 border-gray-300"; - } - if (dayType === 'weekend') { - return "bg-gray-100 text-gray-400 border-gray-200"; - } - if (percent === 0) return "bg-gray-50 text-gray-400"; - if (percent <= 50) return "bg-green-100 text-green-700 border-green-200"; - if (percent <= 80) return "bg-yellow-100 text-yellow-700 border-yellow-200"; - if (percent <= 100) return "bg-orange-100 text-orange-700 border-orange-200"; - return "bg-red-100 text-red-700 border-red-200"; // Over capacity - }; + const handleCellClick = useCallback((cell: any, date: Date) => { + setSelectedCell(cell); + setSelectedDate(date); + setDialogOpen(true); + }, []); - const isLoading = cellsLoading || calendarLoading || allocationsLoading || opsLoading; + const handleDialogDateChange = useCallback((newDate: Date) => { + setSelectedDate(newDate); + }, []); - if (isLoading) { - return ( -
- -
- ); + // Show skeleton during initial load + const isInitialLoading = cellsLoading; + + if (isInitialLoading) { + return ; } + // Get dialog data + const dialogAllocations = selectedCell && selectedDate + ? getAllocationsForCellDate(selectedCell.id, selectedDate) + : []; + const dialogDayInfo = selectedDate ? getDayInfo(selectedDate) : { type: 'working', label: null, multiplier: 1 }; + const dialogLoad = selectedCell && selectedDate + ? getCellLoad(selectedCell.id, selectedDate, selectedCell) + : { hours: 0, percent: 0, capacity: 0 }; + return (
@@ -290,11 +416,11 @@ export default function CapacityMatrix() { {t("capacity.workloadOverview", "Workload Overview")} - Click on a cell to see scheduled operations + Click on a cell to view and manage scheduled operations - + @@ -337,72 +463,35 @@ export default function CapacityMatrix() { {dates.map(date => { - const { hours, percent, capacity } = getCellLoad(cell.id, date); const dayInfo = getDayInfo(date); + + // Show skeleton while allocations are loading/refetching + if (allocationsLoading) { + return ( + + ); + } + + const { hours, percent, capacity } = getCellLoad(cell.id, date, cell); const allocations = getAllocationsForCellDate(cell.id, date); const opsOnDate = getOperationsForCellDate(cell.id, date); - const items = allocations.length > 0 ? allocations : opsOnDate; return ( - + handleCellClick(cell, date)} + /> ); })} @@ -412,6 +501,20 @@ export default function CapacityMatrix() { + + {/* Schedule Dialog */} + ); }
+ + - - -
- {dayInfo.type === 'holiday' || dayInfo.type === 'closure' ? ( -
- - Closed -
- ) : dayInfo.type === 'weekend' ? ( -
- Weekend -
- ) : ( - <> - {Math.round(percent)}% -
- {hours.toFixed(1)}h / {capacity}h -
- - )} -
-
- -
-
- {cell.name} - {format(date, "MMM d")} -
- {dayInfo.label && ( -
{dayInfo.label}
- )} - {capacity > 0 && ( -
- {hours.toFixed(1)}h scheduled / {capacity}h capacity -
- )} - {items.length > 0 && ( -
-
Operations:
- {items.slice(0, 5).map((item: any, idx: number) => ( -
- • {item.operation?.part?.job?.job_number || item.part?.job?.job_number || 'Job'}: - {' '}{item.operation?.name || item.name || 'Operation'} - {item.hours_allocated ? ` (${item.hours_allocated}h)` : ''} -
- ))} - {items.length > 5 && ( -
- +{items.length - 5} more -
- )} -
- )} -
-
-
-