fix: gửi vị trí khẩn cấp trong Dashboard
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user