diff --git a/README.md b/README.md index 54474ee..fb97daa 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,27 @@ npm run dev 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 | Method | Path | Description | diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 969105b..4110f9c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "frontend", "version": "1.0.0", - "license": "ISC", "dependencies": { "@vitejs/plugin-react": "^6.0.1", "date-fns": "^4.1.0", diff --git a/frontend/src/api.js b/frontend/src/api.js index 7cd079f..55a7aa5 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,4 +1,4 @@ -const API_BASE = '/api'; +const API_BASE = (import.meta.env.VITE_API_BASE_URL ?? '') + '/api'; async function request(path, options = {}) { const res = await fetch(`${API_BASE}${path}`, { diff --git a/frontend/src/components/CarManagement.jsx b/frontend/src/components/CarManagement.jsx index 77a8f33..76f593f 100644 --- a/frontend/src/components/CarManagement.jsx +++ b/frontend/src/components/CarManagement.jsx @@ -4,6 +4,7 @@ import styles from './CarManagement.module.css'; export default function CarManagement() { const [cars, setCars] = useState([]); + const [reservations, setReservations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [newCarName, setNewCarName] = useState(''); @@ -16,8 +17,9 @@ export default function CarManagement() { const loadCars = async () => { try { setLoading(true); - const data = await api.getCars(); - setCars(data); + const [carsData, resData] = await Promise.all([api.getCars(), api.getReservations()]); + setCars(carsData); + setReservations(resData); setError(null); } catch (e) { setError(e.message); @@ -47,7 +49,11 @@ export default function CarManagement() { }; const handleDelete = async (id, name) => { - if (!confirm(`「${name}」を削除しますか?\n関連する予約もすべて削除されます。`)) return; + const carReservations = reservations.filter((r) => r.car_id === id); + const message = carReservations.length > 0 + ? `「${name}」を削除しますか?\n⚠ この代車には ${carReservations.length} 件の予約があります。削除するとこれらの予約もすべて削除されます。` + : `「${name}」を削除しますか?\n関連する予約もすべて削除されます。`; + if (!confirm(message)) return; try { await api.deleteCar(id); await loadCars(); diff --git a/frontend/src/components/ScheduleView.jsx b/frontend/src/components/ScheduleView.jsx index e1d1523..a071a3f 100644 --- a/frontend/src/components/ScheduleView.jsx +++ b/frontend/src/components/ScheduleView.jsx @@ -11,6 +11,10 @@ const LABEL_WIDTH = 140; // px for car name column const HEADER_HEIGHT = 72; // px for the date header row 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) const COLORS = [ { bg: '#dbeafe', border: '#3b82f6', text: '#1e3a8a' }, @@ -106,11 +110,25 @@ export default function ScheduleView() { // --- Cell drag to create --- const handleCellMouseDown = (e, carId, dateStr) => { + if (isTouchDevice) return; // drag-to-create is mouse-only if (e.button !== 0) return; e.preventDefault(); 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) => { if (creating) { const col = getColFromX(e.clientX); @@ -223,6 +241,7 @@ export default function ScheduleView() { // --- Reservation drag to move --- const handleReservationMouseDown = (e, reservation) => { e.stopPropagation(); + if (isTouchDevice) return; // drag-to-move is mouse-only if (e.button !== 0) return; e.preventDefault(); @@ -413,6 +432,7 @@ export default function ScheduleView() {
handleCellClick(e, car.id)} > {dates.map((date) => { const ds = dateToStr(date); @@ -422,7 +442,7 @@ export default function ScheduleView() { return (
handleCellMouseDown(e, car.id, ds)} /> @@ -479,7 +499,7 @@ export default function ScheduleView() { background: color.bg, borderColor: color.border, color: color.text, - cursor: 'grab', + cursor: isTouchDevice ? 'pointer' : 'grab', }} onMouseDown={(e) => handleReservationMouseDown(e, r)} onClick={(e) => { diff --git a/frontend/src/components/ScheduleView.module.css b/frontend/src/components/ScheduleView.module.css index 28c1502..4ff720f 100644 --- a/frontend/src/components/ScheduleView.module.css +++ b/frontend/src/components/ScheduleView.module.css @@ -198,6 +198,10 @@ transition: background 0.1s; } +.cellTouch { + cursor: pointer; +} + .cell:hover { background: rgba(26, 86, 219, 0.04); } diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 4414e51..c9266a0 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -7,7 +7,7 @@ export default defineConfig({ port: 5173, proxy: { '/api': { - target: 'http://localhost:3001', + target: process.env.BACKEND_URL || 'http://localhost:3001', changeOrigin: true, }, },