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:
@@ -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)}>
|
||||
編集
|
||||
|
||||
Reference in New Issue
Block a user