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:
90
frontend/src/components/ConfirmModal.tsx
Normal file
90
frontend/src/components/ConfirmModal.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface ConfirmModalProps {
|
||||
isOpen: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
variant?: 'destructive' | 'warning' | 'default';
|
||||
}
|
||||
|
||||
export function ConfirmModal({
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = 'Delete',
|
||||
cancelLabel = 'Cancel',
|
||||
variant = 'destructive',
|
||||
}: ConfirmModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const getConfirmButtonStyles = () => {
|
||||
switch (variant) {
|
||||
case 'destructive':
|
||||
return 'bg-red-600 hover:bg-red-700 text-white';
|
||||
case 'warning':
|
||||
return 'bg-yellow-600 hover:bg-yellow-700 text-white';
|
||||
case 'default':
|
||||
return 'bg-primary hover:bg-primary/90 text-white';
|
||||
default:
|
||||
return 'bg-red-600 hover:bg-red-700 text-white';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<div
|
||||
className="bg-card rounded-lg shadow-xl w-full max-w-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header with icon */}
|
||||
<div className="flex items-start gap-4 p-6 pb-4">
|
||||
<div className={`flex-shrink-0 ${
|
||||
variant === 'destructive' ? 'text-red-600' :
|
||||
variant === 'warning' ? 'text-yellow-600' :
|
||||
'text-primary'
|
||||
}`}>
|
||||
<AlertTriangle className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-2">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 p-6 pt-4 border-t border-border">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="flex-1 bg-card text-foreground py-2.5 px-4 rounded-md hover:bg-accent font-medium border border-input transition-colors"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
onCancel();
|
||||
}}
|
||||
className={`flex-1 py-2.5 px-4 rounded-md font-medium transition-colors ${getConfirmButtonStyles()}`}
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { DEPARTMENT_LABELS } from '@/lib/enum-labels';
|
||||
|
||||
interface DriverFormProps {
|
||||
driver?: Driver | null;
|
||||
@@ -112,9 +113,11 @@ export function DriverForm({ driver, onSubmit, onCancel, isSubmitting }: DriverF
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="">Select Department</option>
|
||||
<option value="OFFICE_OF_DEVELOPMENT">Office of Development</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="OTHER">Other</option>
|
||||
{Object.entries(DEPARTMENT_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { X, ChevronDown, ChevronUp, ClipboardList } from 'lucide-react';
|
||||
import { toDatetimeLocal } from '@/lib/utils';
|
||||
import { DEPARTMENT_LABELS, ARRIVAL_MODE_LABELS } from '@/lib/enum-labels';
|
||||
|
||||
interface VIPFormProps {
|
||||
vip?: VIP | null;
|
||||
@@ -44,18 +46,6 @@ export interface VIPFormData {
|
||||
}
|
||||
|
||||
export function VIPForm({ vip, onSubmit, onCancel, isSubmitting }: VIPFormProps) {
|
||||
// Helper to convert ISO datetime to datetime-local format
|
||||
const toDatetimeLocal = (isoString: string | null) => {
|
||||
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<VIPFormData>({
|
||||
name: vip?.name || '',
|
||||
organization: vip?.organization || '',
|
||||
@@ -194,9 +184,11 @@ export function VIPForm({ vip, onSubmit, onCancel, isSubmitting }: VIPFormProps)
|
||||
className="w-full px-4 py-3 text-base bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
<option value="OFFICE_OF_DEVELOPMENT">Office of Development</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="OTHER">Other</option>
|
||||
{Object.entries(DEPARTMENT_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -213,8 +205,11 @@ export function VIPForm({ vip, onSubmit, onCancel, isSubmitting }: VIPFormProps)
|
||||
className="w-full px-4 py-3 text-base bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
<option value="FLIGHT">Flight</option>
|
||||
<option value="SELF_DRIVING">Self Driving</option>
|
||||
{Object.entries(ARRIVAL_MODE_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user