Add car reordering and AM/PM period for reservations

Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/c0a4b7dc-228e-4e7d-a985-61b9a17de159

Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-06 07:12:49 +00:00
committed by GitHub
parent 2e9e100178
commit 675e5f6fe8
8 changed files with 176 additions and 22 deletions

View File

@@ -23,6 +23,7 @@ export const api = {
createCar: (data) => request('/cars', { method: 'POST', body: JSON.stringify(data) }),
updateCar: (id, data) => request(`/cars/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
deleteCar: (id) => request(`/cars/${id}`, { method: 'DELETE' }),
reorderCars: (ids) => request('/cars/reorder', { method: 'PUT', body: JSON.stringify({ ids }) }),
// Reservations
getReservations: () => request('/reservations'),

View File

@@ -116,6 +116,20 @@ export default function CarManagement({ reloadKey = 0 }) {
}
};
const handleReorder = async (index, direction) => {
const newCars = [...cars];
const swapIndex = index + direction;
if (swapIndex < 0 || swapIndex >= newCars.length) return;
[newCars[index], newCars[swapIndex]] = [newCars[swapIndex], newCars[index]];
setCars(newCars);
try {
await api.reorderCars(newCars.map((c) => c.id));
} catch (e) {
setError(e.message);
await loadCars();
}
};
return (
<div className={styles.container}>
<h2 className={styles.heading}>代車管理</h2>
@@ -187,7 +201,7 @@ export default function CarManagement({ reloadKey = 0 }) {
<table className={styles.table}>
<thead>
<tr>
<th>ID</th>
<th>順番</th>
<th>車名</th>
<th>備考</th>
<th>車検満了日</th>
@@ -202,9 +216,26 @@ export default function CarManagement({ reloadKey = 0 }) {
<td colSpan={7} className={styles.empty}>代車がありません</td>
</tr>
)}
{cars.map((car) => (
{cars.map((car, carIdx) => (
<tr key={car.id}>
<td className={styles.idCell}>{car.id}</td>
<td className={styles.idCell}>
<div className={styles.orderBtns}>
<button
className={styles.btnOrder}
onClick={() => handleReorder(carIdx, -1)}
disabled={carIdx === 0}
title="上に移動"
aria-label="上に移動"
></button>
<button
className={styles.btnOrder}
onClick={() => handleReorder(carIdx, 1)}
disabled={carIdx === cars.length - 1}
title="下に移動"
aria-label="下に移動"
></button>
</div>
</td>
{editingId === car.id ? (
<>
<td>

View File

@@ -113,7 +113,34 @@
.idCell {
color: #9ca3af;
width: 50px;
width: 80px;
}
.orderBtns {
display: flex;
flex-direction: column;
gap: 2px;
align-items: center;
}
.btnOrder {
background: #f3f4f6;
border: 1px solid #d1d5db;
color: #374151;
padding: 1px 8px;
border-radius: 4px;
font-size: 10px;
line-height: 1.4;
transition: background 0.15s;
}
.btnOrder:hover:not(:disabled) {
background: #e5e7eb;
}
.btnOrder:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.descCell {

View File

@@ -2,12 +2,20 @@ import { useState, useEffect } from 'react';
import { format } from 'date-fns';
import styles from './ReservationModal.module.css';
const PERIOD_OPTIONS = [
{ value: '', label: '指定なし' },
{ value: '午前', label: '午前' },
{ value: '午後', label: '午後' },
];
export default function ReservationModal({ cars, reservation, onSave, onDelete, onClose }) {
const isEdit = !!reservation?.id;
const [carId, setCarId] = useState('');
const [startDate, setStartDate] = useState('');
const [startPeriod, setStartPeriod] = useState('');
const [endDate, setEndDate] = useState('');
const [endPeriod, setEndPeriod] = useState('');
const [customerName, setCustomerName] = useState('');
const [notes, setNotes] = useState('');
const [submitting, setSubmitting] = useState(false);
@@ -16,7 +24,9 @@ export default function ReservationModal({ cars, reservation, onSave, onDelete,
if (reservation) {
setCarId(String(reservation.car_id || (cars[0]?.id ?? '')));
setStartDate(reservation.start_date || format(new Date(), 'yyyy-MM-dd'));
setStartPeriod(reservation.start_period || '');
setEndDate(reservation.end_date || format(new Date(), 'yyyy-MM-dd'));
setEndPeriod(reservation.end_period || '');
setCustomerName(reservation.customer_name || '');
setNotes(reservation.notes || '');
}
@@ -34,7 +44,9 @@ export default function ReservationModal({ cars, reservation, onSave, onDelete,
await onSave({
car_id: Number(carId),
start_date: startDate,
start_period: startPeriod,
end_date: endDate,
end_period: endPeriod,
customer_name: customerName,
notes,
});
@@ -90,6 +102,21 @@ export default function ReservationModal({ cars, reservation, onSave, onDelete,
required
/>
</div>
<div className={styles.field}>
<label className={styles.label}>開始時間帯</label>
<select
className={styles.select}
value={startPeriod}
onChange={(e) => setStartPeriod(e.target.value)}
>
{PERIOD_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
</div>
<div className={styles.fieldRow}>
<div className={styles.field}>
<label className={styles.label}>終了日 <span className={styles.required}>*</span></label>
<input
@@ -101,6 +128,18 @@ export default function ReservationModal({ cars, reservation, onSave, onDelete,
required
/>
</div>
<div className={styles.field}>
<label className={styles.label}>終了時間帯</label>
<select
className={styles.select}
value={endPeriod}
onChange={(e) => setEndPeriod(e.target.value)}
>
{PERIOD_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
</div>
<div className={styles.field}>

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { format, addDays, startOfWeek, parseISO, differenceInDays, isSameDay } from 'date-fns';
import { ja } from 'date-fns/locale';
import { api } from '../api.js';
import { isInspectionExpirySoon } from '../utils/carUtils.js';
import { isInspectionExpirySoon, formatDateRange, formatReservationTooltip } from '../utils/carUtils.js';
import ReservationModal from './ReservationModal.jsx';
import styles from './ScheduleView.module.css';
@@ -552,14 +552,14 @@ export default function ScheduleView({ reloadKey = 0 }) {
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, reservation: r });
}}
title={`${r.customer_name || '予約'}\n${r.start_date}${r.end_date}${r.notes ? '\n' + r.notes : ''}`}
title={formatReservationTooltip(r)}
>
<span className={styles.blockText}>
{r.customer_name || '予約'}
</span>
{width > 80 && (
<span className={styles.blockDates}>
{r.start_date.slice(5)} {r.end_date.slice(5)}
{formatDateRange(r.start_date, r.start_period, r.end_date, r.end_period)}
</span>
)}
</div>

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { format, addDays, addMonths, startOfMonth, endOfMonth, parseISO, differenceInDays } from 'date-fns';
import { ja } from 'date-fns/locale';
import { api } from '../api.js';
import { formatDateRange, formatReservationTooltip } from '../utils/carUtils.js';
import ReservationModal from './ReservationModal.jsx';
import styles from './TimelineView.module.css';
@@ -287,14 +288,14 @@ export default function TimelineView({ reloadKey = 0 }) {
e.stopPropagation();
setContextMenu({ x: e.clientX, y: e.clientY, reservation: r });
}}
title={`${r.customer_name || '予約'}\n${r.start_date}${r.end_date}${r.notes ? '\n' + r.notes : ''}`}
title={formatReservationTooltip(r)}
>
<span className={styles.barText}>
{r.customer_name || '予約'}
</span>
{width > 80 && (
<span className={styles.barDates}>
{r.start_date.slice(5)} {r.end_date.slice(5)}
{formatDateRange(r.start_date, r.start_period, r.end_date, r.end_period)}
</span>
)}
</div>

View File

@@ -11,3 +11,28 @@ export function isInspectionExpirySoon(inspectionExpiry) {
oneMonthLater.setMonth(oneMonthLater.getMonth() + 1);
return expiry <= oneMonthLater;
}
/**
* Formats a date range with optional AM/PM periods into a display string.
* @param {string} startDate - ISO date string (YYYY-MM-DD)
* @param {string} startPeriod - '午前', '午後', or ''
* @param {string} endDate - ISO date string (YYYY-MM-DD)
* @param {string} endPeriod - '午前', '午後', or ''
* @returns {string}
*/
export function formatDateRange(startDate, startPeriod, endDate, endPeriod) {
const start = startDate.slice(5) + (startPeriod ? ' ' + startPeriod : '');
const end = endDate.slice(5) + (endPeriod ? ' ' + endPeriod : '');
return `${start}${end}`;
}
/**
* Formats a reservation tooltip string with full dates and optional periods.
* @param {object} r - reservation object
* @returns {string}
*/
export function formatReservationTooltip(r) {
const start = r.start_date + (r.start_period ? ' ' + r.start_period : '');
const end = r.end_date + (r.end_period ? ' ' + r.end_period : '');
return `${r.customer_name || '予約'}\n${start}${end}${r.notes ? '\n' + r.notes : ''}`;
}