146 lines
15 KiB
JavaScript
146 lines
15 KiB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { X, MapPin, Loader2, Map as MapIcon } from 'lucide-react';
|
|
import { useTourStore } from './useTourStore.js';
|
|
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet';
|
|
import L from 'leaflet';
|
|
delete L.Icon.Default.prototype._getIconUrl;
|
|
L.Icon.Default.mergeOptions({
|
|
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
|
|
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
|
|
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
|
});
|
|
const MapPicker = ({ onPick, center }) => {
|
|
const [menuPos, setMenuPos] = useState(null);
|
|
const menuRef = useRef(null);
|
|
const map = useMap();
|
|
useMapEvents({
|
|
contextmenu: (e) => {
|
|
L.DomEvent.preventDefault(e.originalEvent);
|
|
L.DomEvent.stopPropagation(e.originalEvent);
|
|
setMenuPos({ x: e.containerPoint.x, y: e.containerPoint.y, latlng: e.latlng });
|
|
},
|
|
click: () => setMenuPos(null),
|
|
dragstart: () => setMenuPos(null),
|
|
});
|
|
useEffect(() => {
|
|
if (menuPos && menuRef.current) {
|
|
L.DomEvent.disableClickPropagation(menuRef.current);
|
|
}
|
|
}, [menuPos]);
|
|
useEffect(() => {
|
|
map.setView(center, map.getZoom());
|
|
}, [center]);
|
|
return (_jsx(_Fragment, { children: menuPos && (_jsx("div", { ref: menuRef, className: "absolute z-[3000] bg-white rounded-xl shadow-xl border border-gray-100 py-1 w-44 animate-in zoom-in-95 duration-200", style: { top: menuPos.y, left: menuPos.x }, children: _jsxs("button", { type: "button", onClick: () => { onPick(menuPos.latlng); setMenuPos(null); }, className: "w-full text-left px-4 py-2 hover:bg-blue-50 text-xs font-bold text-blue-600 flex items-center gap-2", children: [_jsx(MapPin, { className: "w-3 h-3" }), " Th\u00EAm v\u00E0o ch\u1EB7ng hi\u1EC7n t\u1EA1i"] }) })) }));
|
|
};
|
|
export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editingLocation }) => {
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
address: '',
|
|
latitude: 10.7769,
|
|
longitude: 106.7009,
|
|
type: 'VISIT',
|
|
legId: '',
|
|
note: '',
|
|
expenseAmount: '',
|
|
expenseCategory: 'OTHER',
|
|
expenseDescription: '',
|
|
expenseNote: '',
|
|
paidById: '',
|
|
plannedStart: '',
|
|
plannedEnd: ''
|
|
});
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const { legs, addLocation, updateLocation, mapCenter, currentTour } = useTourStore();
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
if (editingLocation) {
|
|
const leg = legs.find(l => l.id === editingLocation.legId);
|
|
const expense = leg?.expenses?.find((e) => e.locationId === editingLocation.id);
|
|
setFormData({
|
|
name: editingLocation.name || '',
|
|
address: editingLocation.address || '',
|
|
latitude: editingLocation.latitude,
|
|
longitude: editingLocation.longitude,
|
|
type: editingLocation.type || 'VISIT',
|
|
legId: editingLocation.legId || '',
|
|
note: editingLocation.note || '',
|
|
expenseAmount: expense?.amount?.toString() || '',
|
|
expenseCategory: expense?.category || 'OTHER',
|
|
expenseDescription: expense?.description || '',
|
|
expenseNote: expense?.note || '',
|
|
paidById: expense?.paidById || '',
|
|
plannedStart: editingLocation.plannedStart ? editingLocation.plannedStart.slice(0, 16) : '',
|
|
plannedEnd: editingLocation.plannedEnd ? editingLocation.plannedEnd.slice(0, 16) : ''
|
|
});
|
|
}
|
|
else {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
name: '', address: '',
|
|
legId: initialLegId || (legs.length > 0 ? legs[0].id : ''),
|
|
note: '', expenseAmount: '', expenseCategory: 'OTHER',
|
|
expenseDescription: '', expenseNote: '', paidById: '',
|
|
plannedStart: '', plannedEnd: ''
|
|
}));
|
|
}
|
|
}
|
|
}, [initialLegId, editingLocation, isOpen]);
|
|
useEffect(() => {
|
|
if (isOpen && !formData.name) {
|
|
setFormData(prev => ({ ...prev, latitude: mapCenter[0], longitude: mapCenter[1] }));
|
|
}
|
|
}, [isOpen, mapCenter]);
|
|
const currentLegId = formData.legId || (legs.length > 0 ? legs[0].id : '');
|
|
const targetLeg = legs.find(l => l.id === currentLegId);
|
|
const titleText = editingLocation ? `Sửa địa điểm: ${editingLocation.name}` : (initialLegId ? `Thêm địa điểm cho ${targetLeg?.note || `Chặng ${targetLeg?.sequence}`}` : 'Thêm địa điểm mới');
|
|
const buttonText = editingLocation ? 'Cập nhật thay đổi' : (initialLegId ? 'Xác nhận thêm vào chặng' : 'Thêm địa điểm');
|
|
const handlePickLocation = async (latlng) => {
|
|
setFormData(prev => ({ ...prev, latitude: latlng.lat, longitude: latlng.lng }));
|
|
try {
|
|
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng.lat}&lon=${latlng.lng}`);
|
|
const data = await res.json();
|
|
const addr = data.address;
|
|
const detectedName = addr.amenity || addr.building || addr.historic || addr.tourist ||
|
|
addr.shop || addr.office || addr.leisure || addr.attraction ||
|
|
addr.road || addr.neighbourhood || addr.suburb ||
|
|
data.display_name?.split(',')[0] || "";
|
|
setFormData(prev => ({ ...prev, name: detectedName || prev.name, address: data.display_name || prev.address }));
|
|
}
|
|
catch (e) { }
|
|
};
|
|
if (!isOpen)
|
|
return null;
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setIsLoading(true);
|
|
try {
|
|
const payload = {
|
|
...formData,
|
|
legId: currentLegId,
|
|
latitude: parseFloat(formData.latitude),
|
|
longitude: parseFloat(formData.longitude),
|
|
};
|
|
if (editingLocation) {
|
|
await updateLocation(editingLocation.id, payload);
|
|
}
|
|
else {
|
|
await addLocation(tourId, payload);
|
|
}
|
|
onClose();
|
|
}
|
|
catch (error) {
|
|
alert('Lỗi khi lưu địa điểm');
|
|
}
|
|
finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
return (_jsxs("div", { className: "fixed inset-0 z-[2000] flex items-center justify-center p-4", children: [_jsx("div", { className: "absolute inset-0 bg-gray-900/60 backdrop-blur-sm", onClick: onClose }), _jsxs("div", { className: "relative w-full max-w-lg bg-white rounded-3xl shadow-2xl overflow-hidden p-8 max-h-[90vh] overflow-y-auto", children: [_jsxs("div", { className: "flex justify-between items-center mb-6", children: [_jsxs("h2", { className: "text-2xl font-bold text-gray-900 flex items-center gap-2", children: [_jsx(MapIcon, { className: "w-6 h-6 text-blue-600" }), " ", titleText] }), _jsx("button", { onClick: onClose, className: "p-2 hover:bg-gray-100 rounded-full transition-colors", children: _jsx(X, { className: "w-6 h-6 text-gray-400" }) })] }), _jsxs("div", { className: "h-48 w-full rounded-2xl overflow-hidden mb-6 border border-gray-100 relative shadow-inner group", children: [_jsxs(MapContainer, { center: [formData.latitude, formData.longitude], zoom: 13, className: "h-full w-full", children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" }), _jsx(Marker, { position: [formData.latitude, formData.longitude] }), _jsx(MapPicker, { center: [formData.latitude, formData.longitude], onPick: handlePickLocation })] }), _jsx("div", { className: "absolute bottom-2 left-2 z-[1000] bg-white/90 backdrop-blur-sm px-2 py-1 rounded-lg text-[10px] font-black text-gray-500 shadow-sm border border-gray-100", children: "CHU\u1ED8T PH\u1EA2I \u0110\u1EC2 CH\u1ECCN V\u1ECA TR\u00CD" })] }), _jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-sm font-bold text-gray-700 mb-1", children: "T\u00EAn \u0111\u1ECBa \u0111i\u1EC3m" }), _jsx("input", { required: true, className: "w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none", value: formData.name, onChange: e => setFormData({ ...formData, name: e.target.value }) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-bold text-gray-700 mb-1", children: "\u0110\u1ECBa ch\u1EC9" }), _jsx("input", { className: "w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none", value: formData.address, onChange: e => setFormData({ ...formData, address: e.target.value }) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-bold text-gray-700 mb-1", children: "Ghi ch\u00FA \u0111\u1ECBa \u0111i\u1EC3m / D\u1ECBch v\u1EE5 s\u1EED d\u1EE5ng" }), _jsx("textarea", { className: "w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none resize-none", rows: 2, placeholder: "V\u00ED d\u1EE5: \u0102n tr\u01B0a t\u1EA1i qu\u00E1n X, thu\u00EA h\u01B0\u1EDBng d\u1EABn vi\u00EAn...", value: formData.note, onChange: e => setFormData({ ...formData, note: e.target.value }) })] }), _jsxs("div", { className: "bg-blue-50/60 border border-blue-100 rounded-2xl p-4 space-y-3", children: [_jsx("p", { className: "text-xs font-black text-blue-500 uppercase tracking-widest", children: "Chi ph\u00ED nhanh t\u1EA1i \u0111i\u1EC3m n\u00E0y" }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-600 mb-1", children: "S\u1ED1 ti\u1EC1n (VN\u0110)" }), _jsx("input", { type: "number", className: "w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm", placeholder: "0", value: formData.expenseAmount, onChange: e => setFormData({ ...formData, expenseAmount: e.target.value }) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-600 mb-1", children: "Lo\u1EA1i d\u1ECBch v\u1EE5" }), _jsxs("select", { className: "w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm", value: formData.expenseCategory, onChange: e => setFormData({ ...formData, expenseCategory: e.target.value }), children: [_jsx("option", { value: "FOOD", children: "\u0102n u\u1ED1ng" }), _jsx("option", { value: "TRANSPORT", children: "Di chuy\u1EC3n" }), _jsx("option", { value: "ACCOMMODATION", children: "Ch\u1ED7 \u1EDF" }), _jsx("option", { value: "TICKET", children: "V\u00E9 tham quan" }), _jsx("option", { value: "OTHER", children: "Kh\u00E1c" })] })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-600 mb-1", children: "D\u1ECBch v\u1EE5 / M\u00F4 t\u1EA3" }), _jsx("input", { className: "w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm", placeholder: "V\u00ED d\u1EE5: \u0102n tr\u01B0a, taxi, v\u00E9...", value: formData.expenseDescription, onChange: e => setFormData({ ...formData, expenseDescription: e.target.value }) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-600 mb-1", children: "Ghi ch\u00FA chi ph\u00ED" }), _jsx("textarea", { className: "w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none resize-none text-sm", rows: 2, placeholder: "Ghi ch\u00FA th\u00EAm...", value: formData.expenseNote, onChange: e => setFormData({ ...formData, expenseNote: e.target.value }) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-gray-600 mb-1", children: "Th\u00E0nh vi\u00EAn \u0111\u00E3 thanh to\u00E1n" }), _jsxs("select", { className: "w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm", value: formData.paidById, onChange: e => setFormData({ ...formData, paidById: e.target.value }), children: [_jsx("option", { value: "", children: "-- Ch\u1ECDn ng\u01B0\u1EDDi thanh to\u00E1n --" }), currentTour?.participants?.map((p) => {
|
|
const name = p.user?.name;
|
|
const email = p.user?.email;
|
|
const label = [name, email && name ? `(${email})` : email].filter(Boolean).join(' ');
|
|
return (_jsx("option", { value: p.userId, children: label || p.userId }, p.userId));
|
|
})] })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-bold text-gray-700 mb-1", children: "G\u00E1n v\u00E0o ch\u1EB7ng" }), _jsx("select", { required: true, className: "w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none", value: currentLegId, onChange: e => setFormData({ ...formData, legId: e.target.value }), children: legs.map(leg => (_jsxs("option", { value: leg.id, children: ["Ch\u1EB7ng ", leg.sequence, ": ", leg.note || 'Không có tên'] }, leg.id))) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-sm font-bold text-gray-700 mb-1", children: "V\u0129 \u0111\u1ED9" }), _jsx("input", { type: "number", step: "any", required: true, className: "w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none", value: formData.latitude, onChange: e => setFormData({ ...formData, latitude: e.target.value }) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-bold text-gray-700 mb-1", children: "Kinh \u0111\u1ED9" }), _jsx("input", { type: "number", step: "any", required: true, className: "w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none", value: formData.longitude, onChange: e => setFormData({ ...formData, longitude: e.target.value }) })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-bold text-gray-700 mb-1", children: "Lo\u1EA1i" }), _jsxs("select", { className: "w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none", value: formData.type, onChange: e => setFormData({ ...formData, type: e.target.value }), children: [_jsx("option", { value: "VISIT", children: "Tham quan" }), _jsx("option", { value: "EAT", children: "\u0102n u\u1ED1ng" }), _jsx("option", { value: "REST", children: "Ngh\u1EC9 ng\u01A1i" }), _jsx("option", { value: "MOVE", children: "Di chuy\u1EC3n" })] })] }), _jsx("div", { className: "grid grid-cols-2 gap-4", children: _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-bold text-gray-700 mb-1", children: "B\u1EAFt \u0111\u1EA7u" }), _jsx("input", { type: "datetime-local", className: "w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none", value: formData.plannedStart, onChange: e => setFormData({ ...formData, plannedStart: e.target.value }) })] }) }), _jsx("button", { disabled: isLoading, className: "w-full py-4 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-2xl flex items-center justify-center gap-2 mt-4", children: isLoading ? _jsx(Loader2, { className: "w-5 h-5 animate-spin" }) : buttonText })] })] })] }));
|
|
};
|
|
//# sourceMappingURL=AddLocationModal.js.map
|