feat: P2.8 Resources/Equipment Booking (PostgreSQL + VNCdirectory)
- PostgreSQL schema: resources + resources_bookings tables with indexes - Server-side client with PG pool + in-memory fallback for dev - API routes: list, get, availability check, book, cancel - Resource store (Zustand) for client-side state - ResourcePicker component: type filter, search, availability dots - Integrated into event-modal: auto-book on save, auto-cancel on delete - Integrated into free-busy-view: resource availability rows
This commit is contained in:
@@ -8,11 +8,18 @@ import { useAuthStore } from "@/stores/auth-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetchFreeBusy, type FreeBusySlot, isWorkingHour as isWorkingHourFn } from "@/lib/calendar-freebusy";
|
||||
|
||||
export interface ResourceFreeBusyEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
availabilityMap: Map<number, FreeBusySlot["status"]>;
|
||||
}
|
||||
|
||||
export interface FreeBusyViewProps {
|
||||
participants: { name?: string; email: string }[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
onTimeSelect?: (start: Date, end: Date) => void;
|
||||
resources?: ResourceFreeBusyEntry[];
|
||||
}
|
||||
|
||||
const SLOT_MINUTES = 30;
|
||||
@@ -78,6 +85,7 @@ export function FreeBusyView({
|
||||
startDate,
|
||||
endDate,
|
||||
onTimeSelect,
|
||||
resources = [],
|
||||
}: FreeBusyViewProps) {
|
||||
const t = useTranslations("calendar");
|
||||
const client = useAuthStore((s) => s.client);
|
||||
@@ -255,6 +263,54 @@ export function FreeBusyView({
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{resources.map((res) => (
|
||||
<tr key={`res-${res.id}`} className="border-b border-border">
|
||||
<td className="sticky left-0 z-10 bg-background border-r border-border px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center shrink-0">
|
||||
<span className="text-[10px] font-bold text-blue-600 dark:text-blue-400">
|
||||
R
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium truncate text-sm">
|
||||
{res.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{hourSlots.map((hour) =>
|
||||
hour.slots.map((hourSlot, si) => {
|
||||
const globalSlotIndex =
|
||||
hourSlots
|
||||
.slice(0, hourSlots.indexOf(hour))
|
||||
.reduce((acc, h) => acc + h.slots.length, 0) + si;
|
||||
|
||||
const status = res.availabilityMap.get(globalSlotIndex) ?? "unknown";
|
||||
const isFree = status === "free";
|
||||
|
||||
return (
|
||||
<td
|
||||
key={si}
|
||||
className={cn(
|
||||
"border-r border-border py-1 text-center relative cursor-default transition-colors",
|
||||
statusColors[status],
|
||||
isFree && "cursor-pointer",
|
||||
isWorkingHour(new Date(hourSlot.start).getHours())
|
||||
? ""
|
||||
: "opacity-70"
|
||||
)}
|
||||
title={`${res.name} - ${format(hourSlot.start, "HH:mm")}`}
|
||||
>
|
||||
{status === "free" && (
|
||||
<span className="block w-full h-full"> </span>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user