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
This commit is contained in:
21
README.md
21
README.md
@@ -51,6 +51,27 @@ 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 |
|
||||||
|
|||||||
1
frontend/package-lock.json
generated
1
frontend/package-lock.json
generated
@@ -7,7 +7,6 @@
|
|||||||
"": {
|
"": {
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const API_BASE = '/api';
|
const API_BASE = (import.meta.env.VITE_API_BASE_URL ?? '') + '/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}`, {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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('');
|
||||||
@@ -16,8 +17,9 @@ export default function CarManagement() {
|
|||||||
const loadCars = async () => {
|
const loadCars = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await api.getCars();
|
const [carsData, resData] = await Promise.all([api.getCars(), api.getReservations()]);
|
||||||
setCars(data);
|
setCars(carsData);
|
||||||
|
setReservations(resData);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
@@ -47,7 +49,11 @@ export default function CarManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id, name) => {
|
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 {
|
try {
|
||||||
await api.deleteCar(id);
|
await api.deleteCar(id);
|
||||||
await loadCars();
|
await loadCars();
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ 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' },
|
||||||
@@ -106,11 +110,25 @@ 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);
|
||||||
@@ -223,6 +241,7 @@ 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();
|
||||||
|
|
||||||
@@ -413,6 +432,7 @@ 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);
|
||||||
@@ -422,7 +442,7 @@ export default function ScheduleView() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={ds}
|
key={ds}
|
||||||
className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''}`}
|
className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''} ${isTouchDevice ? styles.cellTouch : ''}`}
|
||||||
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)}
|
||||||
/>
|
/>
|
||||||
@@ -479,7 +499,7 @@ export default function ScheduleView() {
|
|||||||
background: color.bg,
|
background: color.bg,
|
||||||
borderColor: color.border,
|
borderColor: color.border,
|
||||||
color: color.text,
|
color: color.text,
|
||||||
cursor: 'grab',
|
cursor: isTouchDevice ? 'pointer' : 'grab',
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => handleReservationMouseDown(e, r)}
|
onMouseDown={(e) => handleReservationMouseDown(e, r)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -198,6 +198,10 @@
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3001',
|
target: process.env.BACKEND_URL || 'http://localhost:3001',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user