Compare commits
8 Commits
copilot/up
...
copilot/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208dddeeb3 | ||
| b597453fac | |||
|
|
696e1b9c9f | ||
|
|
a7daf19cdd | ||
|
|
8588463244 | ||
|
|
01a1dd837f | ||
|
|
85b26ca04e | ||
|
|
8542d092fb |
@@ -68,7 +68,6 @@ 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();
|
||||
|
||||
// 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);
|
||||
@@ -100,17 +99,13 @@ function broadcast(message) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeTireType(value) {
|
||||
return value === 'スタットレス' ? 'スタッドレス' : value;
|
||||
}
|
||||
|
||||
// for future use
|
||||
function normalizeCar(car) {
|
||||
if (!car) {
|
||||
return car;
|
||||
}
|
||||
return {
|
||||
...car,
|
||||
tire_type: normalizeTireType(car.tire_type),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import { isInspectionExpirySoon } from '../utils/carUtils.js';
|
||||
import styles from './CarManagement.module.css';
|
||||
@@ -20,6 +20,8 @@ export default function CarManagement({ reloadKey = 0 }) {
|
||||
const [editEtc, setEditEtc] = useState(false);
|
||||
const [editTire, setEditTire] = useState('ノーマル');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [dragOverIdx, setDragOverIdx] = useState(null);
|
||||
const dragSrcIdx = useRef(null);
|
||||
|
||||
const loadCars = useCallback(async () => {
|
||||
try {
|
||||
@@ -116,11 +118,7 @@ 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]];
|
||||
const applyReorder = async (newCars) => {
|
||||
setCars(newCars);
|
||||
try {
|
||||
await api.reorderCars(newCars.map((c) => c.id));
|
||||
@@ -130,6 +128,40 @@ export default function CarManagement({ reloadKey = 0 }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReorder = async (index, direction) => {
|
||||
const swapIndex = index + direction;
|
||||
if (swapIndex < 0 || swapIndex >= cars.length) return;
|
||||
const newCars = [...cars];
|
||||
[newCars[index], newCars[swapIndex]] = [newCars[swapIndex], newCars[index]];
|
||||
await applyReorder(newCars);
|
||||
};
|
||||
|
||||
const handleDragStart = (index) => {
|
||||
dragSrcIdx.current = index;
|
||||
};
|
||||
|
||||
const handleDragOver = (e, index) => {
|
||||
e.preventDefault();
|
||||
setDragOverIdx(index);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDragOverIdx(null);
|
||||
dragSrcIdx.current = null;
|
||||
};
|
||||
|
||||
const handleDrop = async (e, dropIndex) => {
|
||||
e.preventDefault();
|
||||
const srcIndex = dragSrcIdx.current;
|
||||
setDragOverIdx(null);
|
||||
dragSrcIdx.current = null;
|
||||
if (srcIndex === null || srcIndex === dropIndex) return;
|
||||
const newCars = [...cars];
|
||||
const [moved] = newCars.splice(srcIndex, 1);
|
||||
newCars.splice(dropIndex, 0, moved);
|
||||
await applyReorder(newCars);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.heading}>代車管理</h2>
|
||||
@@ -217,7 +249,15 @@ export default function CarManagement({ reloadKey = 0 }) {
|
||||
</tr>
|
||||
)}
|
||||
{cars.map((car, carIdx) => (
|
||||
<tr key={car.id}>
|
||||
<tr
|
||||
key={car.id}
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(carIdx)}
|
||||
onDragOver={(e) => handleDragOver(e, carIdx)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDrop={(e) => handleDrop(e, carIdx)}
|
||||
className={dragOverIdx === carIdx ? styles.dragOver : ''}
|
||||
>
|
||||
<td className={styles.idCell}>
|
||||
<div className={styles.orderBtns}>
|
||||
<button
|
||||
|
||||
@@ -111,6 +111,16 @@
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.table tbody tr[draggable] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.dragOver {
|
||||
background: #eff6ff !important;
|
||||
outline: 2px dashed #1a56db;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.idCell {
|
||||
color: #9ca3af;
|
||||
width: 80px;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { format, addDays, startOfWeek, parseISO, differenceInDays, isSameDay } from 'date-fns';
|
||||
import { format, addDays, parseISO, differenceInDays } from 'date-fns';
|
||||
import { ja } from 'date-fns/locale';
|
||||
import { api } from '../api.js';
|
||||
import { isInspectionExpirySoon, formatDateRange, formatReservationTooltip } from '../utils/carUtils.js';
|
||||
import ReservationModal from './ReservationModal.jsx';
|
||||
import styles from './ScheduleView.module.css';
|
||||
|
||||
const CELL_WIDTH = 52; // px per day column
|
||||
const CELL_WIDTH = 52; // px per half-day slot (午前 or 午後)
|
||||
const ROW_HEIGHT = 64; // px per car row
|
||||
const LABEL_WIDTH = 140; // px for car name column
|
||||
const HEADER_HEIGHT = 72; // px for the date header row
|
||||
const HEADER_HEIGHT = 80; // px for the date header row (top: date+dow, bottom: 午前/午後)
|
||||
const DAYS_SHOWN = 21; // number of days to show
|
||||
const HALF_SLOTS = DAYS_SHOWN * 2; // total half-day slot columns (AM + PM per day)
|
||||
|
||||
// Detect touch-primary device to disable mouse-only drag & drop
|
||||
const isTouchDevice = typeof window !== 'undefined' &&
|
||||
@@ -42,10 +43,8 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// The first date shown in the grid
|
||||
const [viewStart, setViewStart] = useState(() =>
|
||||
startOfWeek(new Date(), { weekStartsOn: 1 })
|
||||
);
|
||||
// The first date shown in the grid (start from today)
|
||||
const [viewStart, setViewStart] = useState(() => new Date());
|
||||
|
||||
// Drag-to-create state
|
||||
const [creating, setCreating] = useState(null);
|
||||
@@ -91,17 +90,18 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
// --- Navigation ---
|
||||
const prevWeek = () => setViewStart((d) => addDays(d, -7));
|
||||
const nextWeek = () => setViewStart((d) => addDays(d, 7));
|
||||
const goToday = () => setViewStart(startOfWeek(new Date(), { weekStartsOn: 1 }));
|
||||
const goToday = () => setViewStart(new Date());
|
||||
|
||||
// --- Grid position helpers ---
|
||||
// Given a mouse clientX within the grid scroll area, get the day index (0-based)
|
||||
const getColFromX = useCallback((clientX) => {
|
||||
// Given a mouse clientX within the grid scroll area, get the half-day slot index (0-based)
|
||||
// Each slot is CELL_WIDTH wide; even slots = 午前, odd slots = 午後
|
||||
const getSlotFromX = useCallback((clientX) => {
|
||||
if (!gridRef.current) return -1;
|
||||
const rect = gridRef.current.getBoundingClientRect();
|
||||
const scrollLeft = gridRef.current.scrollLeft;
|
||||
const x = clientX - rect.left + scrollLeft - LABEL_WIDTH;
|
||||
if (x < 0) return -1;
|
||||
return Math.floor(x / CELL_WIDTH);
|
||||
return Math.min(Math.floor(x / CELL_WIDTH), HALF_SLOTS - 1);
|
||||
}, []);
|
||||
|
||||
const getRowFromY = useCallback((clientY) => {
|
||||
@@ -114,59 +114,62 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
}, []);
|
||||
|
||||
// --- Cell drag to create ---
|
||||
const handleCellMouseDown = (e, carId, dateStr) => {
|
||||
const handleCellMouseDown = (e, carId, slot) => {
|
||||
if (isTouchDevice) return; // drag-to-create is mouse-only
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
setCreating({ carId, startDateStr: dateStr, endDateStr: dateStr });
|
||||
setCreating({ carId, startSlot: slot, endSlot: slot });
|
||||
};
|
||||
|
||||
// --- Cell tap to create (touch devices) ---
|
||||
const handleCellClick = useCallback((e, carId) => {
|
||||
if (!isTouchDevice) return;
|
||||
const col = getColFromX(e.clientX);
|
||||
if (col >= 0 && col < DAYS_SHOWN) {
|
||||
const dateStr = dateToStr(dates[col]);
|
||||
const slot = getSlotFromX(e.clientX);
|
||||
if (slot >= 0 && slot < HALF_SLOTS) {
|
||||
const dayIdx = Math.floor(slot / 2);
|
||||
const dateStr = dateToStr(dates[dayIdx]);
|
||||
const period = slot % 2 === 0 ? '午前' : '午後';
|
||||
setModal({
|
||||
mode: 'create',
|
||||
prefill: { car_id: carId, start_date: dateStr, end_date: dateStr },
|
||||
prefill: { car_id: carId, start_date: dateStr, start_period: period, end_date: dateStr, end_period: period },
|
||||
});
|
||||
}
|
||||
}, [dates, getColFromX]);
|
||||
}, [dates, getSlotFromX]);
|
||||
|
||||
const handleGridMouseMove = useCallback((e) => {
|
||||
if (creating) {
|
||||
const col = getColFromX(e.clientX);
|
||||
if (col >= 0 && col < DAYS_SHOWN) {
|
||||
const hoveredDate = dateToStr(dates[col]);
|
||||
const slot = getSlotFromX(e.clientX);
|
||||
if (slot >= 0 && slot < HALF_SLOTS) {
|
||||
setCreating((prev) => {
|
||||
if (!prev) return null;
|
||||
// Ensure start <= end
|
||||
const s = prev.startDateStr;
|
||||
const h = hoveredDate;
|
||||
const s = prev.startSlot;
|
||||
const h = slot;
|
||||
return {
|
||||
...prev,
|
||||
endDateStr: h >= s ? h : s,
|
||||
startDateStr: h < s ? h : prev.startDateStr,
|
||||
endSlot: h >= s ? h : s,
|
||||
startSlot: h < s ? h : prev.startSlot,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (moving) {
|
||||
const col = getColFromX(e.clientX);
|
||||
const slot = getSlotFromX(e.clientX);
|
||||
const row = getRowFromY(e.clientY);
|
||||
movingRef.current = { ...movingRef.current };
|
||||
|
||||
if (col >= 0 && col < DAYS_SHOWN) {
|
||||
const newStartCol = Math.max(0, col - moving.grabDayOffset);
|
||||
const duration = differenceInDays(
|
||||
parseISO(moving.reservation.end_date),
|
||||
parseISO(moving.reservation.start_date)
|
||||
);
|
||||
const clampedStartCol = Math.min(newStartCol, DAYS_SHOWN - 1 - duration);
|
||||
const newStartDate = dateToStr(dates[Math.max(0, clampedStartCol)]);
|
||||
const newEndDate = dateToStr(addDays(dates[Math.max(0, clampedStartCol)], duration));
|
||||
if (slot >= 0 && slot < HALF_SLOTS) {
|
||||
const durationSlots = moving.durationSlots;
|
||||
const newStartSlot = Math.max(0, slot - moving.grabSlotOffset);
|
||||
const clampedStartSlot = Math.min(newStartSlot, HALF_SLOTS - 1 - durationSlots);
|
||||
const clampedEndSlot = clampedStartSlot + durationSlots;
|
||||
|
||||
const newStartDayIdx = Math.max(0, Math.min(Math.floor(clampedStartSlot / 2), DAYS_SHOWN - 1));
|
||||
const newEndDayIdx = Math.max(0, Math.min(Math.floor(clampedEndSlot / 2), DAYS_SHOWN - 1));
|
||||
const newStartDate = dateToStr(dates[newStartDayIdx]);
|
||||
const newStartPeriod = clampedStartSlot % 2 === 0 ? '午前' : '午後';
|
||||
const newEndDate = dateToStr(dates[newEndDayIdx]);
|
||||
const newEndPeriod = clampedEndSlot % 2 === 0 ? '午前' : '午後';
|
||||
|
||||
let newCarId = moving.currentCarId;
|
||||
if (row >= 0 && row < cars.length) {
|
||||
@@ -176,54 +179,56 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
setMoving((prev) => prev ? {
|
||||
...prev,
|
||||
currentCarId: newCarId,
|
||||
currentStartSlot: clampedStartSlot,
|
||||
currentStartDate: newStartDate,
|
||||
currentStartPeriod: newStartPeriod,
|
||||
currentEndDate: newEndDate,
|
||||
col: clampedStartCol,
|
||||
currentEndPeriod: newEndPeriod,
|
||||
col: clampedStartSlot,
|
||||
row: row >= 0 && row < cars.length ? row : prev.row,
|
||||
} : null);
|
||||
}
|
||||
}
|
||||
}, [creating, moving, dates, cars, getColFromX, getRowFromY]);
|
||||
}, [creating, moving, dates, cars, getSlotFromX, getRowFromY]);
|
||||
|
||||
const handleGridMouseUp = useCallback(async (e) => {
|
||||
if (creating) {
|
||||
const { carId, startDateStr, endDateStr } = creating;
|
||||
const { carId, startSlot, endSlot } = creating;
|
||||
setCreating(null);
|
||||
const startDayIdx = Math.max(0, Math.min(Math.floor(startSlot / 2), DAYS_SHOWN - 1));
|
||||
const endDayIdx = Math.max(0, Math.min(Math.floor(endSlot / 2), DAYS_SHOWN - 1));
|
||||
// Open modal to confirm/fill details
|
||||
setModal({
|
||||
mode: 'create',
|
||||
prefill: {
|
||||
car_id: carId,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
start_date: dateToStr(dates[startDayIdx]),
|
||||
start_period: startSlot % 2 === 0 ? '午前' : '午後',
|
||||
end_date: dateToStr(dates[endDayIdx]),
|
||||
end_period: endSlot % 2 === 0 ? '午前' : '午後',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (moving) {
|
||||
const { reservation, currentCarId, currentStartDate, currentEndDate } = moving;
|
||||
const { reservation, currentCarId, origStartSlot, currentStartSlot, currentStartDate, currentStartPeriod, currentEndDate, currentEndPeriod } = moving;
|
||||
setMoving(null);
|
||||
movingRef.current = null;
|
||||
|
||||
const duration = differenceInDays(
|
||||
parseISO(reservation.end_date),
|
||||
parseISO(reservation.start_date)
|
||||
);
|
||||
const newEndDate = currentEndDate ||
|
||||
dateToStr(addDays(parseISO(currentStartDate), duration));
|
||||
|
||||
// Only update if something changed
|
||||
if (
|
||||
currentCarId !== reservation.car_id ||
|
||||
currentStartDate !== reservation.start_date
|
||||
currentStartSlot !== origStartSlot
|
||||
) {
|
||||
try {
|
||||
await api.updateReservation(reservation.id, {
|
||||
car_id: currentCarId,
|
||||
start_date: currentStartDate,
|
||||
end_date: newEndDate,
|
||||
end_date: currentEndDate,
|
||||
customer_name: reservation.customer_name,
|
||||
notes: reservation.notes,
|
||||
start_period: currentStartPeriod,
|
||||
end_period: currentEndPeriod,
|
||||
});
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
@@ -232,7 +237,7 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [creating, moving, loadData]);
|
||||
}, [creating, moving, dates, loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', handleGridMouseMove);
|
||||
@@ -263,18 +268,28 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
|
||||
const col = getColFromX(e.clientX);
|
||||
const startCol = Math.max(0, dates.findIndex((d) => dateToStr(d) === reservation.start_date));
|
||||
const grabOffset = col >= 0 ? col - startCol : 0;
|
||||
const slot = getSlotFromX(e.clientX);
|
||||
// Compute the reservation's start/end slot within the current view
|
||||
const startDayCol = Math.max(0, differenceInDays(parseISO(reservation.start_date), viewStart));
|
||||
const endDayCol = Math.max(0, differenceInDays(parseISO(reservation.end_date), viewStart));
|
||||
const startSlot = startDayCol * 2 + (reservation.start_period === '午後' ? 1 : 0);
|
||||
const endSlot = endDayCol * 2 + (reservation.end_period === '午前' ? 0 : 1);
|
||||
const durationSlots = Math.max(0, endSlot - startSlot);
|
||||
const grabOffset = slot >= 0 ? slot - startSlot : 0;
|
||||
const carRow = cars.findIndex((c) => c.id === reservation.car_id);
|
||||
|
||||
setMoving({
|
||||
reservation,
|
||||
grabDayOffset: Math.max(0, grabOffset),
|
||||
grabSlotOffset: Math.max(0, grabOffset),
|
||||
durationSlots,
|
||||
origStartSlot: startSlot,
|
||||
currentCarId: reservation.car_id,
|
||||
currentStartSlot: startSlot,
|
||||
currentStartDate: reservation.start_date,
|
||||
currentStartPeriod: reservation.start_period || '午前',
|
||||
currentEndDate: reservation.end_date,
|
||||
col: startCol,
|
||||
currentEndPeriod: reservation.end_period || '午後',
|
||||
col: startSlot,
|
||||
row: carRow,
|
||||
});
|
||||
};
|
||||
@@ -313,26 +328,26 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
(r) => r.end_date >= viewStartStr && r.start_date <= viewEndStr
|
||||
);
|
||||
|
||||
// For each reservation, calculate its left/width in the grid
|
||||
// For each reservation, calculate its left/width in the grid (accounting for 午前/午後 periods)
|
||||
function getReservationLayout(r) {
|
||||
const rStart = r.start_date < viewStartStr ? viewStartStr : r.start_date;
|
||||
const rEnd = r.end_date > viewEndStr ? viewEndStr : r.end_date;
|
||||
const startCol = differenceInDays(parseISO(rStart), viewStart);
|
||||
const endCol = differenceInDays(parseISO(rEnd), viewStart);
|
||||
const left = startCol * CELL_WIDTH;
|
||||
const width = (endCol - startCol + 1) * CELL_WIDTH;
|
||||
return { left, width, startCol, endCol };
|
||||
const startDayCol = differenceInDays(parseISO(rStart), viewStart);
|
||||
const endDayCol = differenceInDays(parseISO(rEnd), viewStart);
|
||||
// Even slot = 午前, odd slot = 午後; empty period treated as 午前 for start, 午後 for end
|
||||
const startSlot = startDayCol * 2 + (r.start_period === '午後' ? 1 : 0);
|
||||
const endSlot = endDayCol * 2 + (r.end_period === '午前' ? 0 : 1);
|
||||
const left = startSlot * CELL_WIDTH;
|
||||
const width = (endSlot - startSlot + 1) * CELL_WIDTH;
|
||||
return { left, width, startSlot, endSlot };
|
||||
}
|
||||
|
||||
// Create ghost for currently moving reservation
|
||||
const movingGhost = moving ? (() => {
|
||||
const duration = differenceInDays(
|
||||
parseISO(moving.reservation.end_date),
|
||||
parseISO(moving.reservation.start_date)
|
||||
);
|
||||
const col = moving.col ?? 0;
|
||||
const durationSlots = moving.durationSlots;
|
||||
const left = col * CELL_WIDTH;
|
||||
const width = (duration + 1) * CELL_WIDTH;
|
||||
const width = (durationSlots + 1) * CELL_WIDTH;
|
||||
return { col, left, width, row: moving.row };
|
||||
})() : null;
|
||||
|
||||
@@ -392,7 +407,7 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
)}
|
||||
<div
|
||||
className={styles.grid}
|
||||
style={{ width: LABEL_WIDTH + DAYS_SHOWN * CELL_WIDTH }}
|
||||
style={{ width: LABEL_WIDTH + DAYS_SHOWN * 2 * CELL_WIDTH }}
|
||||
>
|
||||
{/* Header row */}
|
||||
<div className={styles.headerRow} style={{ height: HEADER_HEIGHT }}>
|
||||
@@ -401,7 +416,7 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
className={styles.cornerCell}
|
||||
style={{ width: LABEL_WIDTH, height: HEADER_HEIGHT }}
|
||||
/>
|
||||
{/* Date headers */}
|
||||
{/* Date headers — each day spans two half-day slots (午前 + 午後) */}
|
||||
{dates.map((date) => {
|
||||
const ds = dateToStr(date);
|
||||
const isToday = ds === todayStr;
|
||||
@@ -410,12 +425,18 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
return (
|
||||
<div
|
||||
key={ds}
|
||||
className={`${styles.dateHeader} ${isToday ? styles.todayHeader : ''} ${isWeekend ? styles.weekendHeader : ''}`}
|
||||
style={{ width: CELL_WIDTH, height: HEADER_HEIGHT }}
|
||||
className={`${styles.dateHeaderGroup} ${isToday ? styles.todayHeader : ''} ${isWeekend ? styles.weekendHeader : ''}`}
|
||||
style={{ width: CELL_WIDTH * 2, height: HEADER_HEIGHT }}
|
||||
>
|
||||
<div className={styles.dateHeaderTop}>
|
||||
<span className={styles.dateDay}>{format(date, 'd')}</span>
|
||||
<span className={`${styles.dateDow} ${isWeekend ? styles.weekendDow : ''}`}>{dow}</span>
|
||||
</div>
|
||||
<div className={styles.dateHeaderAmPm}>
|
||||
<div className={styles.periodLabel}>午前</div>
|
||||
<div className={styles.periodLabel}>午後</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -432,10 +453,8 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
const isCreatingRow = creating && creating.carId === car.id;
|
||||
let creatingLeft = 0, creatingWidth = 0;
|
||||
if (isCreatingRow) {
|
||||
const startCol = differenceInDays(parseISO(creating.startDateStr), viewStart);
|
||||
const endCol = differenceInDays(parseISO(creating.endDateStr), viewStart);
|
||||
creatingLeft = Math.max(0, startCol) * CELL_WIDTH;
|
||||
creatingWidth = (Math.min(endCol, DAYS_SHOWN - 1) - Math.max(0, startCol) + 1) * CELL_WIDTH;
|
||||
creatingLeft = Math.max(0, creating.startSlot) * CELL_WIDTH;
|
||||
creatingWidth = (Math.min(creating.endSlot, HALF_SLOTS - 1) - Math.max(0, creating.startSlot) + 1) * CELL_WIDTH;
|
||||
}
|
||||
|
||||
// Ghost reservation for this row
|
||||
@@ -467,25 +486,25 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Day cells */}
|
||||
{/* Day cells — two per day (午前 then 午後) */}
|
||||
<div
|
||||
className={styles.cellArea}
|
||||
style={{ width: DAYS_SHOWN * CELL_WIDTH, height: ROW_HEIGHT }}
|
||||
style={{ width: DAYS_SHOWN * 2 * CELL_WIDTH, height: ROW_HEIGHT }}
|
||||
onClick={(e) => handleCellClick(e, car.id)}
|
||||
>
|
||||
{dates.map((date) => {
|
||||
{dates.flatMap((date, dayIdx) => {
|
||||
const ds = dateToStr(date);
|
||||
const isToday = ds === todayStr;
|
||||
const dow = format(date, 'E', { locale: ja });
|
||||
const isWeekend = dow === '土' || dow === '日';
|
||||
return (
|
||||
return ['午前', '午後'].map((period, pIdx) => (
|
||||
<div
|
||||
key={ds}
|
||||
className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''} ${isTouchDevice ? styles.cellTouch : ''}`}
|
||||
key={`${ds}-${period}`}
|
||||
className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''} ${isTouchDevice ? styles.cellTouch : ''} ${pIdx === 1 ? styles.cellDayEnd : ''}`}
|
||||
style={{ width: CELL_WIDTH, height: ROW_HEIGHT }}
|
||||
onMouseDown={(e) => handleCellMouseDown(e, car.id, ds)}
|
||||
onMouseDown={(e) => handleCellMouseDown(e, car.id, dayIdx * 2 + pIdx)}
|
||||
/>
|
||||
);
|
||||
));
|
||||
})}
|
||||
|
||||
{/* Creating highlight */}
|
||||
|
||||
@@ -124,6 +124,47 @@
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.dateHeaderGroup {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-right: 2px solid #d1d5db;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dateHeaderTop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.dateHeaderAmPm {
|
||||
display: flex;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.periodLabel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.periodLabel:first-child {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.dateHeader {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -281,6 +322,10 @@
|
||||
background: rgba(26, 86, 219, 0.04);
|
||||
}
|
||||
|
||||
.cellDayEnd {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.todayCell {
|
||||
background: rgba(59, 130, 246, 0.06);
|
||||
}
|
||||
|
||||
@@ -136,9 +136,9 @@ export default function TimelineView({ reloadKey = 0 }) {
|
||||
const carColorMap = {};
|
||||
cars.forEach((car, i) => { carColorMap[car.id] = getColor(i); });
|
||||
|
||||
// Sort reservations by start_date then car
|
||||
// Sort reservations by start_date descending (newest first) then car
|
||||
const sortedReservations = [...reservations].sort((a, b) => {
|
||||
if (a.start_date !== b.start_date) return a.start_date < b.start_date ? -1 : 1;
|
||||
if (a.start_date !== b.start_date) return a.start_date > b.start_date ? -1 : 1;
|
||||
return a.car_id - b.car_id;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user