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

427 lines
18 KiB
JavaScript

import React, { useMemo, useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Minus, Trash2, Search, DollarSign, TrendingUp, Check } from "lucide-react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
const DEPARTMENTS = [
"Accounting", "Operations", "Sales", "HR", "Finance",
"IT", "Marketing", "Customer Service", "Logistics"
];
const UNIFORMS = ["Type 1", "Type 2", "Type 3", "Casual", "Formal"];
const TIME_OPTIONS = [];
for (let h = 1; h <= 12; h++) {
for (let m of ['00', '30']) {
TIME_OPTIONS.push(`${h.toString().padStart(2, '0')}:${m} AM`);
}
}
for (let h = 1; h <= 12; h++) {
for (let m of ['00', '30']) {
TIME_OPTIONS.push(`${h.toString().padStart(2, '0')}:${m} PM`);
}
}
export default function ShiftRoleCard({ role, roleIndex, onRoleChange, onDelete, canDelete, selectedVendor }) {
const [roleSearchOpen, setRoleSearchOpen] = useState(false);
// Get current user to check role
const { data: user } = useQuery({
queryKey: ['current-user-shift-role'],
queryFn: () => base44.auth.me(),
});
const userRole = user?.user_role || user?.role;
const isClient = userRole === "client";
// Get client's vendor relationships if client
const { data: clientVendors = [] } = useQuery({
queryKey: ['client-vendors-for-roles', user?.id],
queryFn: async () => {
if (!isClient) return [];
const allEvents = await base44.entities.Event.list();
const clientEvents = allEvents.filter(e =>
e.client_email === user?.email ||
e.business_name === user?.company_name ||
e.created_by === user?.email
);
const vendorNames = new Set();
clientEvents.forEach(event => {
if (event.shifts && Array.isArray(event.shifts)) {
event.shifts.forEach(shift => {
if (shift.roles && Array.isArray(shift.roles)) {
shift.roles.forEach(role => {
if (role.vendor_name) vendorNames.add(role.vendor_name);
});
}
});
}
});
return Array.from(vendorNames);
},
enabled: isClient && !!user,
initialData: [],
});
// Fetch all vendor rates
const { data: allRates = [], isLoading } = useQuery({
queryKey: ['vendor-rates-for-event'],
queryFn: () => base44.entities.VendorRate.list(),
initialData: [],
});
// Filter rates by selected vendor AND client access
const availableRates = useMemo(() => {
if (!allRates || !Array.isArray(allRates)) return [];
let filtered = allRates.filter(r => r && r.is_active);
// If client, only show rates from vendors they work with
if (isClient) {
filtered = filtered.filter(r => {
const hasVendorRelationship = clientVendors.length === 0 || clientVendors.includes(r.vendor_name);
const isVisibleToClient =
r.client_visibility === 'all' ||
(r.client_visibility === 'specific' &&
r.available_to_clients &&
Array.isArray(r.available_to_clients) && // Ensure it's an array before using includes
r.available_to_clients.includes(user?.id));
return hasVendorRelationship && isVisibleToClient;
});
}
// Filter by selected vendor if provided
if (selectedVendor) {
filtered = filtered.filter(r => r.vendor_name === selectedVendor);
}
return filtered;
}, [allRates, selectedVendor, isClient, clientVendors, user?.id]);
// Group rates by category
const ratesByCategory = useMemo(() => {
if (!availableRates || !Array.isArray(availableRates)) return {};
return availableRates.reduce((acc, rate) => {
if (!rate || !rate.category) return acc;
if (!acc[rate.category]) acc[rate.category] = [];
acc[rate.category].push(rate);
return acc;
}, {});
}, [availableRates]);
// Handle role selection from vendor rates
const handleRoleSelect = (rate) => {
if (!rate) return;
onRoleChange('role', rate.role_name || '');
onRoleChange('department', rate.category || '');
onRoleChange('cost_per_hour', rate.client_rate || 0);
onRoleChange('vendor_name', rate.vendor_name || '');
onRoleChange('vendor_id', rate.vendor_id || '');
setRoleSearchOpen(false);
};
// Get selected rate details
const selectedRate = availableRates.find(r => r && r.role_name === role.role);
return (
<Card className="border border-slate-200 bg-white hover:border-[#0A39DF] transition-all">
<CardContent className="p-4">
<div className="grid grid-cols-12 gap-4 items-start">
{/* Row Number */}
<div className="col-span-12 md:col-span-1 flex items-center justify-center">
<div className="w-8 h-8 bg-slate-700 rounded-full flex items-center justify-center text-white font-bold text-sm">
{roleIndex + 1}
</div>
</div>
{/* Role Selection with Vendor Rates - SEARCHABLE */}
<div className="col-span-12 md:col-span-3 space-y-2">
<div>
<Label className="text-xs text-slate-600 mb-1">Service / Role</Label>
<Popover open={roleSearchOpen} onOpenChange={setRoleSearchOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between text-sm h-auto py-2 bg-white hover:bg-slate-50",
!role.role && "text-slate-500"
)}
>
<div className="flex flex-col items-start gap-1 text-left">
{role.role ? (
<>
<span className="font-semibold text-slate-900">{role.role}</span>
{selectedRate && (
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] bg-blue-50 text-blue-700 border-blue-200">
{selectedRate.category}
</Badge>
<span className="text-xs text-green-600 font-bold">${selectedRate.client_rate}/hr</span>
</div>
)}
</>
) : (
<span>Select service...</span>
)}
</div>
<Search className="w-4 h-4 ml-2 text-slate-400 flex-shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0" align="start">
<Command>
<CommandInput placeholder="Search services..." className="h-9" />
<CommandEmpty>
<div className="p-4 text-center text-sm text-slate-500">
<p>No services found.</p>
{selectedVendor && (
<p className="mt-2 text-xs">Contact {selectedVendor} to add services.</p>
)}
{!selectedVendor && (
<p className="mt-2 text-xs">Select a vendor first to see available services.</p>
)}
</div>
</CommandEmpty>
<div className="max-h-[300px] overflow-y-auto">
{Object.entries(ratesByCategory || {}).map(([category, rates]) => (
<CommandGroup key={category} heading={category} className="text-slate-700">
{Array.isArray(rates) && rates.map((rate) => (
<CommandItem
key={rate.id}
value={`${rate.role_name} ${rate.vendor_name} ${rate.category}`}
onSelect={() => handleRoleSelect(rate)}
className="flex items-center justify-between py-3 cursor-pointer"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<p className="font-semibold text-sm text-slate-900">{rate.role_name}</p>
{role.role === rate.role_name && (
<Check className="w-4 h-4 text-[#0A39DF]" />
)}
</div>
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-[10px] bg-slate-50">
{rate.vendor_name}
</Badge>
{rate.pricing_status === 'optimal' && (
<Badge className="bg-green-100 text-green-700 text-[10px] border-0">
Optimal Price
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<div className="text-right">
<p className="text-lg font-bold text-[#0A39DF]">${rate.client_rate}</p>
<p className="text-[10px] text-slate-500">per hour</p>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
))}
</div>
</Command>
</PopoverContent>
</Popover>
</div>
{/* Department (Auto-filled from category) */}
<div>
<Label className="text-xs text-slate-600 mb-1">Department</Label>
<Select value={role.department || ""} onValueChange={(value) => onRoleChange('department', value)}>
<SelectTrigger className="h-9 text-sm bg-white">
<SelectValue placeholder="Department" />
</SelectTrigger>
<SelectContent>
{DEPARTMENTS.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Count */}
<div className="col-span-6 md:col-span-2">
<Label className="text-xs text-slate-600 mb-1 block">Count</Label>
<div className="flex items-center gap-1">
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 bg-white hover:bg-slate-50"
onClick={() => onRoleChange('count', Math.max(1, (role.count || 1) - 1))}
>
<Minus className="w-3 h-3" />
</Button>
<Input
type="number"
value={role.count || 1}
onChange={(e) => onRoleChange('count', parseInt(e.target.value) || 1)}
className="w-14 h-9 text-center p-0 text-sm bg-white"
/>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 bg-white hover:bg-slate-50"
onClick={() => onRoleChange('count', (role.count || 1) + 1)}
>
<Plus className="w-3 h-3" />
</Button>
</div>
</div>
{/* Time */}
<div className="col-span-6 md:col-span-3 space-y-2">
<div>
<Label className="text-xs text-slate-600 mb-1">Start Time</Label>
<Select value={role.start_time || "12:00 PM"} onValueChange={(value) => onRoleChange('start_time', value)}>
<SelectTrigger className="h-9 text-sm bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-48">
{TIME_OPTIONS.map(time => (
<SelectItem key={time} value={time}>{time}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-slate-600 mb-1">End Time</Label>
<Select value={role.end_time || "05:00 PM"} onValueChange={(value) => onRoleChange('end_time', value)}>
<SelectTrigger className="h-9 text-sm bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-48">
{TIME_OPTIONS.map(time => (
<SelectItem key={time} value={time}>{time}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Hours Badge */}
<div className="col-span-3 md:col-span-1 flex flex-col items-center justify-center">
<Label className="text-xs text-slate-600 mb-1">Hours</Label>
<div className="bg-[#0A39DF] text-white rounded-full w-10 h-10 flex items-center justify-center font-bold text-sm">
{role.hours || 0}
</div>
</div>
{/* Uniform & Break */}
<div className="col-span-9 md:col-span-2 space-y-2">
<div>
<Label className="text-xs text-slate-600 mb-1">Uniform</Label>
<Select value={role.uniform || "Type 1"} onValueChange={(value) => onRoleChange('uniform', value)}>
<SelectTrigger className="h-9 text-sm bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
{UNIFORMS.map(u => (
<SelectItem key={u} value={u}>{u}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-slate-600 mb-1">Break (min)</Label>
<Input
type="number"
value={role.break_minutes || 30}
onChange={(e) => onRoleChange('break_minutes', parseInt(e.target.value) || 0)}
className="h-9 text-center text-sm bg-white"
placeholder="30"
/>
</div>
</div>
{/* Cost & Value */}
<div className="col-span-12 md:col-span-3 flex items-end justify-between gap-4 pt-4 md:pt-0 border-t md:border-t-0 border-slate-200">
<div className="flex-1">
<Label className="text-xs text-slate-600 mb-1 block">Rate/hr</Label>
<div className="relative">
<DollarSign className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-400" />
<Input
type="number"
value={role.cost_per_hour || 0}
onChange={(e) => onRoleChange('cost_per_hour', parseFloat(e.target.value) || 0)}
className="h-9 text-sm pl-6 bg-white"
placeholder="0.00"
disabled={!!selectedRate}
/>
</div>
{selectedRate && (
<p className="text-[10px] text-green-600 mt-1">From vendor rate card</p>
)}
</div>
<div className="flex-1">
<Label className="text-xs text-slate-600 mb-1 block">Total</Label>
<div className="h-9 flex items-center justify-end font-bold text-[#0A39DF] text-lg">
${(role.total_value || 0).toFixed(2)}
</div>
</div>
{canDelete && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={onDelete}
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</div>
{/* Pricing Info Bar */}
{selectedRate && (
<div className="mt-3 pt-3 border-t border-slate-200 flex items-center justify-between text-xs bg-slate-50 -mx-4 -mb-4 px-4 py-3 rounded-b-lg">
<div className="flex items-center gap-4">
<span className="text-slate-500">
Employee Wage: <span className="font-semibold text-slate-700">${selectedRate.employee_wage || 0}/hr</span>
</span>
<span className="text-slate-500">
Markup: <span className="font-semibold text-blue-600">{selectedRate.markup_percentage || 0}%</span>
</span>
<span className="text-slate-500">
VA Fee: <span className="font-semibold text-purple-600">{selectedRate.vendor_fee_percentage || 0}%</span>
</span>
</div>
<Badge variant="outline" className="text-[10px] bg-green-50 text-green-700 border-green-200">
Transparent Pricing
</Badge>
</div>
)}
</CardContent>
</Card>
);
}