Files
motovaultpro/frontend/src/features/subscription/components/VehicleSelectionDialog.tsx
Eric Gullickson 325cf08df0 fix: promote vehicle display utils to core with null safety (refs #165)
Create shared getVehicleLabel/getVehicleSubtitle in core/utils with
VehicleLike interface. Replace all direct year/make/model concatenation
across 17 consumer files to prevent null values in vehicle names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:32:40 -06:00

161 lines
4.7 KiB
TypeScript

import { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
FormGroup,
FormControlLabel,
Checkbox,
Typography,
Alert,
Box,
} from '@mui/material';
import type { SubscriptionTier } from '../types/subscription.types';
import { getVehicleLabel } from '@/core/utils/vehicleDisplay';
interface Vehicle {
id: string;
make?: string;
model?: string;
year?: number;
nickname?: string;
}
interface VehicleSelectionDialogProps {
open: boolean;
onClose: () => void;
onConfirm: (selectedVehicleIds: string[]) => void;
vehicles: Vehicle[];
maxSelections: number;
targetTier: SubscriptionTier;
/** When true, dialog cannot be dismissed - user must make a selection */
blocking?: boolean;
}
export const VehicleSelectionDialog = ({
open,
onClose,
onConfirm,
vehicles,
maxSelections,
targetTier,
blocking = false,
}: VehicleSelectionDialogProps) => {
const [selectedVehicleIds, setSelectedVehicleIds] = useState<string[]>([]);
// Pre-select first N vehicles when dialog opens
useEffect(() => {
if (open && vehicles.length > 0) {
const initialSelection = vehicles.slice(0, maxSelections).map((v) => v.id);
setSelectedVehicleIds(initialSelection);
}
}, [open, vehicles, maxSelections]);
const handleToggle = (vehicleId: string) => {
setSelectedVehicleIds((prev) => {
if (prev.includes(vehicleId)) {
return prev.filter((id) => id !== vehicleId);
} else {
// Only add if under the limit
if (prev.length < maxSelections) {
return [...prev, vehicleId];
}
return prev;
}
});
};
const handleConfirm = () => {
onConfirm(selectedVehicleIds);
};
const canConfirm = selectedVehicleIds.length > 0 && selectedVehicleIds.length <= maxSelections;
// Handle dialog close - prevent if blocking
const handleClose = (_event: object, reason: string) => {
if (blocking && (reason === 'backdropClick' || reason === 'escapeKeyDown')) {
return;
}
onClose();
};
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
disableEscapeKeyDown={blocking}
>
<DialogTitle>
{blocking ? 'Vehicle Selection Required' : 'Select Vehicles to Keep'}
</DialogTitle>
<DialogContent>
<Alert severity={blocking ? 'info' : 'warning'} sx={{ mb: 2 }}>
{blocking ? (
<>
Your subscription has been downgraded to the {targetTier} tier, which allows{' '}
{maxSelections} vehicle{maxSelections > 1 ? 's' : ''}. Please select which vehicles
you want to keep active to continue using the app. Unselected vehicles will be hidden
but not deleted, and you can unlock them by upgrading later.
</>
) : (
<>
You are downgrading to the {targetTier} tier, which allows {maxSelections} vehicle
{maxSelections > 1 ? 's' : ''}. Select which vehicles you want to keep active.
Unselected vehicles will be hidden but not deleted, and you can unlock them by
upgrading later.
</>
)}
</Alert>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Selected {selectedVehicleIds.length} of {maxSelections} allowed
</Typography>
</Box>
<FormGroup>
{vehicles.map((vehicle) => (
<FormControlLabel
key={vehicle.id}
control={
<Checkbox
checked={selectedVehicleIds.includes(vehicle.id)}
onChange={() => handleToggle(vehicle.id)}
disabled={
!selectedVehicleIds.includes(vehicle.id) &&
selectedVehicleIds.length >= maxSelections
}
/>
}
label={getVehicleLabel(vehicle)}
/>
))}
</FormGroup>
{selectedVehicleIds.length === 0 && (
<Alert severity="error" sx={{ mt: 2 }}>
You must select at least one vehicle.
</Alert>
)}
{selectedVehicleIds.length > maxSelections && (
<Alert severity="error" sx={{ mt: 2 }}>
You can only select up to {maxSelections} vehicle{maxSelections > 1 ? 's' : ''}.
</Alert>
)}
</DialogContent>
<DialogActions>
{!blocking && <Button onClick={onClose}>Cancel</Button>}
<Button onClick={handleConfirm} variant="contained" disabled={!canConfirm}>
{blocking ? 'Confirm Selection' : 'Confirm Downgrade'}
</Button>
</DialogActions>
</Dialog>
);
};