fix: gửi vị trí khẩn cấp trong Dashboard

This commit is contained in:
2026-06-23 12:44:38 +07:00
parent 6b15e7ff02
commit 5145835c8a
6 changed files with 250 additions and 69 deletions
+180 -24
View File
@@ -22,7 +22,8 @@ import {
Loader2,
Bell,
BellOff,
ShieldAlert
ShieldAlert,
Camera
} from 'lucide-react';
import { useTourStore } from '@/store/useTourStore';
import { useNotification } from '@/hooks/useNotification';
@@ -128,6 +129,56 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
}
};
const handleEmergencyShare = async (tour: any) => {
try {
setLoadingShare(true);
// Get current location
const position = await new Promise<GeolocationCoordinates>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve(pos.coords),
(err) => reject(err),
{ enableHighAccuracy: true, timeout: 5000 }
);
});
// Send location message to tour chat
const token = localStorage.getItem('token');
const message = `📍 Vị trí hiện tại: ${position.latitude.toFixed(6)}, ${position.longitude.toFixed(6)}\n🔗 Google Maps: https://maps.google.com/?q=${position.latitude},${position.longitude}`;
const res = await fetch(`/api/v1/tours/${tour.id}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ content: message })
});
if (res.ok) {
notify({
title: 'Thành công',
message: 'Đã gửi vị trí hiện tại cho nhóm',
type: 'success'
});
} else {
notify({
title: 'Lỗi',
message: 'Không thể gửi vị trí',
type: 'error'
});
}
} catch (e) {
console.error('Error sharing location:', e);
notify({
title: 'Lỗi',
message: 'Không thể lấy vị trí hiện tại',
type: 'error'
});
} finally {
setLoadingShare(false);
}
};
const handleToggleShare = async (isEnabled: boolean) => {
if (!sharingTour) return;
try {
@@ -153,8 +204,82 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
console.error(e);
}
};
const handleTakeCameraPhoto = async (tour: any) => {
if (cameraInputRef.current) {
cameraInputRef.current.click();
// Store tour ID for later processing
(cameraInputRef.current as any).dataset.tourId = tour.id;
}
};
const handleCameraFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const tourId = (event.target as any).dataset.tourId;
// Get current location if available
try {
setIsLocating(true);
const position = await new Promise<GeolocationCoordinates>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve(pos.coords),
(err) => reject(err),
{ enableHighAccuracy: true, timeout: 5000 }
);
});
setAttachedLocation({ latitude: position.latitude, longitude: position.longitude });
} catch (e) {
console.log('Could not get location:', e);
}
setIsLocating(false);
// Upload photo to tour
setIsUploading(true);
try {
const token = localStorage.getItem('token');
const formData = new FormData();
formData.append('images', file);
if (attachedLocation) {
formData.append('latitude', attachedLocation.latitude.toString());
formData.append('longitude', attachedLocation.longitude.toString());
}
const res = await fetch(`/api/v1/tours/${tourId}/photos`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
if (res.ok) {
notify({
title: 'Thành công',
message: 'Đã upload ảnh vào thư viện tour',
type: 'success'
});
// Clear input
if (cameraInputRef.current) cameraInputRef.current.value = '';
} else {
notify({
title: 'Lỗi',
message: 'Không thể upload ảnh',
type: 'error'
});
}
} catch (e) {
console.error('Upload error:', e);
notify({
title: 'Lỗi',
message: 'Lỗi upload ảnh',
type: 'error'
});
} finally {
setIsUploading(false);
}
}
};
const fileInputRef = useRef<HTMLInputElement>(null);
const cameraInputRef = useRef<HTMLInputElement>(null);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
@@ -1300,42 +1425,73 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
</span>
</div>
<div className="flex flex-col gap-2 mt-2">
{/* Chat button - moved to top */}
<button
onClick={() => {
localStorage.setItem('tour_detail_default_tab', 'chat');
setUnreadTourChats(prev => prev.filter(id => id !== tour.id));
onViewTour(tour.id, 'dashboard');
}}
className="w-full py-2 px-2 bg-indigo-600/20 hover:bg-indigo-600 text-indigo-300 hover:text-white rounded-xl text-xs font-bold transition-all border border-indigo-500/30 hover:border-indigo-500 flex items-center justify-center gap-1 relative whitespace-nowrap"
>
<MessageSquare className="w-3.5 h-3.5" />
<span>Trò chuyện</span>
{unreadTourChats.includes(tour.id) && (
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-rose-600 rounded-full animate-ping border border-slate-800" />
)}
{unreadTourChats.includes(tour.id) && (
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-rose-600 rounded-full border border-slate-800" />
)}
</button>
{/* Camera + Detail buttons row */}
<div className="flex gap-2">
<button
onClick={() => handleTakeCameraPhoto(tour)}
disabled={isUploading}
className="flex-1 py-2 px-2 bg-green-600/20 hover:bg-green-600 text-green-300 hover:text-white rounded-xl text-xs font-bold transition-all border border-green-500/30 hover:border-green-500 flex items-center justify-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isUploading ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Camera className="w-3.5 h-3.5" />
)}
<span>Chụp nh</span>
</button>
<button
onClick={() => onViewTour(tour.id, 'dashboard')}
className="flex-1 py-2 px-2 bg-slate-900 hover:bg-slate-800 text-slate-350 hover:text-white rounded-xl text-xs font-bold transition-all border border-slate-800 hover:border-slate-700 flex items-center justify-center gap-1"
>
<span>Xem chi tiết</span>
<span>Chi tiết hành trình</span>
<ChevronRight className="w-3.5 h-3.5" />
</button>
<button
onClick={() => {
localStorage.setItem('tour_detail_default_tab', 'chat');
setUnreadTourChats(prev => prev.filter(id => id !== tour.id));
onViewTour(tour.id, 'dashboard');
}}
className="flex-1 py-2 px-2 bg-indigo-600/20 hover:bg-indigo-600 text-indigo-300 hover:text-white rounded-xl text-xs font-bold transition-all border border-indigo-500/30 hover:border-indigo-500 flex items-center justify-center gap-1 relative whitespace-nowrap"
>
<MessageSquare className="w-3.5 h-3.5" />
<span>Trò chuyện</span>
{unreadTourChats.includes(tour.id) && (
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-rose-600 rounded-full animate-ping border border-slate-800" />
)}
{unreadTourChats.includes(tour.id) && (
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-rose-600 rounded-full border border-slate-800" />
)}
</button>
</div>
{/* Emergency Share Button - moved to bottom */}
<button
onClick={() => handleOpenShareModal(tour)}
className="w-full py-2 px-3 bg-rose-600/10 hover:bg-rose-600 text-rose-400 hover:text-white rounded-xl text-xs font-bold transition-all border border-rose-500/20 hover:border-rose-500 flex items-center justify-center gap-1.5"
onClick={() => handleEmergencyShare(tour)}
disabled={loadingShare}
className="w-full py-2 px-3 bg-rose-600/10 hover:bg-rose-600 text-rose-400 hover:text-white rounded-xl text-xs font-bold transition-all border border-rose-500/20 hover:border-rose-500 flex items-center justify-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ShieldAlert className="w-3.5 h-3.5 text-rose-500" />
<span>Chia sẻ khẩn cấp</span>
{loadingShare ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<ShieldAlert className="w-3.5 h-3.5 text-rose-500" />
)}
<span>Chia sẻ vị trí khẩn cấp</span>
</button>
</div>
{/* Hidden camera input */}
<input
ref={cameraInputRef}
type="file"
accept="image/*"
capture="environment"
onChange={handleCameraFileSelect}
className="hidden"
/>
</div>
</div>
</div>