export base44 - Nov 18
This commit is contained in:
@@ -1,84 +1,160 @@
|
||||
import React from "react";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
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 { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { MapPin, Plus } from "lucide-react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Clock, MapPin, Users, DollarSign, UserPlus } from "lucide-react";
|
||||
import SmartAssignModal from "./SmartAssignModal";
|
||||
import AssignedStaffManager from "./AssignedStaffManager";
|
||||
|
||||
export default function ShiftCard({ shift, onNotifyStaff }) {
|
||||
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="border-slate-200">
|
||||
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-100 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base font-semibold">{shift.shift_name || "Shift 1"}</CardTitle>
|
||||
<Button onClick={onNotifyStaff} className="bg-blue-600 hover:bg-blue-700 text-white text-sm">
|
||||
Notify Staff
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-start gap-6 mt-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-2">Managers:</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{shift.assigned_staff?.slice(0, 3).map((staff, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Avatar className="w-8 h-8 bg-slate-300">
|
||||
<AvatarFallback className="text-xs">{staff.staff_name?.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-xs">
|
||||
<p className="font-medium">{staff.staff_name}</p>
|
||||
<p className="text-slate-500">{staff.position || "john@email.com"}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-xs">
|
||||
<MapPin className="w-4 h-4 text-slate-400 mt-0.5" />
|
||||
<>
|
||||
<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>
|
||||
<p className="font-medium">Location:</p>
|
||||
<p className="text-slate-600">{shift.location || "848 East Glen Road New York CA, USA"}</p>
|
||||
<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>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50 hover:bg-slate-50">
|
||||
<TableHead className="text-xs">Unpaid break</TableHead>
|
||||
<TableHead className="text-xs">Count</TableHead>
|
||||
<TableHead className="text-xs">Assigned</TableHead>
|
||||
<TableHead className="text-xs">Uniform type</TableHead>
|
||||
<TableHead className="text-xs">Price</TableHead>
|
||||
<TableHead className="text-xs">Amount</TableHead>
|
||||
<TableHead className="text-xs">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(shift.assigned_staff || []).length > 0 ? (
|
||||
shift.assigned_staff.map((staff, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="text-xs">{shift.unpaid_break || 0}</TableCell>
|
||||
<TableCell className="text-xs">1</TableCell>
|
||||
<TableCell className="text-xs">0</TableCell>
|
||||
<TableCell className="text-xs">{shift.uniform_type || "uniform type"}</TableCell>
|
||||
<TableCell className="text-xs">${shift.price || 23}</TableCell>
|
||||
<TableCell className="text-xs">{shift.amount || 120}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="sm" className="text-xs">⋮</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-4 text-slate-500 text-xs">
|
||||
No staff assigned yet
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user