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
This commit is contained in:
copilot-swe-agent[bot]
2026-03-20 18:50:51 +00:00
parent 1eb96877ff
commit cc3ad148fc
6 changed files with 773 additions and 1 deletions

View File

@@ -58,6 +58,10 @@ export default function ScheduleView() {
const [modal, setModal] = useState(null);
// null | { mode: 'create', prefill: {...} } | { mode: 'edit', reservation: {...} }
// Context menu state (right-click on reservation)
const [contextMenu, setContextMenu] = useState(null);
// null | { x, y, reservation }
const gridRef = useRef(null);
const movingRef = useRef(null); // keeps latest moving state for event handlers
@@ -238,6 +242,19 @@ export default function ScheduleView() {
};
}, [handleGridMouseMove, handleGridMouseUp]);
// Close context menu on any click or Escape
useEffect(() => {
if (!contextMenu) return;
const close = () => setContextMenu(null);
const onKey = (e) => { if (e.key === 'Escape') close(); };
window.addEventListener('click', close);
window.addEventListener('keydown', onKey);
return () => {
window.removeEventListener('click', close);
window.removeEventListener('keydown', onKey);
};
}, [contextMenu]);
// --- Reservation drag to move ---
const handleReservationMouseDown = (e, reservation) => {
e.stopPropagation();
@@ -508,6 +525,11 @@ export default function ScheduleView() {
setModal({ mode: 'edit', reservation: r });
}
}}
onContextMenu={(e) => {
e.preventDefault();
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 : ''}`}
>
<span className={styles.blockText}>
@@ -544,6 +566,34 @@ export default function ScheduleView() {
onClose={() => setModal(null)}
/>
)}
{/* Right-click context menu */}
{contextMenu && (
<div
className={styles.contextMenu}
style={{ left: contextMenu.x, top: contextMenu.y }}
onClick={(e) => e.stopPropagation()}
>
<button
className={styles.contextMenuItem}
onClick={() => {
setModal({ mode: 'edit', reservation: contextMenu.reservation });
setContextMenu(null);
}}
>
編集
</button>
<button
className={`${styles.contextMenuItem} ${styles.contextMenuItemDelete}`}
onClick={async () => {
setContextMenu(null);
await handleModalDelete(contextMenu.reservation.id);
}}
>
🗑 削除
</button>
</div>
)}
</div>
);
}