Files
car/frontend/src/components/CarManagement.jsx

299 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
import { api } from '../api.js';
import { isInspectionExpirySoon } from '../utils/carUtils.js';
import styles from './CarManagement.module.css';
export default function CarManagement({ reloadKey = 0 }) {
const [cars, setCars] = useState([]);
const [reservations, setReservations] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [newCarName, setNewCarName] = useState('');
const [newCarDesc, setNewCarDesc] = useState('');
const [newCarExpiry, setNewCarExpiry] = useState('');
const [newCarEtc, setNewCarEtc] = useState(false);
const [newCarTire, setNewCarTire] = useState('ノーマル');
const [editingId, setEditingId] = useState(null);
const [editName, setEditName] = useState('');
const [editDesc, setEditDesc] = useState('');
const [editExpiry, setEditExpiry] = useState('');
const [editEtc, setEditEtc] = useState(false);
const [editTire, setEditTire] = useState('ノーマル');
const [submitting, setSubmitting] = useState(false);
const loadCars = useCallback(async () => {
try {
setLoading(true);
const [carsData, resData] = await Promise.all([api.getCars(), api.getReservations()]);
setCars(carsData);
setReservations(resData);
setError(null);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadCars();
}, [loadCars, reloadKey]);
const handleAdd = async (e) => {
e.preventDefault();
if (!newCarName.trim()) return;
try {
setSubmitting(true);
await api.createCar({
name: newCarName.trim(),
description: newCarDesc.trim(),
inspection_expiry: newCarExpiry,
has_etc: newCarEtc,
tire_type: newCarTire,
});
setNewCarName('');
setNewCarDesc('');
setNewCarExpiry('');
setNewCarEtc(false);
setNewCarTire('ノーマル');
await loadCars();
} catch (e) {
setError(e.message);
} finally {
setSubmitting(false);
}
};
const handleDelete = async (id, name) => {
const carReservations = reservations.filter((r) => r.car_id === id);
const message = carReservations.length > 0
? `${name}」を削除しますか?\n⚠ この代車には ${carReservations.length} 件の予約があります。削除するとこれらの予約もすべて削除されます。`
: `${name}」を削除しますか?\n関連する予約もすべて削除されます。`;
if (!confirm(message)) return;
try {
await api.deleteCar(id);
await loadCars();
} catch (e) {
setError(e.message);
}
};
const startEdit = (car) => {
setEditingId(car.id);
setEditName(car.name);
setEditDesc(car.description || '');
setEditExpiry(car.inspection_expiry || '');
setEditEtc(!!car.has_etc);
setEditTire(car.tire_type || 'ノーマル');
};
const cancelEdit = () => {
setEditingId(null);
setEditName('');
setEditDesc('');
setEditExpiry('');
setEditEtc(false);
setEditTire('ノーマル');
};
const handleUpdate = async (id) => {
if (!editName.trim()) return;
try {
setSubmitting(true);
await api.updateCar(id, {
name: editName.trim(),
description: editDesc.trim(),
inspection_expiry: editExpiry,
has_etc: editEtc,
tire_type: editTire,
});
cancelEdit();
await loadCars();
} catch (e) {
setError(e.message);
} finally {
setSubmitting(false);
}
};
return (
<div className={styles.container}>
<h2 className={styles.heading}>代車管理</h2>
<div className={styles.addCard}>
<h3 className={styles.subHeading}>代車を追加</h3>
<form className={styles.form} onSubmit={handleAdd}>
<div className={styles.formRow}>
<input
type="text"
className={styles.input}
placeholder="車名(例:プリウス A"
value={newCarName}
onChange={(e) => setNewCarName(e.target.value)}
required
/>
<input
type="text"
className={styles.input}
placeholder="備考(任意)"
value={newCarDesc}
onChange={(e) => setNewCarDesc(e.target.value)}
/>
</div>
<div className={styles.formRow}>
<label className={styles.fieldLabel}>
車検満了日
<input
type="date"
className={styles.input}
value={newCarExpiry}
onChange={(e) => setNewCarExpiry(e.target.value)}
/>
</label>
<label className={styles.fieldLabel}>
ETC
<select
className={styles.input}
value={newCarEtc ? 'あり' : 'なし'}
onChange={(e) => setNewCarEtc(e.target.value === 'あり')}
>
<option value="なし">なし</option>
<option value="あり">あり</option>
</select>
</label>
<label className={styles.fieldLabel}>
タイヤ
<select
className={styles.input}
value={newCarTire}
onChange={(e) => setNewCarTire(e.target.value)}
>
<option value="ノーマル">ノーマル</option>
<option value="スタットレス">スタットレス</option>
</select>
</label>
<button type="submit" className={styles.btnPrimary} disabled={submitting || !newCarName.trim()}>
+ 追加
</button>
</div>
</form>
</div>
{loading && <p className={styles.message}>読み込み中...</p>}
{error && <p className={styles.error}>エラー: {error}</p>}
{!loading && !error && (
<div className={styles.tableWrapper}>
<table className={styles.table}>
<thead>
<tr>
<th>ID</th>
<th>車名</th>
<th>備考</th>
<th>車検満了日</th>
<th>ETC</th>
<th>タイヤ</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{cars.length === 0 && (
<tr>
<td colSpan={7} className={styles.empty}>代車がありません</td>
</tr>
)}
{cars.map((car) => (
<tr key={car.id}>
<td className={styles.idCell}>{car.id}</td>
{editingId === car.id ? (
<>
<td>
<input
type="text"
className={styles.input}
value={editName}
onChange={(e) => setEditName(e.target.value)}
/>
</td>
<td>
<input
type="text"
className={styles.input}
value={editDesc}
onChange={(e) => setEditDesc(e.target.value)}
/>
</td>
<td>
<input
type="date"
className={styles.input}
value={editExpiry}
onChange={(e) => setEditExpiry(e.target.value)}
/>
</td>
<td>
<select
className={styles.input}
value={editEtc ? 'あり' : 'なし'}
onChange={(e) => setEditEtc(e.target.value === 'あり')}
>
<option value="なし">なし</option>
<option value="あり">あり</option>
</select>
</td>
<td>
<select
className={styles.input}
value={editTire}
onChange={(e) => setEditTire(e.target.value)}
>
<option value="ノーマル">ノーマル</option>
<option value="スタットレス">スタットレス</option>
</select>
</td>
<td className={styles.actions}>
<button
className={styles.btnSave}
onClick={() => handleUpdate(car.id)}
disabled={submitting}
>
保存
</button>
<button className={styles.btnCancel} onClick={cancelEdit}>
キャンセル
</button>
</td>
</>
) : (
<>
<td>{car.name}</td>
<td className={styles.descCell}>{car.description || '-'}</td>
<td className={styles.descCell}>
{car.inspection_expiry
? isInspectionExpirySoon(car.inspection_expiry)
? <span className={styles.expiryWarn}> {car.inspection_expiry}</span>
: car.inspection_expiry
: '-'}
</td>
<td>{car.has_etc ? <span className={styles.badgeEtc}>ETC</span> : ''}</td>
<td>{car.tire_type === 'スタットレス' ? <span className={styles.badgeStudless}>スタッドレス</span> : ''}</td>
<td className={styles.actions}>
<button className={styles.btnEdit} onClick={() => startEdit(car)}>
編集
</button>
<button className={styles.btnDelete} onClick={() => handleDelete(car.id, car.name)}>
削除
</button>
</td>
</>
)}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}