refactor: code efficiency improvements (Issues #9-13, #15, #17-20)

Backend:
- Extract shared hard-delete authorization utility (#9)
- Extract Prisma include constants per entity (#11)
- Fix N+1 query pattern in events findAll (#12)
- Extract shared date utility functions (#13)
- Move vehicle utilization filtering to DB query (#15)
- Add ParseBooleanPipe for query params
- Add CurrentDriver decorator + ResolveDriverInterceptor (#20)

Frontend:
- Extract shared form utilities (toDatetimeLocal) and enum labels (#17)
- Replace browser confirm() with styled ConfirmModal (#18)
- Add centralized query-keys.ts constants (#19)
- Clean up unused imports, add useMemo where needed (#19)
- Standardize filter button styling across list pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 16:07:19 +01:00
parent 806b67954e
commit f2b3f34a72
38 changed files with 1042 additions and 463 deletions

View File

@@ -1,10 +1,13 @@
import { useState, useEffect, useMemo } from 'react';
import { useState, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useQuery } from '@tanstack/react-query';
import { X, AlertTriangle, Users, Car, Link2 } from 'lucide-react';
import { api } from '@/lib/api';
import { ScheduleEvent, VIP, Driver, Vehicle } from '@/types';
import { useFormattedDate } from '@/hooks/useFormattedDate';
import { toDatetimeLocal } from '@/lib/utils';
import { EVENT_TYPE_LABELS, EVENT_STATUS_LABELS } from '@/lib/enum-labels';
import { queryKeys } from '@/lib/query-keys';
interface EventFormProps {
event?: ScheduleEvent | null;
@@ -41,18 +44,6 @@ interface ScheduleConflict {
export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraActions }: EventFormProps) {
const { formatDateTime } = useFormattedDate();
// Helper to convert ISO datetime to datetime-local format
const toDatetimeLocal = (isoString: string | null | undefined) => {
if (!isoString) return '';
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
};
const [formData, setFormData] = useState<EventFormData>({
vipIds: event?.vipIds || [],
title: event?.title || '',
@@ -77,7 +68,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraAction
// Fetch VIPs for selection
const { data: vips } = useQuery<VIP[]>({
queryKey: ['vips'],
queryKey: queryKeys.vips.all,
queryFn: async () => {
const { data } = await api.get('/vips');
return data;
@@ -86,7 +77,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraAction
// Fetch Drivers for dropdown
const { data: drivers } = useQuery<Driver[]>({
queryKey: ['drivers'],
queryKey: queryKeys.drivers.all,
queryFn: async () => {
const { data } = await api.get('/drivers');
return data;
@@ -95,7 +86,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraAction
// Fetch Vehicles for dropdown
const { data: vehicles } = useQuery<Vehicle[]>({
queryKey: ['vehicles'],
queryKey: queryKeys.vehicles.all,
queryFn: async () => {
const { data } = await api.get('/vehicles');
return data;
@@ -104,7 +95,7 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraAction
// Fetch all events (for master event selector)
const { data: allEvents } = useQuery<ScheduleEvent[]>({
queryKey: ['events'],
queryKey: queryKeys.events.all,
queryFn: async () => {
const { data } = await api.get('/events');
return data;
@@ -219,10 +210,12 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraAction
});
};
const selectedVipNames = vips
?.filter(vip => formData.vipIds.includes(vip.id))
.map(vip => vip.partySize > 1 ? `${vip.name} (+${vip.partySize - 1})` : vip.name)
.join(', ') || 'None selected';
const selectedVipNames = useMemo(() => {
return vips
?.filter(vip => formData.vipIds.includes(vip.id))
.map(vip => vip.partySize > 1 ? `${vip.name} (+${vip.partySize - 1})` : vip.name)
.join(', ') || 'None selected';
}, [vips, formData.vipIds]);
return (
<>
@@ -452,11 +445,11 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraAction
onChange={handleChange}
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="TRANSPORT">Transport</option>
<option value="MEETING">Meeting</option>
<option value="EVENT">Event</option>
<option value="MEAL">Meal</option>
<option value="ACCOMMODATION">Accommodation</option>
{Object.entries(EVENT_TYPE_LABELS).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
<div>
@@ -470,10 +463,11 @@ export function EventForm({ event, onSubmit, onCancel, isSubmitting, extraAction
onChange={handleChange}
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="SCHEDULED">Scheduled</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="COMPLETED">Completed</option>
<option value="CANCELLED">Cancelled</option>
{Object.entries(EVENT_STATUS_LABELS).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
</div>