Files
reservas/resources/js/components/weekly-slot-selector.tsx
João Miranda fb04d71a8d
Some checks failed
tests / ci (push) Has been cancelled
first commit
2026-10-27 11:01:11 +00:00

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>
);
}