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
This commit is contained in:
1
frontend/package-lock.json
generated
1
frontend/package-lock.json
generated
@@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"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 = {}) {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
<div
|
||||
className={styles.cellArea}
|
||||
style={{ width: DAYS_SHOWN * CELL_WIDTH, height: ROW_HEIGHT }}
|
||||
onClick={(e) => handleCellClick(e, car.id)}
|
||||
>
|
||||
{dates.map((date) => {
|
||||
const ds = dateToStr(date);
|
||||
@@ -422,7 +442,7 @@ export default function ScheduleView() {
|
||||
return (
|
||||
<div
|
||||
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 }}
|
||||
onMouseDown={(e) => 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) => {
|
||||
|
||||
@@ -198,6 +198,10 @@
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.cellTouch {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cell:hover {
|
||||
background: rgba(26, 86, 219, 0.04);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user