217 lines
8.3 KiB
TypeScript
217 lines
8.3 KiB
TypeScript
import { Check } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import type { WeeklySlot } from '@/types';
|
|
|
|
interface WeeklySlotSelectorProps {
|
|
weeklySlots: WeeklySlot[];
|
|
onChange: (slots: WeeklySlot[]) => void;
|
|
slotAvailability?: Map<string, boolean>;
|
|
weekOffset?: number;
|
|
}
|
|
|
|
const timeSlots = [
|
|
{ start: '08:30', end: '09:30' },
|
|
{ start: '09:40', end: '10:40' },
|
|
{ start: '10:50', end: '11:50' },
|
|
{ start: '11:50', end: '12:50' },
|
|
{ start: '13:00', end: '14:00' },
|
|
{ start: '14:00', end: '15:00' },
|
|
{ start: '15:10', end: '16:10' },
|
|
{ start: '16:20', end: '17:20' },
|
|
{ start: '17:20', end: '18:20' },
|
|
];
|
|
|
|
const weekDays = [
|
|
{ value: 1, label: 'Seg' },
|
|
{ value: 2, label: 'Ter' },
|
|
{ value: 3, label: 'Qua' },
|
|
{ value: 4, label: 'Qui' },
|
|
{ value: 5, label: 'Sex' },
|
|
];
|
|
|
|
export function WeeklySlotSelector({
|
|
weeklySlots,
|
|
onChange,
|
|
slotAvailability,
|
|
weekOffset = 0,
|
|
}: WeeklySlotSelectorProps) {
|
|
// Calculate dates for the week
|
|
const getWeekDates = (offset: number) => {
|
|
const now = new Date();
|
|
const startOfWeek = new Date(now);
|
|
startOfWeek.setDate(now.getDate() - now.getDay() + 1 + offset * 7); // Monday
|
|
|
|
const dates: Record<number, Date> = {};
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const date = new Date(startOfWeek);
|
|
date.setDate(startOfWeek.getDate() + i);
|
|
dates[i + 1] = date;
|
|
}
|
|
|
|
return dates;
|
|
};
|
|
|
|
const weekDates = getWeekDates(weekOffset);
|
|
|
|
const formatDate = (date: Date) => {
|
|
return date.toLocaleDateString('pt-PT', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
});
|
|
};
|
|
|
|
const isSlotSelected = (day: number, start: string, end: string) => {
|
|
return weeklySlots.some(
|
|
(slot) =>
|
|
slot.day_of_week === day &&
|
|
slot.start_time === start &&
|
|
slot.end_time === end,
|
|
);
|
|
};
|
|
|
|
const isSlotAvailable = (day: number, start: string, end: string) => {
|
|
if (!slotAvailability) {
|
|
return true;
|
|
}
|
|
|
|
const key = `${day}-${start}-${end}`;
|
|
|
|
return slotAvailability.get(key) ?? true;
|
|
};
|
|
|
|
const toggleSlot = (day: number, start: string, end: string) => {
|
|
if (!isSlotAvailable(day, start, end)) {
|
|
return; // Don't allow selecting unavailable slots
|
|
}
|
|
|
|
const existingIndex = weeklySlots.findIndex(
|
|
(slot) =>
|
|
slot.day_of_week === day &&
|
|
slot.start_time === start &&
|
|
slot.end_time === end,
|
|
);
|
|
|
|
if (existingIndex >= 0) {
|
|
// Remove slot
|
|
const newSlots = weeklySlots.filter((_, i) => i !== existingIndex);
|
|
onChange(newSlots);
|
|
} else {
|
|
// Add slot
|
|
onChange([
|
|
...weeklySlots,
|
|
{ day_of_week: day, start_time: start, end_time: end },
|
|
]);
|
|
}
|
|
};
|
|
|
|
const clearAll = () => {
|
|
onChange([]);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<Label>Selecione os horários</Label>
|
|
{weeklySlots.length > 0 && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={clearAll}
|
|
>
|
|
Limpar tudo
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<div className="min-w-[500px]">
|
|
{/* Header row with day names */}
|
|
<div className="grid grid-cols-6 gap-2 mb-2">
|
|
<div className="text-xs font-medium text-muted-foreground" />
|
|
{weekDays.map((day) => (
|
|
<div
|
|
key={day.value}
|
|
className="text-center text-xs font-medium text-foreground"
|
|
>
|
|
{day.label} <span className="text-muted-foreground">{formatDate(weekDates[day.value])}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Time slots rows */}
|
|
{timeSlots.map((time) => (
|
|
<div key={time.start} className="grid grid-cols-6 gap-2 mb-2">
|
|
<div className="flex items-center text-xs text-muted-foreground">
|
|
{time.start}-{time.end}
|
|
</div>
|
|
{weekDays.map((day) => {
|
|
const selected = isSlotSelected(
|
|
day.value,
|
|
time.start,
|
|
time.end,
|
|
);
|
|
const available = isSlotAvailable(
|
|
day.value,
|
|
time.start,
|
|
time.end,
|
|
);
|
|
|
|
return (
|
|
<button
|
|
key={`${day.value}-${time.start}`}
|
|
type="button"
|
|
onClick={() =>
|
|
toggleSlot(
|
|
day.value,
|
|
time.start,
|
|
time.end,
|
|
)
|
|
}
|
|
disabled={!available}
|
|
title={
|
|
available
|
|
? `${time.start} - ${time.end}`
|
|
: 'Sem equipamentos disponíveis'
|
|
}
|
|
className={`
|
|
relative rounded-lg border-2 p-2 text-center text-xs transition-all
|
|
${
|
|
selected
|
|
? 'border-primary bg-primary text-primary-foreground'
|
|
: available
|
|
? 'border-slate-200 bg-slate-50 hover:border-slate-300 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-800/50 dark:hover:border-slate-600 dark:hover:bg-slate-800'
|
|
: 'border-slate-200 bg-slate-100 opacity-50 cursor-not-allowed dark:border-slate-700 dark:bg-slate-800/30'
|
|
}
|
|
`}
|
|
>
|
|
<div className="font-medium">
|
|
{time.start}-{time.end}
|
|
</div>
|
|
{selected && (
|
|
<div className="absolute right-1 top-1">
|
|
<Check className="size-3" />
|
|
</div>
|
|
)}
|
|
{!available && (
|
|
<div className="absolute -top-1 -right-1 size-3 rounded-full bg-destructive" />
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{weeklySlots.length === 0 && (
|
|
<div className="rounded-lg border border-dashed border-slate-300 p-4 text-center text-sm text-slate-500 dark:border-slate-700 dark:text-slate-400">
|
|
Selecione pelo menos um horário para continuar
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|