feat: add inspection_expiry/has_etc/tire_type fields, icons in schedule view, and WebSocket real-time sync

Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/6d0f25ae-6db4-4937-ae2b-6674456a5ca1
This commit is contained in:
copilot-swe-agent[bot]
2026-03-22 03:58:07 +00:00
parent 09872737b7
commit 8e9db971d3
13 changed files with 451 additions and 26 deletions

View File

@@ -1,20 +1,27 @@
import { useState, useEffect } from 'react';
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() {
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 = async () => {
const loadCars = useCallback(async () => {
try {
setLoading(true);
const [carsData, resData] = await Promise.all([api.getCars(), api.getReservations()]);
@@ -26,20 +33,29 @@ export default function CarManagement() {
} 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() });
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);
@@ -66,19 +82,31 @@ export default function CarManagement() {
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() });
await api.updateCar(id, {
name: editName.trim(),
description: editDesc.trim(),
inspection_expiry: editExpiry,
has_etc: editEtc,
tire_type: editTire,
});
cancelEdit();
await loadCars();
} catch (e) {
@@ -111,6 +139,39 @@ export default function CarManagement() {
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>
@@ -129,13 +190,16 @@ export default function CarManagement() {
<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={4} className={styles.empty}>代車がありません</td>
<td colSpan={7} className={styles.empty}>代車がありません</td>
</tr>
)}
{cars.map((car) => (
@@ -159,6 +223,34 @@ export default function CarManagement() {
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}
@@ -176,6 +268,15 @@ export default function CarManagement() {
<>
<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 ? '🛣️ あり' : 'なし'}</td>
<td>{car.tire_type === 'スタットレス' ? '❄️ スタットレス' : 'ノーマル'}</td>
<td className={styles.actions}>
<button className={styles.btnEdit} onClick={() => startEdit(car)}>
編集