fix: lỗi bong bóng Tour vẫn xuất hiện và điểm bắt đầu là điểm xuất phát
This commit is contained in:
@@ -61,7 +61,7 @@ const MapPicker = ({ onPick, center }: { onPick: (latlng: L.LatLng) => void, cen
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editingLocation, isPublicView = false, onSuccess }: { isOpen: boolean, onClose: () => void, tourId: string, initialLegId?: string, editingLocation?: any, isPublicView?: boolean, onSuccess?: () => void }) => {
|
export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editingLocation, isStartPoint = false, isPublicView = false, onSuccess }: { isOpen: boolean, onClose: () => void, tourId: string, initialLegId?: string, editingLocation?: any, isStartPoint?: boolean, isPublicView?: boolean, onSuccess?: () => void }) => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
address: '',
|
address: '',
|
||||||
@@ -85,7 +85,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
const searchTimeout = useRef<any>(null);
|
const searchTimeout = useRef<any>(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
|
// 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, updateLocation, mapCenter, currentTour, userRole } = useTourStore();
|
const { legs, addLocation, updateLocation, updateTourStartPoint, mapCenter, currentTour, userRole } = useTourStore();
|
||||||
const notify = useNotification();
|
const notify = useNotification();
|
||||||
|
|
||||||
// 1. Luôn khai báo Hook ở cấp cao nhất, không đặt code logic/return giữa các Hook
|
// 1. Luôn khai báo Hook ở cấp cao nhất, không đặt code logic/return giữa các Hook
|
||||||
@@ -153,8 +153,10 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
// 2. Thực hiện các tính toán và hàm xử lý
|
// 2. Thực hiện các tính toán và hàm xử lý
|
||||||
const currentLegId = formData.legId || (legs.length > 0 ? legs[0].id : '');
|
const currentLegId = formData.legId || (legs.length > 0 ? legs[0].id : '');
|
||||||
const targetLeg = legs.find(l => l.id === currentLegId);
|
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 titleText = isStartPoint ? 'Thiết lập Điểm xuất phát' :
|
||||||
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');
|
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 = isStartPoint ? 'Xác nhận Điểm xuất phát' : editingLocation ? 'Cập nhật thay đổi' : (initialLegId ? 'Xác nhận thêm vào chặng' : 'Thêm địa điểm');
|
||||||
|
|
||||||
const handleSearchLocation = (query: string) => {
|
const handleSearchLocation = (query: string) => {
|
||||||
setFormData(prev => ({ ...prev, name: query }));
|
setFormData(prev => ({ ...prev, name: query }));
|
||||||
@@ -281,20 +283,28 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const payload: any = {
|
if (isStartPoint) {
|
||||||
...formData,
|
await updateTourStartPoint(tourId, {
|
||||||
expenseAmount: formData.expenseAmount.replace(/\./g, ''), // Loại bỏ dấu chấm trước khi gửi
|
name: formData.name || "Điểm xuất phát",
|
||||||
legId: currentLegId,
|
latitude: parseFloat(formData.latitude as any),
|
||||||
latitude: parseFloat(formData.latitude as any),
|
longitude: parseFloat(formData.longitude as any),
|
||||||
longitude: parseFloat(formData.longitude as any),
|
});
|
||||||
};
|
|
||||||
|
|
||||||
if (editingLocation) {
|
|
||||||
await updateLocation(editingLocation.id, payload);
|
|
||||||
} else {
|
} else {
|
||||||
await addLocation(tourId, payload);
|
const payload: any = {
|
||||||
|
...formData,
|
||||||
|
expenseAmount: formData.expenseAmount.replace(/\./g, ''), // Loại bỏ dấu chấm trước khi gửi
|
||||||
|
legId: currentLegId,
|
||||||
|
latitude: parseFloat(formData.latitude as any),
|
||||||
|
longitude: parseFloat(formData.longitude as any),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editingLocation) {
|
||||||
|
await updateLocation(editingLocation.id, payload);
|
||||||
|
} else {
|
||||||
|
await addLocation(tourId, payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
notify({ title: 'Thành công', message: editingLocation ? 'Đã cập nhật địa điểm.' : 'Đã thêm địa điểm mới.', type: 'success' });
|
notify({ title: 'Thành công', message: isStartPoint ? 'Đã thiết lập điểm xuất phát.' : editingLocation ? 'Đã cập nhật địa điểm.' : 'Đã thêm địa điểm mới.', type: 'success' });
|
||||||
onSuccess?.(); // Gọi callback onSuccess sau khi thành công
|
onSuccess?.(); // Gọi callback onSuccess sau khi thành công
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const ItineraryTimeline = ({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
isPublicView = false
|
isPublicView = false
|
||||||
}: {
|
}: {
|
||||||
onAddLocation?: (legId: string) => void,
|
onAddLocation?: (legId: string, isStart?: boolean) => void,
|
||||||
onEditLocation?: (location: any) => void,
|
onEditLocation?: (location: any) => void,
|
||||||
onQuickNote?: (name: string) => void,
|
onQuickNote?: (name: string) => void,
|
||||||
onSuccess?: () => void,
|
onSuccess?: () => void,
|
||||||
@@ -301,6 +301,27 @@ export const ItineraryTimeline = ({
|
|||||||
<div className={`absolute left-6 top-16 ${legIdx === legs.length - 1 ? 'bottom-0' : 'bottom-[-3.5rem]'} w-0.5 bg-blue-100 -z-0`} />
|
<div className={`absolute left-6 top-16 ${legIdx === legs.length - 1 ? 'bottom-0' : 'bottom-[-3.5rem]'} w-0.5 bg-blue-100 -z-0`} />
|
||||||
|
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
|
{/* Nút thêm nhanh "Điểm xuất phát" cho Chặng 1 nếu chưa có */}
|
||||||
|
{legIdx === 0 && !leg.locations.some(loc => loc.plannedStart && new Date(loc.plannedStart).getTime() === 0) && (
|
||||||
|
<div className="relative flex group mb-6 opacity-80 hover:opacity-100 transition-opacity">
|
||||||
|
<div className="z-10 mt-1.5 mr-4">
|
||||||
|
<div className="w-8 h-8 bg-white rounded-full border-2 border-dashed border-blue-200 flex items-center justify-center text-blue-400">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onAddLocation?.(leg.id, true)}
|
||||||
|
className="flex-1 bg-blue-50/20 p-4 rounded-xl border border-dashed border-blue-100 hover:border-blue-400 hover:bg-blue-50 transition-all flex items-center justify-between group"
|
||||||
|
>
|
||||||
|
<div className="text-left">
|
||||||
|
<span className="inline-block px-2 py-0.5 bg-blue-50 text-blue-600 text-[10px] font-bold rounded-md mb-1 uppercase tracking-wider">Điểm xuất phát</span>
|
||||||
|
<h3 className="font-bold text-gray-400 text-sm italic">Nhấn để ghim điểm bắt đầu cho Tour...</h3>
|
||||||
|
</div>
|
||||||
|
<Plus className="w-5 h-5 text-blue-500 group-hover:scale-110 transition-transform" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{leg.locations.map((location, idx) => {
|
{leg.locations.map((location, idx) => {
|
||||||
// Tìm vị trí của điểm này trong toàn bộ hành trình
|
// Tìm vị trí của điểm này trong toàn bộ hành trình
|
||||||
const globalIdx = allLocations.findIndex(loc => loc.id === location.id);
|
const globalIdx = allLocations.findIndex(loc => loc.id === location.id);
|
||||||
@@ -316,8 +337,9 @@ export const ItineraryTimeline = ({
|
|||||||
|
|
||||||
const locationExpense = leg.expenses?.find((e: any) => e.locationId === location.id);
|
const locationExpense = leg.expenses?.find((e: any) => e.locationId === location.id);
|
||||||
|
|
||||||
const isStartPoint = legs[0]?.id === leg.id && idx === 0;
|
// Nhận diện điểm mốc dựa trên timestamp đặc biệt (0) thay vì chỉ số mảng
|
||||||
const isEndPoint = legs[legs.length - 1]?.id === leg.id && idx === leg.locations.length - 1;
|
const isStartPoint = location.plannedStart && new Date(location.plannedStart).getTime() === 0;
|
||||||
|
const isEndPoint = location.plannedEnd && new Date(location.plannedEnd).getTime() === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={location.id}>
|
<div key={location.id}>
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export const TourDetailPage = ({
|
|||||||
const [isAddLocationOpen, setIsAddLocationOpen] = useState(false);
|
const [isAddLocationOpen, setIsAddLocationOpen] = useState(false);
|
||||||
const [isAddMemberOpen, setIsAddMemberOpen] = useState(false);
|
const [isAddMemberOpen, setIsAddMemberOpen] = useState(false);
|
||||||
const [isAddPhotoOpen, setIsAddPhotoOpen] = useState(false);
|
const [isAddPhotoOpen, setIsAddPhotoOpen] = useState(false);
|
||||||
|
const [isStartPointAction, setIsStartPointAction] = useState(false);
|
||||||
const [targetLegId, setTargetLegId] = useState<string | null>(null);
|
const [targetLegId, setTargetLegId] = useState<string | null>(null);
|
||||||
const [editingLocation, setEditingLocation] = useState<any>(null);
|
const [editingLocation, setEditingLocation] = useState<any>(null);
|
||||||
const [selectedMember, setSelectedMember] = useState<any>(null);
|
const [selectedMember, setSelectedMember] = useState<any>(null);
|
||||||
@@ -1014,14 +1015,16 @@ export const TourDetailPage = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'timeline' ? (
|
{viewMode === 'timeline' ? (
|
||||||
<ItineraryTimeline onAddLocation={(legId) => {
|
<ItineraryTimeline onAddLocation={(legId, isStart) => {
|
||||||
setTargetLegId(legId);
|
setTargetLegId(legId);
|
||||||
setEditingLocation(null);
|
setEditingLocation(null);
|
||||||
|
setIsStartPointAction(!!isStart);
|
||||||
setIsAddLocationOpen(true);
|
setIsAddLocationOpen(true);
|
||||||
}} onEditLocation={(loc) => {
|
}} onEditLocation={(loc) => {
|
||||||
setEditingLocation(loc);
|
setEditingLocation(loc);
|
||||||
setTargetLegId(loc.legId);
|
setTargetLegId(loc.legId);
|
||||||
setMapCenter([loc.latitude, loc.longitude]);
|
setMapCenter([loc.latitude, loc.longitude]);
|
||||||
|
setIsStartPointAction(false);
|
||||||
setIsAddLocationOpen(true);
|
setIsAddLocationOpen(true);
|
||||||
}}
|
}}
|
||||||
onQuickNote={(locName: string) => handleQuickNote(locName)}
|
onQuickNote={(locName: string) => handleQuickNote(locName)}
|
||||||
@@ -1505,6 +1508,7 @@ export const TourDetailPage = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTargetLegId(null); // Reset khi nhấn nút floating tổng quát
|
setTargetLegId(null); // Reset khi nhấn nút floating tổng quát
|
||||||
setEditingLocation(null);
|
setEditingLocation(null);
|
||||||
|
setIsStartPointAction(false);
|
||||||
if (activeTab === 'plan') setIsAddLocationOpen(true);
|
if (activeTab === 'plan') setIsAddLocationOpen(true);
|
||||||
if (activeTab === 'photo' && canUploadPhoto) setIsAddPhotoOpen(true);
|
if (activeTab === 'photo' && canUploadPhoto) setIsAddPhotoOpen(true);
|
||||||
}}
|
}}
|
||||||
@@ -1535,6 +1539,7 @@ export const TourDetailPage = ({
|
|||||||
isOpen={isAddLocationOpen}
|
isOpen={isAddLocationOpen}
|
||||||
onClose={() => setIsAddLocationOpen(false)}
|
onClose={() => setIsAddLocationOpen(false)}
|
||||||
initialLegId={targetLegId || undefined}
|
initialLegId={targetLegId || undefined}
|
||||||
|
isStartPoint={isStartPointAction}
|
||||||
editingLocation={editingLocation}
|
editingLocation={editingLocation}
|
||||||
tourId={currentTour.id}
|
tourId={currentTour.id}
|
||||||
isPublicView={isPublicView} // Pass isPublicView
|
isPublicView={isPublicView} // Pass isPublicView
|
||||||
|
|||||||
Reference in New Issue
Block a user