Sửa lỗi khi click vào dấu + để thêm chặng

This commit is contained in:
2026-06-14 09:37:40 +07:00
parent 14bf43487e
commit 4545bde8ec
13 changed files with 139 additions and 133 deletions
+21 -8
View File
@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { X, MapPin, Loader2, Clock } from 'lucide-react';
import { useTourStore } from './useTourStore.js';
export const AddLocationModal = ({ isOpen, onClose, tourId }: { isOpen: boolean, onClose: () => void, tourId: string }) => {
export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId }: { isOpen: boolean, onClose: () => void, tourId: string, initialLegId?: string }) => {
const [formData, setFormData] = useState({
name: '',
address: '',
@@ -14,13 +14,26 @@ export const AddLocationModal = ({ isOpen, onClose, tourId }: { isOpen: boolean,
plannedEnd: ''
});
const [isLoading, setIsLoading] = useState(false);
const legs = useTourStore(state => state.legs);
const addLocation = useTourStore(state => state.addLocation);
if (!isOpen) return null;
// Gom các selector lại để giảm số lượng Hook gọi nội bộ và tăng hiệu năng
const { legs, addLocation } = useTourStore();
// Mặc định chọn chặng đầu nếu chưa chọn
// 1. Luôn khai báo Hook ở cấp cao nhất, không đặt code logic/return giữa các Hook
useEffect(() => {
// Chỉ cập nhật nếu giá trị thực sự thay đổi để tránh vòng lặp re-render
if (initialLegId && formData.legId !== initialLegId) {
setFormData(prev => ({ ...prev, legId: initialLegId }));
}
}, [initialLegId, isOpen]);
// 2. Thực hiện các tính toán phụ trợ
const currentLegId = formData.legId || (legs.length > 0 ? legs[0].id : '');
const targetLeg = legs.find(l => l.id === currentLegId);
const titleText = initialLegId ? `Thêm địa điểm cho ${targetLeg?.note || `Chặng ${targetLeg?.sequence}`}` : 'Thêm địa điểm mới';
const buttonText = initialLegId ? 'Xác nhận thêm vào chặng' : 'Thêm địa điểm';
// 3. Early return phải nằm SAU tất cả các khai báo Hook
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -46,7 +59,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId }: { isOpen: boolean,
<div className="relative w-full max-w-lg bg-white rounded-3xl shadow-2xl overflow-hidden p-8 max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<MapPin className="w-6 h-6 text-blue-600" /> Thêm đa điểm mới
<MapPin className="w-6 h-6 text-blue-600" /> {titleText}
</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<X className="w-6 h-6 text-gray-400" />
@@ -103,7 +116,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId }: { isOpen: boolean,
</div>
</div>
<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">
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Thêm địa điểm'}
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : buttonText}
</button>
</form>
</div>
+43 -56
View File
@@ -35,8 +35,8 @@ const formatTravelTime = (minutes: number) => {
return mins > 0 ? `${hours} giờ ${mins} phút` : `${hours} giờ`;
};
export const ItineraryTimeline = () => {
const { currentTour, legs, optimizeRouting, userRole, activeLegId, setActiveLegId, addLeg, updateLeg, deleteLeg, initializeLegs } = useTourStore();
export const ItineraryTimeline = ({ onAddLocation }: { onAddLocation?: (legId: string) => void }) => {
const { currentTour, legs, optimizeRouting, userRole, addLeg, updateLeg, deleteLeg, initializeLegs } = useTourStore();
const toggleComplete = async (locationId: string) => {
// Gọi API PATCH /api/v1/locations/:id để cập nhật trạng thái
@@ -75,51 +75,16 @@ export const ItineraryTimeline = () => {
}
};
const activeLeg = legs.find(l => l.id === activeLegId) || legs[0];
return (
<div className="max-w-2xl mx-auto p-0 bg-gray-50 min-h-screen">
{/* Tầng 1: Horizontal Leg Picker (6.3-A) */}
<div className="sticky top-[136px] z-20 bg-gray-50/80 backdrop-blur-sm pb-4 mb-4">
<div className="flex overflow-x-auto gap-2 px-2 no-scrollbar py-2">
{legs.map((leg) => (
<button
key={leg.id}
onClick={() => setActiveLegId(leg.id)}
className={`flex-shrink-0 px-6 py-2.5 rounded-2xl text-xs font-black uppercase tracking-widest transition-all border ${
activeLegId === leg.id
? 'bg-blue-600 text-white border-blue-600 shadow-lg shadow-blue-100 scale-105'
: 'bg-white text-gray-400 border-gray-100 hover:border-gray-200'
}`}
>
Chặng {leg.sequence}
</button>
))}
{['OWNER', 'MANAGER'].includes(userRole || '') && (
<button
onClick={handleDeclareLegs}
className="flex-shrink-0 px-4 py-2.5 rounded-2xl bg-blue-50 text-blue-600 border border-dashed border-blue-200 hover:bg-blue-100 transition-all flex items-center gap-2 text-xs font-bold"
title="Khai báo nhanh số lượng chặng"
>
<List className="w-4 h-4" /> Khai báo số chặng
</button>
)}
{['OWNER', 'MANAGER'].includes(userRole || '') && (
<button
onClick={handleAddLeg}
className="flex-shrink-0 p-2.5 rounded-2xl bg-white text-blue-600 border border-dashed border-blue-200 hover:bg-blue-50 transition-all flex items-center justify-center w-12"
>
<Plus className="w-5 h-5" />
</button>
)}
<div className="px-2 pt-4">
{legs.length === 0 ? (
<div className="text-center py-20 bg-white rounded-3xl border-2 border-dashed border-gray-200">
<List className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500 font-medium">Chưa chặng nào trong lộ trình.</p>
</div>
</div>
<div className="px-2">
{activeLeg && (
<div className="relative animate-in fade-in slide-in-from-right-4 duration-300">
{/* Leg Info Summary */}
{(() => {
const leg = activeLeg;
) : (
legs.map((leg, legIdx) => {
const totalDwellMinutes = leg.locations.reduce((acc: number, loc: any) => {
if (loc.plannedStart && loc.plannedEnd) {
return acc + differenceInMinutes(parseISO(loc.plannedEnd), parseISO(loc.plannedStart));
@@ -128,15 +93,25 @@ export const ItineraryTimeline = () => {
}, 0);
return (
<>
<div key={leg.id} className="relative mb-12 animate-in fade-in slide-in-from-bottom-4 duration-300">
{/* Leg Header */}
<div className="flex items-center mb-4 px-2">
<div className="font-black text-gray-900 text-lg truncate flex-1">
<div className="sticky top-[136px] z-20 bg-gray-50/90 backdrop-blur-sm flex items-center mb-6 py-2 px-2">
<div className="font-black text-blue-600 text-xl truncate flex-1 flex items-center gap-2">
<span className="bg-blue-600 text-white w-8 h-8 rounded-lg flex items-center justify-center text-sm">
{leg.sequence}
</span>
{leg.note || `Chi tiết Chặng ${leg.sequence}`}
</div>
<div className="flex items-center gap-2 ml-4">
{['OWNER', 'MANAGER'].includes(userRole || '') && (
<>
<button
onClick={() => onAddLocation?.(leg.id)}
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
title="Thêm địa điểm vào chặng này"
>
<Plus className="w-4 h-4" />
</button>
<button
onClick={() => handleEditLeg(leg)}
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
@@ -181,7 +156,7 @@ export const ItineraryTimeline = () => {
</div>
{/* Vertical Line for the whole leg */}
<div className="absolute left-6 top-10 bottom-0 w-0.5 bg-gray-200 -z-0" />
<div className="absolute left-6 top-16 bottom-0 w-0.5 bg-blue-100 -z-0" />
<div className="ml-2">
{leg.locations.map((location, idx) => {
@@ -282,15 +257,27 @@ export const ItineraryTimeline = () => {
);
})}
</div>
{leg.note && (
<p className="ml-14 mt-4 text-sm text-gray-400 italic">
* {leg.note}
</p>
)}
</>
</div>
);
})()}
})
)}
{/* Actions at the bottom of the list */}
{['OWNER', 'MANAGER'].includes(userRole || '') && (
<div className="flex flex-col gap-3 pb-20 mt-8">
<button
onClick={handleDeclareLegs}
className="w-full py-4 rounded-2xl bg-white text-blue-600 border-2 border-dashed border-blue-200 hover:bg-blue-50 transition-all flex items-center justify-center gap-2 text-sm font-bold"
>
<List className="w-5 h-5" />
{legs.length > 0 ? "Khai báo lại số lượng chặng" : "Bắt đầu bằng việc khai báo số chặng"}
</button>
<button
onClick={handleAddLeg}
className="w-full py-4 rounded-2xl bg-blue-600 text-white shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all flex items-center justify-center gap-2 text-sm font-bold"
>
<Plus className="w-5 h-5" /> Thêm chặng lẻ vào cuối
</button>
</div>
)}
</div>
+16 -14
View File
@@ -129,6 +129,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings'>('plan');
const [viewMode, setViewMode] = useState<'map' | 'timeline'>('timeline');
const [isAddLocationOpen, setIsAddLocationOpen] = useState(false);
const [targetLegId, setTargetLegId] = useState<string | null>(null);
// Khôi phục vị trí và mức zoom từ localStorage
const [initialViewState] = useState(() => {
@@ -140,18 +141,12 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
});
// SỬA LỖI: Sử dụng các selectors riêng lẻ để tránh re-render trang khi mapCenter trong store thay đổi
const currentTour = useTourStore(state => state.currentTour);
const legs = useTourStore(state => state.legs);
const fetchTour = useTourStore(state => state.fetchTour);
const fetchPublicTours = useTourStore(state => state.fetchPublicTours);
const publicTours = useTourStore(state => state.publicTours);
const userRole = useTourStore(state => state.userRole);
const mapCenter = useTourStore(state => state.mapCenter);
const setMapCenter = useTourStore(state => state.setMapCenter);
const updateTourStartPoint = useTourStore(state => state.updateTourStartPoint);
const updateTourEndPoint = useTourStore(state => state.updateTourEndPoint);
const initializeLegs = useTourStore(state => state.initializeLegs);
const addLocation = useTourStore(state => state.addLocation);
// Gom các store actions/state lại để tối ưu hóa re-render
const {
currentTour, legs, fetchTour, fetchPublicTours, publicTours,
userRole, mapCenter, setMapCenter, updateTourStartPoint,
updateTourEndPoint, initializeLegs, addLocation
} = useTourStore();
const [mapZoom] = useState(initialViewState?.zoom || 13);
@@ -471,7 +466,10 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
</div>
{viewMode === 'timeline' ? (
<ItineraryTimeline />
<ItineraryTimeline onAddLocation={(legId) => {
setTargetLegId(legId);
setIsAddLocationOpen(true);
}} />
) : (
<div className="h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative">
<MapContainer center={initialViewState?.center || mapCenter} zoom={mapZoom} className="h-full w-full">
@@ -559,7 +557,10 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
{/* Floating Action Button (Mobile) */}
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40">
<button
onClick={() => activeTab === 'plan' ? setIsAddLocationOpen(true) : null}
onClick={() => {
setTargetLegId(null); // Reset khi nhấn nút floating tổng quát
if (activeTab === 'plan') setIsAddLocationOpen(true);
}}
className="bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg hover:bg-blue-700 transition-all flex items-center font-bold">
{activeTab === 'plan' ? 'Thêm địa điểm' : activeTab === 'expense' ? 'Thêm chi phí' : 'Đăng ảnh'}
</button>
@@ -570,6 +571,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
<AddLocationModal
isOpen={isAddLocationOpen}
onClose={() => setIsAddLocationOpen(false)}
initialLegId={targetLegId || undefined}
tourId={currentTour.id}
/>
)}
+1
View File
@@ -5,6 +5,7 @@
* **Tab Active**: Lộ trình (Mặc định).
* **Sub-view Active**: Bản đồ (Component bản đồ đang hiển thị ở nửa dưới màn hình).
* **Thành phần cần can thiệp**: Lớp tương tác (Interaction Layer) của thư viện Bản đồ (Mapbox / Google Maps API / Leaflet) đang render phía dưới.
## 2. Đặc Tả Kỹ Thuật Đóng Gói (Functional Requirements)
+2 -1
View File
@@ -1,6 +1,7 @@
import React from 'react';
export declare const AddLocationModal: ({ isOpen, onClose, tourId }: {
export declare const AddLocationModal: ({ isOpen, onClose, tourId, initialLegId }: {
isOpen: boolean;
onClose: () => void;
tourId: string;
initialLegId?: string;
}) => React.JSX.Element;
+13 -6
View File
@@ -1,8 +1,8 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { X, MapPin, Loader2 } from 'lucide-react';
import { useTourStore } from './useTourStore.js';
export const AddLocationModal = ({ isOpen, onClose, tourId }) => {
export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId }) => {
const [formData, setFormData] = useState({
name: '',
address: '',
@@ -14,11 +14,18 @@ export const AddLocationModal = ({ isOpen, onClose, tourId }) => {
plannedEnd: ''
});
const [isLoading, setIsLoading] = useState(false);
const legs = useTourStore(state => state.legs);
const addLocation = useTourStore(state => state.addLocation);
const { legs, addLocation } = useTourStore();
useEffect(() => {
if (initialLegId && formData.legId !== initialLegId) {
setFormData(prev => ({ ...prev, legId: initialLegId }));
}
}, [initialLegId, isOpen]);
const currentLegId = formData.legId || (legs.length > 0 ? legs[0].id : '');
const targetLeg = legs.find(l => l.id === currentLegId);
const titleText = initialLegId ? `Thêm địa điểm cho ${targetLeg?.note || `Chặng ${targetLeg?.sequence}`}` : 'Thêm địa điểm mới';
const buttonText = initialLegId ? 'Xác nhận thêm vào chặng' : 'Thêm địa điểm';
if (!isOpen)
return null;
const currentLegId = formData.legId || (legs.length > 0 ? legs[0].id : '');
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
@@ -38,6 +45,6 @@ export const AddLocationModal = ({ isOpen, onClose, tourId }) => {
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(MapPin, { className: "w-6 h-6 text-blue-600" }), " Th\u00EAm \u0111\u1ECBa \u0111i\u1EC3m m\u1EDBi"] }), _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("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: "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" }) : 'Thêm địa điểm' })] })] })] }));
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(MapPin, { 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("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: "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
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"AddLocationModal.js","sourceRoot":"","sources":["../AddLocationModal.tsx"],"names":[],"mappings":"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"}
{"version":3,"file":"AddLocationModal.js","sourceRoot":"","sources":["../AddLocationModal.tsx"],"names":[],"mappings":"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"}
+3 -1
View File
@@ -1,2 +1,4 @@
import React from 'react';
export declare const ItineraryTimeline: () => React.JSX.Element;
export declare const ItineraryTimeline: ({ onAddLocation }: {
onAddLocation?: (legId: string) => void;
}) => React.JSX.Element;
+6 -10
View File
@@ -24,8 +24,8 @@ const formatTravelTime = (minutes) => {
const mins = minutes % 60;
return mins > 0 ? `${hours} giờ ${mins} phút` : `${hours} giờ`;
};
export const ItineraryTimeline = () => {
const { currentTour, legs, optimizeRouting, userRole, activeLegId, setActiveLegId, addLeg, updateLeg, deleteLeg, initializeLegs } = useTourStore();
export const ItineraryTimeline = ({ onAddLocation }) => {
const { currentTour, legs, optimizeRouting, userRole, addLeg, updateLeg, deleteLeg, initializeLegs } = useTourStore();
const toggleComplete = async (locationId) => {
console.log("Toggle status for location:", locationId);
};
@@ -58,18 +58,14 @@ export const ItineraryTimeline = () => {
}
}
};
const activeLeg = legs.find(l => l.id === activeLegId) || legs[0];
return (_jsxs("div", { className: "max-w-2xl mx-auto p-0 bg-gray-50 min-h-screen", children: [_jsx("div", { className: "sticky top-[136px] z-20 bg-gray-50/80 backdrop-blur-sm pb-4 mb-4", children: _jsxs("div", { className: "flex overflow-x-auto gap-2 px-2 no-scrollbar py-2", children: [legs.map((leg) => (_jsxs("button", { onClick: () => setActiveLegId(leg.id), className: `flex-shrink-0 px-6 py-2.5 rounded-2xl text-xs font-black uppercase tracking-widest transition-all border ${activeLegId === leg.id
? 'bg-blue-600 text-white border-blue-600 shadow-lg shadow-blue-100 scale-105'
: 'bg-white text-gray-400 border-gray-100 hover:border-gray-200'}`, children: ["Ch\u1EB7ng ", leg.sequence] }, leg.id))), ['OWNER', 'MANAGER'].includes(userRole || '') && (_jsxs("button", { onClick: handleDeclareLegs, className: "flex-shrink-0 px-4 py-2.5 rounded-2xl bg-blue-50 text-blue-600 border border-dashed border-blue-200 hover:bg-blue-100 transition-all flex items-center gap-2 text-xs font-bold", title: "Khai b\u00E1o nhanh s\u1ED1 l\u01B0\u1EE3ng ch\u1EB7ng", children: [_jsx(List, { className: "w-4 h-4" }), " Khai b\u00E1o s\u1ED1 ch\u1EB7ng"] })), ['OWNER', 'MANAGER'].includes(userRole || '') && (_jsx("button", { onClick: handleAddLeg, className: "flex-shrink-0 p-2.5 rounded-2xl bg-white text-blue-600 border border-dashed border-blue-200 hover:bg-blue-50 transition-all flex items-center justify-center w-12", children: _jsx(Plus, { className: "w-5 h-5" }) }))] }) }), _jsx("div", { className: "px-2", children: activeLeg && (_jsx("div", { className: "relative animate-in fade-in slide-in-from-right-4 duration-300", children: (() => {
const leg = activeLeg;
return (_jsx("div", { className: "max-w-2xl mx-auto p-0 bg-gray-50 min-h-screen", children: _jsxs("div", { className: "px-2 pt-4", children: [legs.length === 0 ? (_jsxs("div", { className: "text-center py-20 bg-white rounded-3xl border-2 border-dashed border-gray-200", children: [_jsx(List, { className: "w-12 h-12 text-gray-300 mx-auto mb-4" }), _jsx("p", { className: "text-gray-500 font-medium", children: "Ch\u01B0a c\u00F3 ch\u1EB7ng n\u00E0o trong l\u1ED9 tr\u00ECnh." })] })) : (legs.map((leg, legIdx) => {
const totalDwellMinutes = leg.locations.reduce((acc, loc) => {
if (loc.plannedStart && loc.plannedEnd) {
return acc + differenceInMinutes(parseISO(loc.plannedEnd), parseISO(loc.plannedStart));
}
return acc;
}, 0);
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center mb-4 px-2", children: [_jsx("div", { className: "font-black text-gray-900 text-lg truncate flex-1", children: leg.note || `Chi tiết Chặng ${leg.sequence}` }), _jsxs("div", { className: "flex items-center gap-2 ml-4", children: [['OWNER', 'MANAGER'].includes(userRole || '') && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => handleEditLeg(leg), className: "p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all", children: _jsx(Edit2, { className: "w-4 h-4" }) }), _jsx("button", { onClick: () => handleDeleteLeg(leg.id), className: "p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-all", children: _jsx(Trash2, { className: "w-4 h-4" }) })] })), leg.totalDistance !== undefined && (_jsxs("div", { className: "hidden sm:flex items-center gap-2", children: [_jsxs("div", { className: "text-xs font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded-lg border border-blue-100", children: [leg.totalDistance, " km"] }), _jsxs("div", { className: "text-xs font-bold text-gray-500 bg-gray-50 px-2 py-1 rounded-lg border border-gray-100 flex items-center gap-1", children: [_jsx(Clock, { className: "w-3 h-3" }), "~ ", formatTravelTime(Math.round((leg.totalDistance / 35) * 60))] })] })), totalDwellMinutes > 0 && (_jsxs("div", { className: "hidden md:flex text-xs font-bold text-amber-600 bg-amber-50 px-2 py-1 rounded-lg border border-amber-100 items-center gap-1", children: [_jsx(Clock, { className: "w-3 h-3" }), "D\u1EEBng: ", formatTravelTime(totalDwellMinutes)] }))] }), ['OWNER', 'MANAGER'].includes(userRole || '') && legs.length > 0 && (_jsxs("button", { onClick: () => optimizeRouting(leg.id), className: "ml-auto flex items-center gap-1.5 text-xs font-bold text-indigo-600 hover:text-indigo-700 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-xl transition-all", children: [_jsx(Zap, { className: "w-3 h-3" }), "T\u1ED1i \u01B0u"] }))] }), _jsx("div", { className: "absolute left-6 top-10 bottom-0 w-0.5 bg-gray-200 -z-0" }), _jsx("div", { className: "ml-2", children: leg.locations.map((location, idx) => {
return (_jsxs("div", { className: "relative mb-12 animate-in fade-in slide-in-from-bottom-4 duration-300", children: [_jsxs("div", { className: "sticky top-[136px] z-20 bg-gray-50/90 backdrop-blur-sm flex items-center mb-6 py-2 px-2", children: [_jsxs("div", { className: "font-black text-blue-600 text-xl truncate flex-1 flex items-center gap-2", children: [_jsx("span", { className: "bg-blue-600 text-white w-8 h-8 rounded-lg flex items-center justify-center text-sm", children: leg.sequence }), leg.note || `Chi tiết Chặng ${leg.sequence}`] }), _jsxs("div", { className: "flex items-center gap-2 ml-4", children: [['OWNER', 'MANAGER'].includes(userRole || '') && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => onAddLocation?.(leg.id), className: "p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all", title: "Th\u00EAm \u0111\u1ECBa \u0111i\u1EC3m v\u00E0o ch\u1EB7ng n\u00E0y", children: _jsx(Plus, { className: "w-4 h-4" }) }), _jsx("button", { onClick: () => handleEditLeg(leg), className: "p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all", children: _jsx(Edit2, { className: "w-4 h-4" }) }), _jsx("button", { onClick: () => handleDeleteLeg(leg.id), className: "p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-all", children: _jsx(Trash2, { className: "w-4 h-4" }) })] })), leg.totalDistance !== undefined && (_jsxs("div", { className: "hidden sm:flex items-center gap-2", children: [_jsxs("div", { className: "text-xs font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded-lg border border-blue-100", children: [leg.totalDistance, " km"] }), _jsxs("div", { className: "text-xs font-bold text-gray-500 bg-gray-50 px-2 py-1 rounded-lg border border-gray-100 flex items-center gap-1", children: [_jsx(Clock, { className: "w-3 h-3" }), "~ ", formatTravelTime(Math.round((leg.totalDistance / 35) * 60))] })] })), totalDwellMinutes > 0 && (_jsxs("div", { className: "hidden md:flex text-xs font-bold text-amber-600 bg-amber-50 px-2 py-1 rounded-lg border border-amber-100 items-center gap-1", children: [_jsx(Clock, { className: "w-3 h-3" }), "D\u1EEBng: ", formatTravelTime(totalDwellMinutes)] }))] }), ['OWNER', 'MANAGER'].includes(userRole || '') && legs.length > 0 && (_jsxs("button", { onClick: () => optimizeRouting(leg.id), className: "ml-auto flex items-center gap-1.5 text-xs font-bold text-indigo-600 hover:text-indigo-700 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-xl transition-all", children: [_jsx(Zap, { className: "w-3 h-3" }), "T\u1ED1i \u01B0u"] }))] }), _jsx("div", { className: "absolute left-6 top-16 bottom-0 w-0.5 bg-blue-100 -z-0" }), _jsx("div", { className: "ml-2", children: leg.locations.map((location, idx) => {
const nextLocation = leg.locations[idx + 1];
const distanceToNext = nextLocation
? calculateDistance(location.latitude, location.longitude, nextLocation.latitude, nextLocation.longitude)
@@ -82,7 +78,7 @@ export const ItineraryTimeline = () => {
const isStartPoint = legs[0]?.id === leg.id && idx === 0;
const isEndPoint = legs[legs.length - 1]?.id === leg.id && idx === leg.locations.length - 1;
return (_jsxs("div", { children: [_jsxs("div", { className: "relative flex group mb-6", children: [_jsx("div", { className: "z-10 mt-1.5 mr-4", children: _jsx("button", { onClick: () => toggleComplete(location.id), className: `transition-colors duration-200 ${location.status === 'COMPLETED' ? 'text-green-500' : 'text-gray-300 hover:text-blue-500'}`, children: location.status === 'COMPLETED' ? (_jsx(CheckCircle2, { className: "w-8 h-8 bg-white rounded-full" })) : (_jsx(Circle, { className: "w-8 h-8 bg-white rounded-full fill-white" })) }) }), _jsxs("div", { className: `flex-1 bg-white p-4 rounded-xl border transition-all duration-200 ${location.status === 'COMPLETED' ? 'border-green-100 bg-green-50/30' : 'border-gray-100 shadow-sm hover:shadow-md'}`, children: [_jsxs("div", { className: "flex justify-between items-start", children: [_jsxs("div", { children: [isStartPoint && (_jsx("span", { className: "inline-block px-2 py-0.5 bg-green-100 text-green-700 text-[10px] font-bold rounded-md mb-1 uppercase tracking-wider", children: "\u0110i\u1EC3m b\u1EAFt \u0111\u1EA7u" })), isEndPoint && (_jsx("span", { className: "inline-block px-2 py-0.5 bg-red-100 text-red-700 text-[10px] font-bold rounded-md mb-1 uppercase tracking-wider", children: "\u0110i\u1EC3m k\u1EBFt th\u00FAc" })), _jsx("h3", { className: `font-semibold text-lg ${location.status === 'COMPLETED' ? 'text-gray-500 line-through' : 'text-gray-800'}`, children: location.name }), _jsxs("div", { className: "flex items-center text-sm text-gray-500 mt-1", children: [_jsx(MapPin, { className: "w-3 h-3 mr-1" }), _jsx("span", { className: "truncate max-w-[200px] sm:max-w-md", children: location.address })] }), dwellMinutes !== null && (_jsxs("div", { className: "flex items-center text-xs text-amber-600 font-medium mt-1", children: [_jsx(Clock, { className: "w-3 h-3 mr-1" }), _jsxs("span", { children: ["Th\u1EDDi gian d\u1EEBng: ", formatTravelTime(dwellMinutes)] })] }))] }), _jsxs("div", { className: "text-right flex flex-col items-end", children: [_jsxs("div", { className: "flex items-center text-sm font-medium text-blue-600", children: [_jsx(Clock, { className: "w-3 h-3 mr-1" }), location.plannedStart ? format(parseISO(location.plannedStart), 'HH:mm') : '--:--'] }), location.status === 'COMPLETED' && location.actualStart && (_jsxs("div", { className: "text-[10px] text-gray-400 mt-1 italic", children: ["Th\u1EF1c t\u1EBF: ", format(parseISO(location.actualStart), 'HH:mm')] }))] })] }), _jsx(TimeVariance, { planned: location.plannedStart || '', actual: location.actualStart || null })] })] }), distanceToNext !== null && travelTimeMinutes !== null && (_jsxs("div", { className: "ml-4 -mt-4 mb-2 flex items-center gap-3", children: [_jsx("div", { className: "w-8 flex justify-center", children: _jsx(Navigation, { className: "w-3 h-3 text-blue-400 rotate-180" }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-[10px] font-bold text-blue-500 bg-blue-50 px-2 py-0.5 rounded-full border border-blue-100", children: [distanceToNext.toFixed(2), " km"] }), _jsxs("span", { className: "text-[10px] font-bold text-gray-500 bg-gray-50 px-2 py-0.5 rounded-full border border-gray-100 flex items-center gap-1", children: [_jsx(Clock, { className: "w-2.5 h-2.5" }), "~ ", formatTravelTime(travelTimeMinutes)] })] })] }))] }, location.id));
}) }), leg.note && (_jsxs("p", { className: "ml-14 mt-4 text-sm text-gray-400 italic", children: ["* ", leg.note] }))] }));
})() })) })] }));
}) })] }, leg.id));
})), ['OWNER', 'MANAGER'].includes(userRole || '') && (_jsxs("div", { className: "flex flex-col gap-3 pb-20 mt-8", children: [_jsxs("button", { onClick: handleDeclareLegs, className: "w-full py-4 rounded-2xl bg-white text-blue-600 border-2 border-dashed border-blue-200 hover:bg-blue-50 transition-all flex items-center justify-center gap-2 text-sm font-bold", children: [_jsx(List, { className: "w-5 h-5" }), legs.length > 0 ? "Khai báo lại số lượng chặng" : "Bắt đầu bằng việc khai báo số chặng"] }), _jsxs("button", { onClick: handleAddLeg, className: "w-full py-4 rounded-2xl bg-blue-600 text-white shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all flex items-center justify-center gap-2 text-sm font-bold", children: [_jsx(Plus, { className: "w-5 h-5" }), " Th\u00EAm ch\u1EB7ng l\u1EBB v\u00E0o cu\u1ED1i"] })] }))] }) }));
};
//# sourceMappingURL=ItineraryTimeline.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+11 -14
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long