Files
SRCmail/stores/resource-store.ts
Bernd Rodler b98ab59f0d fix: Phase 2 QA — all 25 remaining HIGH/MEDIUM/LOW issues
HIGH fixes (7):
- H1: VNCdirectory admin i18n — 30+ translation keys added
- H2: handleSave try/catch with error toast
- H3: Free/busy accountId scoping
- H4: cancelEventBookings filter by eventId
- H5: Resource picker static apiFetch import
- H6: Sharing-store toast messages via lastMessage state
- H7: roleLabel for all resource types

MEDIUM fixes (11):
- M1: identitySignatureMap cleanup on delete
- M2: Now-line relative positioning
- M3: Radial menu disabled item keyboard nav
- M4: Radial menu stable event listener via refs
- M5: cancelBooking error on missing booking
- M6: PasswordRow isMasked state flag
- M7: Extract shared rights into lib/sharing-rights.ts
- M8: VNCtalk client server-side guard
- M9: Collabora configManager instead of process.env
- M10: CONFIG_ENV_MAP VNCdirectory fields
- M11: SENSITIVE_CONFIG_KEYS field name unification

LOW fixes (7):
- L1-L3: Unused imports removed
- L4: aria-labels on close, clear, search, spinner
- L5-L7: Comments for intentional patterns, null guard
2026-08-07 14:21:07 +02:00

160 lines
4.9 KiB
TypeScript

import { create } from 'zustand';
import { apiFetch } from '@/lib/browser-navigation';
import type { Resource, ResourceBooking } from '@/lib/resources/client';
interface ResourceState {
resources: Resource[];
selectedResources: Resource[];
bookings: ResourceBooking[];
isLoading: boolean;
bookingError: string | null;
fetchResources: (type?: string) => Promise<void>;
searchResources: (query: string) => Resource[];
toggleResource: (resource: Resource) => void;
selectResource: (resource: Resource) => void;
deselectResource: (resourceId: string) => void;
clearSelection: () => void;
fetchEventBookings: (eventId: string) => Promise<void>;
bookSelectedResources: (start: string, end: string, eventId?: string) => Promise<string[]>;
cancelBooking: (bookingId: string) => Promise<void>;
cancelEventBookings: (eventId: string) => Promise<void>;
}
export const useResourceStore = create<ResourceState>()((set, get) => ({
resources: [],
selectedResources: [],
bookings: [],
isLoading: false,
bookingError: null,
fetchResources: async (type?: string) => {
set({ isLoading: true });
try {
const params = new URLSearchParams();
if (type) params.set('type', type);
const res = await apiFetch(`/api/resources?${params.toString()}`);
if (!res.ok) throw new Error('Failed to fetch resources');
const data = await res.json();
set({ resources: data.resources, isLoading: false });
} catch {
set({ isLoading: false });
}
},
searchResources: (query: string) => {
const { resources } = get();
if (!query.trim()) return resources;
const lower = query.toLowerCase();
return resources.filter(
(r) =>
r.name.toLowerCase().includes(lower) ||
(r.location && r.location.toLowerCase().includes(lower)) ||
(r.description && r.description.toLowerCase().includes(lower))
);
},
toggleResource: (resource: Resource) => {
const { selectedResources } = get();
const exists = selectedResources.some((r) => r.id === resource.id);
if (exists) {
set({ selectedResources: selectedResources.filter((r) => r.id !== resource.id) });
} else {
set({ selectedResources: [...selectedResources, resource] });
}
},
selectResource: (resource: Resource) => {
const { selectedResources } = get();
if (!selectedResources.some((r) => r.id === resource.id)) {
set({ selectedResources: [...selectedResources, resource] });
}
},
deselectResource: (resourceId: string) => {
set({ selectedResources: get().selectedResources.filter((r) => r.id !== resourceId) });
},
clearSelection: () => {
set({ selectedResources: [], bookingError: null });
},
fetchEventBookings: async (eventId: string) => {
try {
const res = await apiFetch(`/api/resources?eventId=${encodeURIComponent(eventId)}`);
if (!res.ok) return;
const data = await res.json();
set({ bookings: data.bookings || [] });
} catch {
// silently fail
}
},
bookSelectedResources: async (start: string, end: string, eventId?: string) => {
set({ bookingError: null });
const { selectedResources } = get();
const bookedIds: string[] = [];
for (const resource of selectedResources) {
try {
const res = await apiFetch(`/api/resources/${resource.id}/book`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start, end, eventId }),
});
if (!res.ok) {
const data = await res.json();
set({ bookingError: `${resource.name}: ${data.error}` });
continue;
}
const data = await res.json();
bookedIds.push(data.booking.id);
} catch {
set({ bookingError: `Failed to book ${resource.name}` });
}
}
return bookedIds;
},
cancelBooking: async (bookingId: string) => {
const { bookings } = get();
const booking = bookings.find((b) => b.id === bookingId);
if (!booking) {
console.error(`cancelBooking: booking with id "${bookingId}" not found`);
set({ bookingError: `Booking ${bookingId} not found` });
return;
}
try {
const res = await apiFetch(
`/api/resources/${booking.resourceId}/book/${bookingId}`,
{ method: 'DELETE' }
);
if (res.ok) {
set({ bookings: bookings.filter((b) => b.id !== bookingId) });
}
} catch {
// silently fail
}
},
cancelEventBookings: async (eventId: string) => {
const { bookings } = get();
const eventBookings = bookings.filter((b) => b.eventId === eventId);
for (const booking of eventBookings) {
try {
const res = await apiFetch(
`/api/resources/${booking.resourceId}/book/${booking.id}`,
{ method: 'DELETE' }
);
if (res.ok) continue;
} catch {
// silently fail
}
}
set({ bookings: bookings.filter((b) => b.eventId !== eventId) });
},
}));