14 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
675e5f6fe8 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>
2026-04-06 07:12:49 +00:00
h
2e9e100178 Commit 2026-04-06 15:36:51 +09:00
h
1081ea1074 Merge pull request #5 from pdf114514/copilot/update-vehicle-indicator-display
Updating vehicle indicator and editing interface
2026-03-22 13:26:06 +09:00
copilot-swe-agent[bot]
761c7f1971 Changes before error encountered
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/a42d4e36-a3cf-4ff7-b1cb-f076e601b1b8
2026-03-22 04:25:00 +00:00
h
0bd5efde2c Merge pull request #4 from pdf114514/copilot/add-field-and-sync-features
Implementing vehicle details and real-time editing synchronization
2026-03-22 13:00:47 +09:00
copilot-swe-agent[bot]
8e9db971d3 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
2026-03-22 03:58:07 +00:00
h
09872737b7 Merge pull request #3 from pdf114514/copilot/add-popup-for-edit-delete-reservations
Add timeline view, right-click context menu on reservations, fix trust proxy
2026-03-21 03:52:02 +09:00
copilot-swe-agent[bot]
cc3ad148fc Add timeline view, right-click context menu, and fix Express trust proxy
- backend/server.js: Add app.set('trust proxy', 1) to fix express-rate-limit
  ValidationError when app runs behind nginx reverse proxy
- ScheduleView.jsx: Add right-click context menu on reservation blocks with
  Edit and Delete options; closes on click-outside or Escape
- ScheduleView.module.css: Add context menu styles
- TimelineView.jsx: New Gantt-style monthly timeline view showing all
  reservations sorted by date, with month navigation and right-click menu
- TimelineView.module.css: Styles for the timeline view
- App.jsx: Add 'タイムライン' tab to navigation

Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/d03ca12c-21ce-45a0-881f-919d6635e7fb
2026-03-20 18:50:51 +00:00
copilot-swe-agent[bot]
1eb96877ff Initial plan 2026-03-20 18:38:28 +00:00
h
76dc94dd78 Merge pull request #2 from pdf114514/copilot/disable-drag-drop-for-touch
Disable touch drag & drop, warn on car delete with reservations, support configurable backend URL
2026-03-21 03:21:50 +09:00
copilot-swe-agent[bot]
c3dd0cfa69 Disable touch drag & drop, warn on car delete with reservations, configure backend URL
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/cd194ca1-b339-4f2f-b717-31a0ba193964
2026-03-20 18:19:53 +00:00
copilot-swe-agent[bot]
40371b43d1 Initial plan 2026-03-20 18:13:46 +00:00
h
19953dff55 Merge pull request #1 from pdf114514/copilot/create-car-rental-schedule-system
Add car rental schedule management system (React+Vite + Express+SQLite)
2026-03-21 03:07:15 +09:00
copilot-swe-agent[bot]
50d3803610 Implement car reservation schedule management system
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/1d8c6b05-0e8d-4484-a2d8-8d427dfad9cb
2026-03-20 18:03:33 +00:00
6 changed files with 101 additions and 210 deletions

View File

@@ -68,6 +68,7 @@ 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);
@@ -99,13 +100,17 @@ function broadcast(message) {
});
}
// for future use
function normalizeTireType(value) {
return value === 'スタットレス' ? 'スタッドレス' : value;
}
function normalizeCar(car) {
if (!car) {
return car;
}
return {
...car,
tire_type: normalizeTireType(car.tire_type),
};
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useRef } 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';
@@ -20,8 +20,6 @@ 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 {
@@ -118,7 +116,11 @@ export default function CarManagement({ reloadKey = 0 }) {
}
};
const applyReorder = async (newCars) => {
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));
@@ -128,40 +130,6 @@ 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>
@@ -249,15 +217,7 @@ export default function CarManagement({ reloadKey = 0 }) {
</tr>
)}
{cars.map((car, carIdx) => (
<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 : ''}
>
<tr key={car.id}>
<td className={styles.idCell}>
<div className={styles.orderBtns}>
<button

View File

@@ -111,16 +111,6 @@
background: #fafafa;
}
.table tbody tr[draggable] {
cursor: grab;
}
.dragOver {
background: #eff6ff !important;
outline: 2px dashed #1a56db;
outline-offset: -2px;
}
.idCell {
color: #9ca3af;
width: 80px;

View File

@@ -1,17 +1,16 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { format, addDays, parseISO, differenceInDays } from 'date-fns';
import { format, addDays, startOfWeek, parseISO, differenceInDays, isSameDay } 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 half-day slot (午前 or 午後)
const CELL_WIDTH = 52; // px per day column
const ROW_HEIGHT = 64; // px per car row
const LABEL_WIDTH = 140; // px for car name column
const HEADER_HEIGHT = 80; // px for the date header row (top: date+dow, bottom: 午前/午後)
const HEADER_HEIGHT = 72; // px for the date header row
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' &&
@@ -43,8 +42,10 @@ export default function ScheduleView({ reloadKey = 0 }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// The first date shown in the grid (start from today)
const [viewStart, setViewStart] = useState(() => new Date());
// The first date shown in the grid
const [viewStart, setViewStart] = useState(() =>
startOfWeek(new Date(), { weekStartsOn: 1 })
);
// Drag-to-create state
const [creating, setCreating] = useState(null);
@@ -90,18 +91,17 @@ export default function ScheduleView({ reloadKey = 0 }) {
// --- Navigation ---
const prevWeek = () => setViewStart((d) => addDays(d, -7));
const nextWeek = () => setViewStart((d) => addDays(d, 7));
const goToday = () => setViewStart(new Date());
const goToday = () => setViewStart(startOfWeek(new Date(), { weekStartsOn: 1 }));
// --- Grid position helpers ---
// 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) => {
// Given a mouse clientX within the grid scroll area, get the day index (0-based)
const getColFromX = 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.min(Math.floor(x / CELL_WIDTH), HALF_SLOTS - 1);
return Math.floor(x / CELL_WIDTH);
}, []);
const getRowFromY = useCallback((clientY) => {
@@ -114,62 +114,59 @@ export default function ScheduleView({ reloadKey = 0 }) {
}, []);
// --- Cell drag to create ---
const handleCellMouseDown = (e, carId, slot) => {
const handleCellMouseDown = (e, carId, dateStr) => {
if (isTouchDevice) return; // drag-to-create is mouse-only
if (e.button !== 0) return;
e.preventDefault();
setCreating({ carId, startSlot: slot, endSlot: slot });
setCreating({ carId, startDateStr: dateStr, endDateStr: dateStr });
};
// --- Cell tap to create (touch devices) ---
const handleCellClick = useCallback((e, carId) => {
if (!isTouchDevice) return;
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 ? '午前' : '午後';
const col = getColFromX(e.clientX);
if (col >= 0 && col < DAYS_SHOWN) {
const dateStr = dateToStr(dates[col]);
setModal({
mode: 'create',
prefill: { car_id: carId, start_date: dateStr, start_period: period, end_date: dateStr, end_period: period },
prefill: { car_id: carId, start_date: dateStr, end_date: dateStr },
});
}
}, [dates, getSlotFromX]);
}, [dates, getColFromX]);
const handleGridMouseMove = useCallback((e) => {
if (creating) {
const slot = getSlotFromX(e.clientX);
if (slot >= 0 && slot < HALF_SLOTS) {
const col = getColFromX(e.clientX);
if (col >= 0 && col < DAYS_SHOWN) {
const hoveredDate = dateToStr(dates[col]);
setCreating((prev) => {
if (!prev) return null;
const s = prev.startSlot;
const h = slot;
// Ensure start <= end
const s = prev.startDateStr;
const h = hoveredDate;
return {
...prev,
endSlot: h >= s ? h : s,
startSlot: h < s ? h : prev.startSlot,
endDateStr: h >= s ? h : s,
startDateStr: h < s ? h : prev.startDateStr,
};
});
}
}
if (moving) {
const slot = getSlotFromX(e.clientX);
const col = getColFromX(e.clientX);
const row = getRowFromY(e.clientY);
movingRef.current = { ...movingRef.current };
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 ? '午前' : '午後';
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));
let newCarId = moving.currentCarId;
if (row >= 0 && row < cars.length) {
@@ -179,56 +176,54 @@ export default function ScheduleView({ reloadKey = 0 }) {
setMoving((prev) => prev ? {
...prev,
currentCarId: newCarId,
currentStartSlot: clampedStartSlot,
currentStartDate: newStartDate,
currentStartPeriod: newStartPeriod,
currentEndDate: newEndDate,
currentEndPeriod: newEndPeriod,
col: clampedStartSlot,
col: clampedStartCol,
row: row >= 0 && row < cars.length ? row : prev.row,
} : null);
}
}
}, [creating, moving, dates, cars, getSlotFromX, getRowFromY]);
}, [creating, moving, dates, cars, getColFromX, getRowFromY]);
const handleGridMouseUp = useCallback(async (e) => {
if (creating) {
const { carId, startSlot, endSlot } = creating;
const { carId, startDateStr, endDateStr } = 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: dateToStr(dates[startDayIdx]),
start_period: startSlot % 2 === 0 ? '午前' : '午後',
end_date: dateToStr(dates[endDayIdx]),
end_period: endSlot % 2 === 0 ? '午前' : '午後',
start_date: startDateStr,
end_date: endDateStr,
},
});
}
if (moving) {
const { reservation, currentCarId, origStartSlot, currentStartSlot, currentStartDate, currentStartPeriod, currentEndDate, currentEndPeriod } = moving;
const { reservation, currentCarId, currentStartDate, currentEndDate } = 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 ||
currentStartSlot !== origStartSlot
currentStartDate !== reservation.start_date
) {
try {
await api.updateReservation(reservation.id, {
car_id: currentCarId,
start_date: currentStartDate,
end_date: currentEndDate,
end_date: newEndDate,
customer_name: reservation.customer_name,
notes: reservation.notes,
start_period: currentStartPeriod,
end_period: currentEndPeriod,
});
await loadData();
} catch (err) {
@@ -237,7 +232,7 @@ export default function ScheduleView({ reloadKey = 0 }) {
}
}
}
}, [creating, moving, dates, loadData]);
}, [creating, moving, loadData]);
useEffect(() => {
window.addEventListener('mousemove', handleGridMouseMove);
@@ -268,28 +263,18 @@ export default function ScheduleView({ reloadKey = 0 }) {
if (e.button !== 0) return;
e.preventDefault();
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 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 carRow = cars.findIndex((c) => c.id === reservation.car_id);
setMoving({
reservation,
grabSlotOffset: Math.max(0, grabOffset),
durationSlots,
origStartSlot: startSlot,
grabDayOffset: Math.max(0, grabOffset),
currentCarId: reservation.car_id,
currentStartSlot: startSlot,
currentStartDate: reservation.start_date,
currentStartPeriod: reservation.start_period || '午前',
currentEndDate: reservation.end_date,
currentEndPeriod: reservation.end_period || '午後',
col: startSlot,
col: startCol,
row: carRow,
});
};
@@ -328,26 +313,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 (accounting for 午前/午後 periods)
// For each reservation, calculate its left/width in the grid
function getReservationLayout(r) {
const rStart = r.start_date < viewStartStr ? viewStartStr : r.start_date;
const rEnd = r.end_date > viewEndStr ? viewEndStr : r.end_date;
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 };
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 };
}
// 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 = (durationSlots + 1) * CELL_WIDTH;
const width = (duration + 1) * CELL_WIDTH;
return { col, left, width, row: moving.row };
})() : null;
@@ -407,7 +392,7 @@ export default function ScheduleView({ reloadKey = 0 }) {
)}
<div
className={styles.grid}
style={{ width: LABEL_WIDTH + DAYS_SHOWN * 2 * CELL_WIDTH }}
style={{ width: LABEL_WIDTH + DAYS_SHOWN * CELL_WIDTH }}
>
{/* Header row */}
<div className={styles.headerRow} style={{ height: HEADER_HEIGHT }}>
@@ -416,7 +401,7 @@ export default function ScheduleView({ reloadKey = 0 }) {
className={styles.cornerCell}
style={{ width: LABEL_WIDTH, height: HEADER_HEIGHT }}
/>
{/* Date headers — each day spans two half-day slots (午前 + 午後) */}
{/* Date headers */}
{dates.map((date) => {
const ds = dateToStr(date);
const isToday = ds === todayStr;
@@ -425,17 +410,11 @@ export default function ScheduleView({ reloadKey = 0 }) {
return (
<div
key={ds}
className={`${styles.dateHeaderGroup} ${isToday ? styles.todayHeader : ''} ${isWeekend ? styles.weekendHeader : ''}`}
style={{ width: CELL_WIDTH * 2, height: HEADER_HEIGHT }}
className={`${styles.dateHeader} ${isToday ? styles.todayHeader : ''} ${isWeekend ? styles.weekendHeader : ''}`}
style={{ width: CELL_WIDTH, 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>
<span className={styles.dateDay}>{format(date, 'd')}</span>
<span className={`${styles.dateDow} ${isWeekend ? styles.weekendDow : ''}`}>{dow}</span>
</div>
);
})}
@@ -453,8 +432,10 @@ export default function ScheduleView({ reloadKey = 0 }) {
const isCreatingRow = creating && creating.carId === car.id;
let creatingLeft = 0, creatingWidth = 0;
if (isCreatingRow) {
creatingLeft = Math.max(0, creating.startSlot) * CELL_WIDTH;
creatingWidth = (Math.min(creating.endSlot, HALF_SLOTS - 1) - Math.max(0, creating.startSlot) + 1) * CELL_WIDTH;
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;
}
// Ghost reservation for this row
@@ -486,25 +467,25 @@ export default function ScheduleView({ reloadKey = 0 }) {
</span>
</div>
{/* Day cells — two per day (午前 then 午後) */}
{/* Day cells */}
<div
className={styles.cellArea}
style={{ width: DAYS_SHOWN * 2 * CELL_WIDTH, height: ROW_HEIGHT }}
style={{ width: DAYS_SHOWN * CELL_WIDTH, height: ROW_HEIGHT }}
onClick={(e) => handleCellClick(e, car.id)}
>
{dates.flatMap((date, dayIdx) => {
{dates.map((date) => {
const ds = dateToStr(date);
const isToday = ds === todayStr;
const dow = format(date, 'E', { locale: ja });
const isWeekend = dow === '土' || dow === '日';
return ['午前', '午後'].map((period, pIdx) => (
return (
<div
key={`${ds}-${period}`}
className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''} ${isTouchDevice ? styles.cellTouch : ''} ${pIdx === 1 ? styles.cellDayEnd : ''}`}
key={ds}
className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''} ${isTouchDevice ? styles.cellTouch : ''}`}
style={{ width: CELL_WIDTH, height: ROW_HEIGHT }}
onMouseDown={(e) => handleCellMouseDown(e, car.id, dayIdx * 2 + pIdx)}
onMouseDown={(e) => handleCellMouseDown(e, car.id, ds)}
/>
));
);
})}
{/* Creating highlight */}

View File

@@ -124,47 +124,6 @@
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;
@@ -322,10 +281,6 @@
background: rgba(26, 86, 219, 0.04);
}
.cellDayEnd {
border-right: 1px solid #d1d5db;
}
.todayCell {
background: rgba(59, 130, 246, 0.06);
}

View File

@@ -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 descending (newest first) then car
// Sort reservations by start_date 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;
});