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:
Bernd Rodler
2026-08-07 13:45:09 +02:00
parent 13ec05da83
commit 4fcd37650d
12 changed files with 1030 additions and 4 deletions
+56
View File
@@ -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">&nbsp;</span>
)}
</td>
);
})
)}
</tr>
))}
</tbody>
</table>
</div>