Files
Krow-workspace/frontend-web-free/src/pages/Schedule.jsx
2025-12-04 18:02:28 -05:00

252 lines
11 KiB
JavaScript

import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { ChevronLeft, ChevronRight, Plus, Clock, DollarSign, Calendar as CalendarIcon } from "lucide-react";
import { format, startOfWeek, addDays, isSameDay, addWeeks, subWeeks, isToday, parseISO } from "date-fns";
const safeParseDate = (dateString) => {
if (!dateString) return null;
try {
if (typeof dateString === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
const [year, month, day] = dateString.split('-').map(Number);
return new Date(year, month - 1, day);
}
return parseISO(dateString);
} catch {
return null;
}
};
export default function Schedule() {
const navigate = useNavigate();
const [currentWeek, setCurrentWeek] = useState(startOfWeek(new Date(), { weekStartsOn: 0 }));
const { data: events = [] } = useQuery({
queryKey: ['events'],
queryFn: () => base44.entities.Event.list('-date'),
initialData: [],
});
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(currentWeek, i));
const getEventsForDay = (date) => {
return events.filter(event => {
const eventDate = safeParseDate(event.date);
return eventDate && isSameDay(eventDate, date);
});
};
const calculateWeekMetrics = () => {
const weekEvents = events.filter(event => {
const eventDate = safeParseDate(event.date);
if (!eventDate) return false;
return weekDays.some(day => isSameDay(eventDate, day));
});
const totalHours = weekEvents.reduce((sum, event) => {
const hours = event.shifts?.reduce((shiftSum, shift) => {
return shiftSum + (shift.roles?.reduce((roleSum, role) => roleSum + (role.hours || 0), 0) || 0);
}, 0) || 0;
return sum + hours;
}, 0);
const totalCost = weekEvents.reduce((sum, event) => sum + (event.total || 0), 0);
const totalShifts = weekEvents.reduce((sum, event) => sum + (event.shifts?.length || 0), 0);
return { totalHours, totalCost, totalShifts };
};
const metrics = calculateWeekMetrics();
const goToPreviousWeek = () => setCurrentWeek(subWeeks(currentWeek, 1));
const goToNextWeek = () => setCurrentWeek(addWeeks(currentWeek, 1));
const goToToday = () => setCurrentWeek(startOfWeek(new Date(), { weekStartsOn: 0 }));
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
<div className="max-w-[1800px] mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-slate-900">Schedule</h1>
<p className="text-sm text-slate-500 mt-1">Plan and manage staff shifts</p>
</div>
<Button
onClick={() => navigate(createPageUrl('CreateEvent'))}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
<Plus className="w-4 h-4 mr-2" />
New Shift
</Button>
</div>
{/* Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card className="border border-blue-200 bg-blue-50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-blue-600 font-medium">Week Total Hours</p>
<p className="text-4xl font-bold text-blue-900 mt-2">{metrics.totalHours.toFixed(1)}</p>
</div>
<Clock className="w-10 h-10 text-blue-400" />
</div>
</CardContent>
</Card>
<Card className="border border-green-200 bg-green-50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-green-600 font-medium">Week Labor Cost</p>
<p className="text-4xl font-bold text-green-900 mt-2">${metrics.totalCost.toLocaleString()}</p>
</div>
<DollarSign className="w-10 h-10 text-green-400" />
</div>
</CardContent>
</Card>
<Card className="border border-teal-200 bg-teal-50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-teal-600 font-medium">Total Shifts</p>
<p className="text-4xl font-bold text-teal-900 mt-2">{metrics.totalShifts}</p>
</div>
<CalendarIcon className="w-10 h-10 text-teal-400" />
</div>
</CardContent>
</Card>
</div>
{/* Week Navigation */}
<Card className="mb-6">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<Button variant="ghost" size="icon" onClick={goToPreviousWeek}>
<ChevronLeft className="w-5 h-5" />
</Button>
<div className="text-center">
<p className="text-sm text-slate-500">Week of</p>
<p className="text-lg font-bold text-slate-900">{format(currentWeek, 'MMM d, yyyy')}</p>
</div>
<Button variant="ghost" size="icon" onClick={goToNextWeek}>
<ChevronRight className="w-5 h-5" />
</Button>
<Button variant="outline" onClick={goToToday}>
Today
</Button>
</div>
</CardContent>
</Card>
{/* Weekly Calendar */}
<div className="grid grid-cols-7 gap-3">
{weekDays.map((day, index) => {
const dayEvents = getEventsForDay(day);
const isTodayDay = isToday(day);
return (
<Card
key={index}
className={`${isTodayDay ? 'bg-gradient-to-br from-blue-500 to-teal-500 text-white border-blue-600' : 'bg-white border-slate-200'}`}
>
<CardContent className="p-4">
{/* Day Header */}
<div className="text-center mb-4">
<p className={`text-xs font-medium ${isTodayDay ? 'text-white/80' : 'text-slate-500'}`}>
{format(day, 'EEE')}
</p>
<p className={`text-2xl font-bold ${isTodayDay ? 'text-white' : 'text-slate-900'}`}>
{format(day, 'd')}
</p>
<p className={`text-xs ${isTodayDay ? 'text-white/80' : 'text-slate-500'}`}>
{format(day, 'MMM')}
</p>
</div>
{/* Add Shift Button */}
<Button
variant={isTodayDay ? "secondary" : "outline"}
size="sm"
className={`w-full mb-4 ${isTodayDay ? 'bg-white/20 hover:bg-white/30 text-white border-white/40' : ''}`}
onClick={() => navigate(createPageUrl('CreateEvent'))}
>
<Plus className="w-3 h-3 mr-1" />
Add Shift
</Button>
{/* Events List */}
<div className="space-y-2">
{dayEvents.length === 0 ? (
<p className={`text-xs text-center ${isTodayDay ? 'text-white/70' : 'text-slate-400'}`}>
No shifts
</p>
) : (
dayEvents.map((event) => {
const firstShift = event.shifts?.[0];
const firstRole = firstShift?.roles?.[0];
const firstStaff = event.assigned_staff?.[0];
return (
<div
key={event.id}
onClick={() => navigate(createPageUrl(`EventDetail?id=${event.id}`))}
className={`p-3 rounded cursor-pointer transition-all ${
isTodayDay
? 'bg-white/20 hover:bg-white/30 border border-white/40'
: 'bg-white hover:bg-slate-50 border border-slate-200 shadow-sm'
}`}
>
{/* Status Badges */}
<div className="flex gap-1 mb-2 flex-wrap">
{firstRole?.role && (
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 text-[10px] font-medium rounded">
{firstRole.role}
</span>
)}
<span className="px-2 py-0.5 bg-green-100 text-green-700 text-[10px] font-medium rounded">
{event.status || 'scheduled'}
</span>
</div>
{/* Staff Member */}
{firstStaff && (
<p className={`text-xs font-semibold mb-1 flex items-center gap-1 ${isTodayDay ? 'text-white' : 'text-slate-900'}`}>
<span className="text-[10px]">👤</span>
{firstStaff.staff_name}
</p>
)}
{/* Time */}
{firstRole && (firstRole.start_time || firstRole.end_time) && (
<p className={`text-[10px] mb-1 flex items-center gap-1 ${isTodayDay ? 'text-white/80' : 'text-slate-500'}`}>
<Clock className="w-3 h-3" />
{firstRole.start_time || '00:00'} - {firstRole.end_time || '00:00'}
</p>
)}
{/* Cost */}
{event.total > 0 && (
<p className={`text-xs font-bold mt-2 ${isTodayDay ? 'text-white' : 'text-slate-900'}`}>
${event.total.toFixed(2)}
</p>
)}
</div>
);
})
)}
</div>
</CardContent>
</Card>
);
})}
</div>
</div>
</div>
);
}