Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com> Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/a42d4e36-a3cf-4ff7-b1cb-f076e601b1b8
299 lines
10 KiB
JavaScript
299 lines
10 KiB
JavaScript
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>
|
||
);
|
||
}
|