460 lines
15 KiB
JavaScript
460 lines
15 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Header } from './components/Header.jsx';
|
|
import { Card } from './components/Card.jsx';
|
|
import { Button } from './components/Button.jsx';
|
|
import { PageTransition } from './components/PageTransition.jsx';
|
|
import {
|
|
getCarsFolder,
|
|
loadCars,
|
|
resetCarsFolder,
|
|
saveCars,
|
|
selectCarsFolder,
|
|
} from './carStorage.js';
|
|
// Admin interface to add, edit, and remove car size presets.
|
|
|
|
export default function CarManager() {
|
|
const navigate = useNavigate();
|
|
const [options, setOptions] = useState([]);
|
|
const [addForm, setAddForm] = useState({
|
|
name: '',
|
|
width: '',
|
|
height: '',
|
|
hasRear: false,
|
|
rearWidth: '',
|
|
rearHeight: '',
|
|
});
|
|
const [editName, setEditName] = useState('');
|
|
const [editForm, setEditForm] = useState({
|
|
name: '',
|
|
width: '',
|
|
height: '',
|
|
hasRear: false,
|
|
rearWidth: '',
|
|
rearHeight: '',
|
|
});
|
|
const [deleteName, setDeleteName] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [authenticated, setAuthenticated] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [toast, setToast] = useState(false);
|
|
const [carsFolder, setCarsFolder] = useState('');
|
|
const [folderMessage, setFolderMessage] = useState('');
|
|
|
|
const applyCars = (cars) => {
|
|
const sorted = [...cars].sort((a, b) => a.name.localeCompare(b.name));
|
|
setOptions(sorted);
|
|
setEditName(sorted[0]?.name || '');
|
|
setDeleteName(sorted[0]?.name || '');
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadCars().then((opts) => {
|
|
applyCars(opts);
|
|
});
|
|
getCarsFolder().then((folder) => {
|
|
setCarsFolder(folder || '');
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const car = options.find((o) => o.name === editName);
|
|
if (car) {
|
|
setEditForm({
|
|
name: car.name,
|
|
width: String(car.width),
|
|
height: String(car.height),
|
|
hasRear: Boolean(car.rearWidth && car.rearHeight),
|
|
rearWidth: car.rearWidth ? String(car.rearWidth) : '',
|
|
rearHeight: car.rearHeight ? String(car.rearHeight) : '',
|
|
});
|
|
}
|
|
}, [editName, options]);
|
|
|
|
useEffect(() => {
|
|
setDeleteName((dn) =>
|
|
options.find((o) => o.name === dn) ? dn : options[0]?.name || ''
|
|
);
|
|
}, [options]);
|
|
|
|
useEffect(() => {
|
|
if (toast) {
|
|
const t = setTimeout(() => setToast(false), 2000);
|
|
return () => clearTimeout(t);
|
|
}
|
|
}, [toast]);
|
|
|
|
useEffect(() => {
|
|
if (folderMessage) {
|
|
const t = setTimeout(() => setFolderMessage(''), 2500);
|
|
return () => clearTimeout(t);
|
|
}
|
|
}, [folderMessage]);
|
|
|
|
const handleAddChange = (e) => {
|
|
const { name, value, type, checked } = e.target;
|
|
setAddForm((fd) => ({
|
|
...fd,
|
|
[name]: type === 'checkbox' ? checked : value,
|
|
}));
|
|
};
|
|
|
|
const handleEditChange = (e) => {
|
|
const { name, value, type, checked } = e.target;
|
|
setEditForm((fd) => ({
|
|
...fd,
|
|
[name]: type === 'checkbox' ? checked : value,
|
|
}));
|
|
};
|
|
|
|
const handleAddSubmit = async (e) => {
|
|
e.preventDefault();
|
|
const newCar = {
|
|
name: addForm.name,
|
|
width: parseFloat(addForm.width),
|
|
height: parseFloat(addForm.height),
|
|
};
|
|
if (addForm.hasRear) {
|
|
newCar.rearWidth = parseFloat(addForm.rearWidth);
|
|
newCar.rearHeight = parseFloat(addForm.rearHeight);
|
|
}
|
|
const newOptions = [...options, newCar].sort((a, b) =>
|
|
a.name.localeCompare(b.name)
|
|
);
|
|
setOptions(newOptions);
|
|
await saveCars(newOptions);
|
|
setAddForm({
|
|
name: '',
|
|
width: '',
|
|
height: '',
|
|
hasRear: false,
|
|
rearWidth: '',
|
|
rearHeight: '',
|
|
});
|
|
setToast(true);
|
|
};
|
|
|
|
const handleEditSubmit = async (e) => {
|
|
e.preventDefault();
|
|
const updatedCar = {
|
|
name: editForm.name,
|
|
width: parseFloat(editForm.width),
|
|
height: parseFloat(editForm.height),
|
|
};
|
|
if (editForm.hasRear) {
|
|
updatedCar.rearWidth = parseFloat(editForm.rearWidth);
|
|
updatedCar.rearHeight = parseFloat(editForm.rearHeight);
|
|
}
|
|
const newOptions = options
|
|
.map((o) => (o.name === editName ? updatedCar : o))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
setOptions(newOptions);
|
|
setEditName(updatedCar.name);
|
|
await saveCars(newOptions);
|
|
setToast(true);
|
|
};
|
|
|
|
const handleDeleteSubmit = async (e) => {
|
|
e.preventDefault();
|
|
const newOptions = options.filter((o) => o.name !== deleteName);
|
|
setOptions(newOptions);
|
|
setDeleteName(newOptions[0]?.name || '');
|
|
await saveCars(newOptions);
|
|
setToast(true);
|
|
};
|
|
|
|
const handleAuthSubmit = (e) => {
|
|
e.preventDefault();
|
|
if (password === 'Rotterdam-010') {
|
|
setAuthenticated(true);
|
|
setPassword('');
|
|
setError('');
|
|
} else {
|
|
setError('Onjuist wachtwoord');
|
|
}
|
|
};
|
|
|
|
const handleSelectFolder = async () => {
|
|
const result = await selectCarsFolder();
|
|
if (!result || result.canceled) {
|
|
setFolderMessage('Geen map gekozen');
|
|
return;
|
|
}
|
|
setCarsFolder(result.folder || '');
|
|
if (result.cars) {
|
|
applyCars(result.cars);
|
|
} else {
|
|
const refreshed = await loadCars();
|
|
applyCars(refreshed);
|
|
}
|
|
setFolderMessage('Opslaglocatie bijgewerkt');
|
|
};
|
|
|
|
const handleResetFolder = async () => {
|
|
const result = await resetCarsFolder();
|
|
if (result?.folder) {
|
|
setCarsFolder(result.folder);
|
|
}
|
|
if (result?.cars) {
|
|
applyCars(result.cars);
|
|
} else {
|
|
const refreshed = await loadCars();
|
|
applyCars(refreshed);
|
|
}
|
|
setFolderMessage('Standaardmap ingesteld');
|
|
};
|
|
|
|
return (
|
|
<PageTransition>
|
|
<Header
|
|
count={options.length}
|
|
title="Kinderauto's beheren"
|
|
onHome={() => navigate('/')}
|
|
/>
|
|
{!authenticated ? (
|
|
<div className="container mt-6">
|
|
<Card title="Toegang vereist">
|
|
<form onSubmit={handleAuthSubmit} className="mb-lg">
|
|
<div className="form-row">
|
|
<label htmlFor="managerPassword">Wachtwoord:</label>
|
|
<input
|
|
id="managerPassword"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
<Button type="submit" variant="primary" className="mt">Inloggen</Button>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
) : (
|
|
<div className="container mt-6">
|
|
<aside className="side-cards">
|
|
<Card title="Opslaglocatie">
|
|
<p className="text-sm">
|
|
Huidige map: <strong>{carsFolder || 'Standaard'}</strong>
|
|
</p>
|
|
<div className="button-row mt">
|
|
<Button type="button" variant="primary" onClick={handleSelectFolder}>
|
|
Kies map
|
|
</Button>
|
|
<Button type="button" variant="ghost" onClick={handleResetFolder}>
|
|
Standaardmap
|
|
</Button>
|
|
</div>
|
|
{folderMessage && <p className="text-sm mt">{folderMessage}</p>}
|
|
</Card>
|
|
<Card title="Kinderauto toevoegen">
|
|
<form onSubmit={handleAddSubmit} className="mb-lg">
|
|
<div className="form-row">
|
|
<label htmlFor="addName">Naam:</label>
|
|
<input
|
|
id="addName"
|
|
name="name"
|
|
value={addForm.name}
|
|
onChange={handleAddChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="addWidth">Breedte (cm):</label>
|
|
<input
|
|
id="addWidth"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
name="width"
|
|
value={addForm.width}
|
|
onChange={handleAddChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="addHeight">Hoogte (cm):</label>
|
|
<input
|
|
id="addHeight"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
name="height"
|
|
value={addForm.height}
|
|
onChange={handleAddChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-row checkbox">
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
name="hasRear"
|
|
checked={addForm.hasRear}
|
|
onChange={handleAddChange}
|
|
/>
|
|
Achter heeft andere maat
|
|
</label>
|
|
</div>
|
|
{addForm.hasRear && (
|
|
<>
|
|
<div className="form-row">
|
|
<label htmlFor="addRearWidth">Breedte achter (cm):</label>
|
|
<input
|
|
id="addRearWidth"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
name="rearWidth"
|
|
value={addForm.rearWidth}
|
|
onChange={handleAddChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="addRearHeight">Hoogte achter (cm):</label>
|
|
<input
|
|
id="addRearHeight"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
name="rearHeight"
|
|
value={addForm.rearHeight}
|
|
onChange={handleAddChange}
|
|
required
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
<Button type="submit" variant="primary" className="mt">
|
|
Opslaan
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
<Card title="Kinderauto bewerken">
|
|
<form onSubmit={handleEditSubmit} className="mb-lg">
|
|
<div className="form-row">
|
|
<label htmlFor="editSelect">Selecteer:</label>
|
|
<select
|
|
id="editSelect"
|
|
value={editName}
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
>
|
|
{options.map((opt) => (
|
|
<option key={opt.name} value={opt.name}>
|
|
{opt.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="editName">Naam:</label>
|
|
<input
|
|
id="editName"
|
|
name="name"
|
|
value={editForm.name}
|
|
onChange={handleEditChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="editWidth">Breedte (cm):</label>
|
|
<input
|
|
id="editWidth"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
name="width"
|
|
value={editForm.width}
|
|
onChange={handleEditChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="editHeight">Hoogte (cm):</label>
|
|
<input
|
|
id="editHeight"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
name="height"
|
|
value={editForm.height}
|
|
onChange={handleEditChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-row checkbox">
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
name="hasRear"
|
|
checked={editForm.hasRear}
|
|
onChange={handleEditChange}
|
|
/>
|
|
Achter heeft andere maat
|
|
</label>
|
|
</div>
|
|
{editForm.hasRear && (
|
|
<>
|
|
<div className="form-row">
|
|
<label htmlFor="editRearWidth">Breedte achter (cm):</label>
|
|
<input
|
|
id="editRearWidth"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
name="rearWidth"
|
|
value={editForm.rearWidth}
|
|
onChange={handleEditChange}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-row">
|
|
<label htmlFor="editRearHeight">Hoogte achter (cm):</label>
|
|
<input
|
|
id="editRearHeight"
|
|
type="number"
|
|
step="0.1"
|
|
min="0"
|
|
name="rearHeight"
|
|
value={editForm.rearHeight}
|
|
onChange={handleEditChange}
|
|
required
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
<Button type="submit" variant="primary" className="mt">
|
|
Bijwerken
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
<Card title="Kinderauto verwijderen">
|
|
<form onSubmit={handleDeleteSubmit} className="mb-lg">
|
|
<div className="form-row">
|
|
<label htmlFor="deleteSelect">Selecteer:</label>
|
|
<select
|
|
id="deleteSelect"
|
|
value={deleteName}
|
|
onChange={(e) => setDeleteName(e.target.value)}
|
|
>
|
|
{options.map((opt) => (
|
|
<option key={opt.name} value={opt.name}>
|
|
{opt.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<Button type="submit" variant="ghost" className="mt">
|
|
Verwijder
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
</aside>
|
|
</div>
|
|
)}
|
|
{toast && <div className="toast">Opgeslagen</div>}
|
|
</PageTransition>
|
|
);
|
|
}
|