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:
committed by
GitHub
parent
2e9e100178
commit
675e5f6fe8
@@ -64,15 +64,28 @@ if (!carCols.includes('has_etc')) {
|
|||||||
if (!carCols.includes('tire_type')) {
|
if (!carCols.includes('tire_type')) {
|
||||||
db.exec("ALTER TABLE cars ADD COLUMN tire_type TEXT DEFAULT 'ノーマル'");
|
db.exec("ALTER TABLE cars ADD COLUMN tire_type TEXT DEFAULT 'ノーマル'");
|
||||||
}
|
}
|
||||||
|
if (!carCols.includes('sort_order')) {
|
||||||
|
db.exec('ALTER TABLE cars ADD COLUMN sort_order INTEGER DEFAULT 0');
|
||||||
|
db.exec('UPDATE cars SET sort_order = id');
|
||||||
|
}
|
||||||
db.prepare("UPDATE cars SET tire_type = 'スタッドレス' WHERE tire_type = 'スタットレス'").run();
|
db.prepare("UPDATE cars SET tire_type = 'スタッドレス' WHERE tire_type = 'スタットレス'").run();
|
||||||
|
|
||||||
|
// Migrate: add period fields to reservations if they don't exist yet
|
||||||
|
const resCols = db.prepare("PRAGMA table_info(reservations)").all().map((c) => c.name);
|
||||||
|
if (!resCols.includes('start_period')) {
|
||||||
|
db.exec("ALTER TABLE reservations ADD COLUMN start_period TEXT DEFAULT ''");
|
||||||
|
}
|
||||||
|
if (!resCols.includes('end_period')) {
|
||||||
|
db.exec("ALTER TABLE reservations ADD COLUMN end_period TEXT DEFAULT ''");
|
||||||
|
}
|
||||||
|
|
||||||
// Seed some initial cars if none exist
|
// Seed some initial cars if none exist
|
||||||
const carCount = db.prepare('SELECT COUNT(*) as cnt FROM cars').get();
|
const carCount = db.prepare('SELECT COUNT(*) as cnt FROM cars').get();
|
||||||
if (carCount.cnt === 0) {
|
if (carCount.cnt === 0) {
|
||||||
const insertCar = db.prepare('INSERT INTO cars (name, description) VALUES (?, ?)');
|
const insertCar = db.prepare('INSERT INTO cars (name, description, sort_order) VALUES (?, ?, ?)');
|
||||||
insertCar.run('代車 A', '');
|
insertCar.run('代車 A', '', 1);
|
||||||
insertCar.run('代車 B', '');
|
insertCar.run('代車 B', '', 2);
|
||||||
insertCar.run('代車 C', '');
|
insertCar.run('代車 C', '', 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WebSocket Server ---
|
// --- WebSocket Server ---
|
||||||
@@ -107,7 +120,7 @@ wss.on('connection', (ws) => {
|
|||||||
|
|
||||||
// --- Cars API ---
|
// --- Cars API ---
|
||||||
app.get('/api/cars', (req, res) => {
|
app.get('/api/cars', (req, res) => {
|
||||||
const cars = db.prepare('SELECT * FROM cars ORDER BY id').all().map(normalizeCar);
|
const cars = db.prepare('SELECT * FROM cars ORDER BY sort_order, id').all().map(normalizeCar);
|
||||||
res.json(cars);
|
res.json(cars);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,14 +129,31 @@ app.post('/api/cars', (req, res) => {
|
|||||||
if (!name || !name.trim()) {
|
if (!name || !name.trim()) {
|
||||||
return res.status(400).json({ error: '車名は必須です' });
|
return res.status(400).json({ error: '車名は必須です' });
|
||||||
}
|
}
|
||||||
|
const maxOrder = db.prepare('SELECT MAX(sort_order) as m FROM cars').get().m ?? 0;
|
||||||
const result = db.prepare(
|
const result = db.prepare(
|
||||||
'INSERT INTO cars (name, description, inspection_expiry, has_etc, tire_type) VALUES (?, ?, ?, ?, ?)'
|
'INSERT INTO cars (name, description, inspection_expiry, has_etc, tire_type, sort_order) VALUES (?, ?, ?, ?, ?, ?)'
|
||||||
).run(name.trim(), description, inspection_expiry, has_etc ? 1 : 0, tire_type);
|
).run(name.trim(), description, inspection_expiry, has_etc ? 1 : 0, tire_type, maxOrder + 1);
|
||||||
const car = normalizeCar(db.prepare('SELECT * FROM cars WHERE id = ?').get(result.lastInsertRowid));
|
const car = normalizeCar(db.prepare('SELECT * FROM cars WHERE id = ?').get(result.lastInsertRowid));
|
||||||
broadcast({ type: 'data_changed', entity: 'cars' });
|
broadcast({ type: 'data_changed', entity: 'cars' });
|
||||||
res.status(201).json(car);
|
res.status(201).json(car);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.put('/api/cars/reorder', (req, res) => {
|
||||||
|
const { ids } = req.body;
|
||||||
|
if (!Array.isArray(ids) || ids.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'ids は配列で指定してください' });
|
||||||
|
}
|
||||||
|
const updateOrder = db.prepare('UPDATE cars SET sort_order = ? WHERE id = ?');
|
||||||
|
const transaction = db.transaction((orderedIds) => {
|
||||||
|
orderedIds.forEach((id, index) => {
|
||||||
|
updateOrder.run(index + 1, id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
transaction(ids);
|
||||||
|
broadcast({ type: 'data_changed', entity: 'cars' });
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
app.put('/api/cars/:id', (req, res) => {
|
app.put('/api/cars/:id', (req, res) => {
|
||||||
const { name, description, inspection_expiry, has_etc, tire_type } = req.body;
|
const { name, description, inspection_expiry, has_etc, tire_type } = req.body;
|
||||||
if (!name || !name.trim()) {
|
if (!name || !name.trim()) {
|
||||||
@@ -156,7 +186,7 @@ app.get('/api/reservations', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/reservations', (req, res) => {
|
app.post('/api/reservations', (req, res) => {
|
||||||
const { car_id, start_date, end_date, customer_name = '', notes = '' } = req.body;
|
const { car_id, start_date, end_date, customer_name = '', notes = '', start_period = '', end_period = '' } = req.body;
|
||||||
if (!car_id || !start_date || !end_date) {
|
if (!car_id || !start_date || !end_date) {
|
||||||
return res.status(400).json({ error: 'car_id, start_date, end_date は必須です' });
|
return res.status(400).json({ error: 'car_id, start_date, end_date は必須です' });
|
||||||
}
|
}
|
||||||
@@ -164,15 +194,15 @@ app.post('/api/reservations', (req, res) => {
|
|||||||
return res.status(400).json({ error: '開始日は終了日以前である必要があります' });
|
return res.status(400).json({ error: '開始日は終了日以前である必要があります' });
|
||||||
}
|
}
|
||||||
const result = db.prepare(
|
const result = db.prepare(
|
||||||
'INSERT INTO reservations (car_id, start_date, end_date, customer_name, notes) VALUES (?, ?, ?, ?, ?)'
|
'INSERT INTO reservations (car_id, start_date, end_date, customer_name, notes, start_period, end_period) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
).run(car_id, start_date, end_date, customer_name, notes);
|
).run(car_id, start_date, end_date, customer_name, notes, start_period, end_period);
|
||||||
const reservation = db.prepare('SELECT * FROM reservations WHERE id = ?').get(result.lastInsertRowid);
|
const reservation = db.prepare('SELECT * FROM reservations WHERE id = ?').get(result.lastInsertRowid);
|
||||||
broadcast({ type: 'data_changed', entity: 'reservations' });
|
broadcast({ type: 'data_changed', entity: 'reservations' });
|
||||||
res.status(201).json(reservation);
|
res.status(201).json(reservation);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put('/api/reservations/:id', (req, res) => {
|
app.put('/api/reservations/:id', (req, res) => {
|
||||||
const { car_id, start_date, end_date, customer_name, notes } = req.body;
|
const { car_id, start_date, end_date, customer_name, notes, start_period, end_period } = req.body;
|
||||||
if (!car_id || !start_date || !end_date) {
|
if (!car_id || !start_date || !end_date) {
|
||||||
return res.status(400).json({ error: 'car_id, start_date, end_date は必須です' });
|
return res.status(400).json({ error: 'car_id, start_date, end_date は必須です' });
|
||||||
}
|
}
|
||||||
@@ -180,8 +210,8 @@ app.put('/api/reservations/:id', (req, res) => {
|
|||||||
return res.status(400).json({ error: '開始日は終了日以前である必要があります' });
|
return res.status(400).json({ error: '開始日は終了日以前である必要があります' });
|
||||||
}
|
}
|
||||||
const result = db.prepare(
|
const result = db.prepare(
|
||||||
'UPDATE reservations SET car_id = ?, start_date = ?, end_date = ?, customer_name = ?, notes = ? WHERE id = ?'
|
'UPDATE reservations SET car_id = ?, start_date = ?, end_date = ?, customer_name = ?, notes = ?, start_period = ?, end_period = ? WHERE id = ?'
|
||||||
).run(car_id, start_date, end_date, customer_name ?? '', notes ?? '', req.params.id);
|
).run(car_id, start_date, end_date, customer_name ?? '', notes ?? '', start_period ?? '', end_period ?? '', req.params.id);
|
||||||
if (result.changes === 0) {
|
if (result.changes === 0) {
|
||||||
return res.status(404).json({ error: '予約が見つかりません' });
|
return res.status(404).json({ error: '予約が見つかりません' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const api = {
|
|||||||
createCar: (data) => request('/cars', { method: 'POST', body: JSON.stringify(data) }),
|
createCar: (data) => request('/cars', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
updateCar: (id, data) => request(`/cars/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
updateCar: (id, data) => request(`/cars/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
deleteCar: (id) => request(`/cars/${id}`, { method: 'DELETE' }),
|
deleteCar: (id) => request(`/cars/${id}`, { method: 'DELETE' }),
|
||||||
|
reorderCars: (ids) => request('/cars/reorder', { method: 'PUT', body: JSON.stringify({ ids }) }),
|
||||||
|
|
||||||
// Reservations
|
// Reservations
|
||||||
getReservations: () => request('/reservations'),
|
getReservations: () => request('/reservations'),
|
||||||
|
|||||||
@@ -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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h2 className={styles.heading}>代車管理</h2>
|
<h2 className={styles.heading}>代車管理</h2>
|
||||||
@@ -187,7 +201,7 @@ export default function CarManagement({ reloadKey = 0 }) {
|
|||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>順番</th>
|
||||||
<th>車名</th>
|
<th>車名</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>
|
<td colSpan={7} className={styles.empty}>代車がありません</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{cars.map((car) => (
|
{cars.map((car, carIdx) => (
|
||||||
<tr key={car.id}>
|
<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 ? (
|
{editingId === car.id ? (
|
||||||
<>
|
<>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -113,7 +113,34 @@
|
|||||||
|
|
||||||
.idCell {
|
.idCell {
|
||||||
color: #9ca3af;
|
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 {
|
.descCell {
|
||||||
|
|||||||
@@ -2,12 +2,20 @@ import { useState, useEffect } from 'react';
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import styles from './ReservationModal.module.css';
|
import styles from './ReservationModal.module.css';
|
||||||
|
|
||||||
|
const PERIOD_OPTIONS = [
|
||||||
|
{ value: '', label: '指定なし' },
|
||||||
|
{ value: '午前', label: '午前' },
|
||||||
|
{ value: '午後', label: '午後' },
|
||||||
|
];
|
||||||
|
|
||||||
export default function ReservationModal({ cars, reservation, onSave, onDelete, onClose }) {
|
export default function ReservationModal({ cars, reservation, onSave, onDelete, onClose }) {
|
||||||
const isEdit = !!reservation?.id;
|
const isEdit = !!reservation?.id;
|
||||||
|
|
||||||
const [carId, setCarId] = useState('');
|
const [carId, setCarId] = useState('');
|
||||||
const [startDate, setStartDate] = useState('');
|
const [startDate, setStartDate] = useState('');
|
||||||
|
const [startPeriod, setStartPeriod] = useState('');
|
||||||
const [endDate, setEndDate] = useState('');
|
const [endDate, setEndDate] = useState('');
|
||||||
|
const [endPeriod, setEndPeriod] = useState('');
|
||||||
const [customerName, setCustomerName] = useState('');
|
const [customerName, setCustomerName] = useState('');
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -16,7 +24,9 @@ export default function ReservationModal({ cars, reservation, onSave, onDelete,
|
|||||||
if (reservation) {
|
if (reservation) {
|
||||||
setCarId(String(reservation.car_id || (cars[0]?.id ?? '')));
|
setCarId(String(reservation.car_id || (cars[0]?.id ?? '')));
|
||||||
setStartDate(reservation.start_date || format(new Date(), 'yyyy-MM-dd'));
|
setStartDate(reservation.start_date || format(new Date(), 'yyyy-MM-dd'));
|
||||||
|
setStartPeriod(reservation.start_period || '');
|
||||||
setEndDate(reservation.end_date || format(new Date(), 'yyyy-MM-dd'));
|
setEndDate(reservation.end_date || format(new Date(), 'yyyy-MM-dd'));
|
||||||
|
setEndPeriod(reservation.end_period || '');
|
||||||
setCustomerName(reservation.customer_name || '');
|
setCustomerName(reservation.customer_name || '');
|
||||||
setNotes(reservation.notes || '');
|
setNotes(reservation.notes || '');
|
||||||
}
|
}
|
||||||
@@ -34,7 +44,9 @@ export default function ReservationModal({ cars, reservation, onSave, onDelete,
|
|||||||
await onSave({
|
await onSave({
|
||||||
car_id: Number(carId),
|
car_id: Number(carId),
|
||||||
start_date: startDate,
|
start_date: startDate,
|
||||||
|
start_period: startPeriod,
|
||||||
end_date: endDate,
|
end_date: endDate,
|
||||||
|
end_period: endPeriod,
|
||||||
customer_name: customerName,
|
customer_name: customerName,
|
||||||
notes,
|
notes,
|
||||||
});
|
});
|
||||||
@@ -90,6 +102,21 @@ export default function ReservationModal({ cars, reservation, onSave, onDelete,
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}>
|
<div className={styles.field}>
|
||||||
<label className={styles.label}>終了日 <span className={styles.required}>*</span></label>
|
<label className={styles.label}>終了日 <span className={styles.required}>*</span></label>
|
||||||
<input
|
<input
|
||||||
@@ -101,6 +128,18 @@ export default function ReservationModal({ cars, reservation, onSave, onDelete,
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|||||||
import { format, addDays, startOfWeek, parseISO, differenceInDays, isSameDay } from 'date-fns';
|
import { format, addDays, startOfWeek, parseISO, differenceInDays, isSameDay } from 'date-fns';
|
||||||
import { ja } from 'date-fns/locale';
|
import { ja } from 'date-fns/locale';
|
||||||
import { api } from '../api.js';
|
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 ReservationModal from './ReservationModal.jsx';
|
||||||
import styles from './ScheduleView.module.css';
|
import styles from './ScheduleView.module.css';
|
||||||
|
|
||||||
@@ -552,14 +552,14 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setContextMenu({ x: e.clientX, y: e.clientY, reservation: r });
|
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}>
|
<span className={styles.blockText}>
|
||||||
{r.customer_name || '予約'}
|
{r.customer_name || '予約'}
|
||||||
</span>
|
</span>
|
||||||
{width > 80 && (
|
{width > 80 && (
|
||||||
<span className={styles.blockDates}>
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|||||||
import { format, addDays, addMonths, startOfMonth, endOfMonth, parseISO, differenceInDays } from 'date-fns';
|
import { format, addDays, addMonths, startOfMonth, endOfMonth, parseISO, differenceInDays } from 'date-fns';
|
||||||
import { ja } from 'date-fns/locale';
|
import { ja } from 'date-fns/locale';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
|
import { formatDateRange, formatReservationTooltip } from '../utils/carUtils.js';
|
||||||
import ReservationModal from './ReservationModal.jsx';
|
import ReservationModal from './ReservationModal.jsx';
|
||||||
import styles from './TimelineView.module.css';
|
import styles from './TimelineView.module.css';
|
||||||
|
|
||||||
@@ -287,14 +288,14 @@ export default function TimelineView({ reloadKey = 0 }) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setContextMenu({ x: e.clientX, y: e.clientY, reservation: r });
|
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}>
|
<span className={styles.barText}>
|
||||||
{r.customer_name || '予約'}
|
{r.customer_name || '予約'}
|
||||||
</span>
|
</span>
|
||||||
{width > 80 && (
|
{width > 80 && (
|
||||||
<span className={styles.barDates}>
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,3 +11,28 @@ export function isInspectionExpirySoon(inspectionExpiry) {
|
|||||||
oneMonthLater.setMonth(oneMonthLater.getMonth() + 1);
|
oneMonthLater.setMonth(oneMonthLater.getMonth() + 1);
|
||||||
return expiry <= oneMonthLater;
|
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 : ''}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user