new temporal folder to test
This commit is contained in:
252
frontend-web-free/src/pages/Schedule.jsx
Normal file
252
frontend-web-free/src/pages/Schedule.jsx
Normal file
@@ -0,0 +1,252 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user