160 lines
6.5 KiB
JavaScript
160 lines
6.5 KiB
JavaScript
import React, { useState } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Clock, MapPin, Users, DollarSign, UserPlus } from "lucide-react";
|
|
import SmartAssignModal from "./SmartAssignModal";
|
|
import AssignedStaffManager from "./AssignedStaffManager";
|
|
|
|
const convertTo12Hour = (time24) => {
|
|
if (!time24 || time24 === "—") return time24;
|
|
|
|
try {
|
|
const parts = time24.split(':');
|
|
if (!parts || parts.length < 2) return time24;
|
|
|
|
const hours = parseInt(parts[0], 10);
|
|
const minutes = parseInt(parts[1], 10);
|
|
|
|
if (isNaN(hours) || isNaN(minutes)) return time24;
|
|
|
|
const period = hours >= 12 ? 'PM' : 'AM';
|
|
const hours12 = hours % 12 || 12;
|
|
const minutesStr = minutes.toString().padStart(2, '0');
|
|
|
|
return `${hours12}:${minutesStr} ${period}`;
|
|
} catch (error) {
|
|
console.error('Error converting time:', error);
|
|
return time24;
|
|
}
|
|
};
|
|
|
|
export default function ShiftCard({ shift, event }) {
|
|
const [assignModal, setAssignModal] = useState({ open: false, role: null });
|
|
|
|
const roles = shift?.roles || [];
|
|
|
|
return (
|
|
<>
|
|
<Card className="bg-white border-2 border-slate-200 shadow-sm">
|
|
<CardHeader className="border-b border-slate-100 bg-slate-50">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-lg font-bold text-slate-900">
|
|
{shift.shift_name || "Shift"}
|
|
</CardTitle>
|
|
{shift.location && (
|
|
<div className="flex items-center gap-2 text-sm text-slate-600 mt-1">
|
|
<MapPin className="w-4 h-4" />
|
|
{shift.location}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Badge className="bg-[#0A39DF] text-white font-semibold px-3 py-1.5">
|
|
{roles.length} Role{roles.length !== 1 ? 's' : ''}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="p-6">
|
|
<div className="space-y-4">
|
|
{roles.map((role, idx) => {
|
|
const requiredCount = role.count || 1;
|
|
const assignedCount = event?.assigned_staff?.filter(s => s.role === role.role)?.length || 0;
|
|
const remainingCount = Math.max(requiredCount - assignedCount, 0);
|
|
|
|
// Consistent status color logic
|
|
const statusColor = remainingCount === 0
|
|
? "bg-green-100 text-green-700 border-green-300"
|
|
: assignedCount > 0
|
|
? "bg-blue-100 text-blue-700 border-blue-300"
|
|
: "bg-slate-100 text-slate-700 border-slate-300";
|
|
|
|
return (
|
|
<div
|
|
key={idx}
|
|
className="border-2 border-slate-200 rounded-xl p-4 hover:shadow-sm transition-shadow bg-white"
|
|
>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h4 className="font-bold text-slate-900 text-lg">{role.role}</h4>
|
|
<Badge className={`${statusColor} border-2 font-bold px-3 py-1`}>
|
|
{assignedCount} / {requiredCount} Assigned
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 text-sm text-slate-600">
|
|
{role.start_time && role.end_time && (
|
|
<span className="flex items-center gap-1.5">
|
|
<Clock className="w-4 h-4" />
|
|
{convertTo12Hour(role.start_time)} - {convertTo12Hour(role.end_time)}
|
|
</span>
|
|
)}
|
|
{role.department && (
|
|
<Badge variant="outline" className="text-xs border-slate-300">
|
|
{role.department}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{remainingCount > 0 && (
|
|
<Button
|
|
onClick={() => setAssignModal({ open: true, role })}
|
|
className="bg-[#0A39DF] hover:bg-blue-700 gap-2 font-semibold"
|
|
>
|
|
<UserPlus className="w-4 h-4" />
|
|
Assign Staff ({remainingCount} needed)
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Show assigned staff */}
|
|
{assignedCount > 0 && (
|
|
<div className="border-t border-slate-200 pt-4 mt-4">
|
|
<p className="text-xs font-bold text-slate-700 mb-3 uppercase tracking-wide">
|
|
Assigned Staff
|
|
</p>
|
|
<AssignedStaffManager event={event} shift={shift} role={role} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Additional role details */}
|
|
{(role.uniform || role.cost_per_hour) && (
|
|
<div className="grid grid-cols-2 gap-4 mt-4 pt-4 border-t border-slate-200">
|
|
{role.uniform && (
|
|
<div>
|
|
<p className="text-xs text-slate-500">Uniform</p>
|
|
<p className="text-sm font-medium text-slate-900">{role.uniform}</p>
|
|
</div>
|
|
)}
|
|
{role.cost_per_hour && (
|
|
<div className="flex items-center gap-2">
|
|
<DollarSign className="w-4 h-4 text-[#0A39DF]" />
|
|
<div>
|
|
<p className="text-xs text-slate-500">Rate</p>
|
|
<p className="text-sm font-bold text-slate-900">${role.cost_per_hour}/hr</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Smart Assignment Modal */}
|
|
<SmartAssignModal
|
|
open={assignModal.open}
|
|
onClose={() => setAssignModal({ open: false, role: null })}
|
|
event={event}
|
|
shift={shift}
|
|
role={assignModal.role}
|
|
/>
|
|
</>
|
|
);
|
|
} |