1 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
3d92f4902d Implement car reservation schedule management system
Co-authored-by: h <57948770+h@users.noreply.github.com>
Agent-Logs-Url: https://github.com/h/CarReservation/sessions/1d8c6b05-0e8d-4484-a2d8-8d427dfad9cb
2026-03-20 18:03:33 +00:00
7 changed files with 8 additions and 58 deletions

View File

@@ -51,27 +51,6 @@ npm run dev
Open http://localhost:5173 in your browser. Open http://localhost:5173 in your browser.
### Configuration
#### Backend URL (development proxy)
By default the Vite dev server proxies `/api` requests to `http://localhost:3001`.
To point to a different backend server, set the `BACKEND_URL` environment variable when starting the frontend:
```bash
BACKEND_URL=http://192.168.1.10:3001 npm run dev:frontend
```
#### Backend URL (production build)
When the frontend is deployed separately from the backend, set `VITE_API_BASE_URL` to the backend server's origin before building:
```bash
VITE_API_BASE_URL=https://api.example.com npm run build
```
This makes the built frontend send API requests to `https://api.example.com/api`.
### API Endpoints ### API Endpoints
| Method | Path | Description | | Method | Path | Description |

View File

@@ -7,6 +7,7 @@
"": { "": {
"name": "frontend", "name": "frontend",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC",
"dependencies": { "dependencies": {
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",

View File

@@ -1,4 +1,4 @@
const API_BASE = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api'; const API_BASE = '/api';
async function request(path, options = {}) { async function request(path, options = {}) {
const res = await fetch(`${API_BASE}${path}`, { const res = await fetch(`${API_BASE}${path}`, {

View File

@@ -4,7 +4,6 @@ import styles from './CarManagement.module.css';
export default function CarManagement() { export default function CarManagement() {
const [cars, setCars] = useState([]); const [cars, setCars] = useState([]);
const [reservations, setReservations] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [newCarName, setNewCarName] = useState(''); const [newCarName, setNewCarName] = useState('');
@@ -17,9 +16,8 @@ export default function CarManagement() {
const loadCars = async () => { const loadCars = async () => {
try { try {
setLoading(true); setLoading(true);
const [carsData, resData] = await Promise.all([api.getCars(), api.getReservations()]); const data = await api.getCars();
setCars(carsData); setCars(data);
setReservations(resData);
setError(null); setError(null);
} catch (e) { } catch (e) {
setError(e.message); setError(e.message);
@@ -49,11 +47,7 @@ export default function CarManagement() {
}; };
const handleDelete = async (id, name) => { const handleDelete = async (id, name) => {
const carReservations = reservations.filter((r) => r.car_id === id); if (!confirm(`${name}」を削除しますか?\n関連する予約もすべて削除されます。`)) return;
const message = carReservations.length > 0
? `${name}」を削除しますか?\n⚠ この代車には ${carReservations.length} 件の予約があります。削除するとこれらの予約もすべて削除されます。`
: `${name}」を削除しますか?\n関連する予約もすべて削除されます。`;
if (!confirm(message)) return;
try { try {
await api.deleteCar(id); await api.deleteCar(id);
await loadCars(); await loadCars();

View File

@@ -11,10 +11,6 @@ const LABEL_WIDTH = 140; // px for car name column
const HEADER_HEIGHT = 72; // px for the date header row const HEADER_HEIGHT = 72; // px for the date header row
const DAYS_SHOWN = 21; // number of days to show const DAYS_SHOWN = 21; // number of days to show
// Detect touch-primary device to disable mouse-only drag & drop
const isTouchDevice = typeof window !== 'undefined' &&
('ontouchstart' in window || navigator.maxTouchPoints > 0);
// Palette for reservation colors (cycle through them by car index) // Palette for reservation colors (cycle through them by car index)
const COLORS = [ const COLORS = [
{ bg: '#dbeafe', border: '#3b82f6', text: '#1e3a8a' }, { bg: '#dbeafe', border: '#3b82f6', text: '#1e3a8a' },
@@ -110,25 +106,11 @@ export default function ScheduleView() {
// --- Cell drag to create --- // --- Cell drag to create ---
const handleCellMouseDown = (e, carId, dateStr) => { const handleCellMouseDown = (e, carId, dateStr) => {
if (isTouchDevice) return; // drag-to-create is mouse-only
if (e.button !== 0) return; if (e.button !== 0) return;
e.preventDefault(); e.preventDefault();
setCreating({ carId, startDateStr: dateStr, endDateStr: dateStr }); setCreating({ carId, startDateStr: dateStr, endDateStr: dateStr });
}; };
// --- 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]);
setModal({
mode: 'create',
prefill: { car_id: carId, start_date: dateStr, end_date: dateStr },
});
}
}, [dates, getColFromX]);
const handleGridMouseMove = useCallback((e) => { const handleGridMouseMove = useCallback((e) => {
if (creating) { if (creating) {
const col = getColFromX(e.clientX); const col = getColFromX(e.clientX);
@@ -241,7 +223,6 @@ export default function ScheduleView() {
// --- Reservation drag to move --- // --- Reservation drag to move ---
const handleReservationMouseDown = (e, reservation) => { const handleReservationMouseDown = (e, reservation) => {
e.stopPropagation(); e.stopPropagation();
if (isTouchDevice) return; // drag-to-move is mouse-only
if (e.button !== 0) return; if (e.button !== 0) return;
e.preventDefault(); e.preventDefault();
@@ -432,7 +413,6 @@ export default function ScheduleView() {
<div <div
className={styles.cellArea} className={styles.cellArea}
style={{ width: DAYS_SHOWN * CELL_WIDTH, height: ROW_HEIGHT }} style={{ width: DAYS_SHOWN * CELL_WIDTH, height: ROW_HEIGHT }}
onClick={(e) => handleCellClick(e, car.id)}
> >
{dates.map((date) => { {dates.map((date) => {
const ds = dateToStr(date); const ds = dateToStr(date);
@@ -442,7 +422,7 @@ export default function ScheduleView() {
return ( return (
<div <div
key={ds} key={ds}
className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''} ${isTouchDevice ? styles.cellTouch : ''}`} className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''}`}
style={{ width: CELL_WIDTH, height: ROW_HEIGHT }} style={{ width: CELL_WIDTH, height: ROW_HEIGHT }}
onMouseDown={(e) => handleCellMouseDown(e, car.id, ds)} onMouseDown={(e) => handleCellMouseDown(e, car.id, ds)}
/> />
@@ -499,7 +479,7 @@ export default function ScheduleView() {
background: color.bg, background: color.bg,
borderColor: color.border, borderColor: color.border,
color: color.text, color: color.text,
cursor: isTouchDevice ? 'pointer' : 'grab', cursor: 'grab',
}} }}
onMouseDown={(e) => handleReservationMouseDown(e, r)} onMouseDown={(e) => handleReservationMouseDown(e, r)}
onClick={(e) => { onClick={(e) => {

View File

@@ -198,10 +198,6 @@
transition: background 0.1s; transition: background 0.1s;
} }
.cellTouch {
cursor: pointer;
}
.cell:hover { .cell:hover {
background: rgba(26, 86, 219, 0.04); background: rgba(26, 86, 219, 0.04);
} }

View File

@@ -7,7 +7,7 @@ export default defineConfig({
port: 5173, port: 5173,
proxy: { proxy: {
'/api': { '/api': {
target: process.env.BACKEND_URL || 'http://localhost:3001', target: 'http://localhost:3001',
changeOrigin: true, changeOrigin: true,
}, },
}, },