feat: tíng năng trò chuyện giữa các thành viên và tin nhắn trực tiếp cho bạn bè

This commit is contained in:
2026-06-21 09:46:37 +07:00
parent 392a4d4766
commit 8d79cd76f6
15 changed files with 3932 additions and 234 deletions
+31 -4
View File
@@ -10,6 +10,7 @@ import { useConfirm } from '@/hooks/useConfirm';
import { useNotification } from '@/hooks/useNotification';
import { CommentModal } from '@/components/CommentModal';
import { AddPhotoModal } from '@/components/AddPhotoModal';
import { TourChat } from '../components/TourChat';
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline, useMap, Tooltip } from 'react-leaflet';
import _MarkerClusterGroup from 'react-leaflet-cluster';
const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup;
@@ -361,8 +362,14 @@ export const TourDetailPage = ({
const [userSpeed, setUserSpeed] = useState<number | null>(null); // Tốc độ di chuyển từ GPS
// Di chuyển khai báo state lên trên useEffect để tránh lỗi "before initialization"
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings' | 'members'>('plan');
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings' | 'members' | 'chat'>(() => {
const defaultTab = localStorage.getItem('tour_detail_default_tab');
localStorage.removeItem('tour_detail_default_tab');
if (defaultTab === 'chat') return 'chat';
return 'plan';
});
const [mergingId, setMergingId] = useState<string | null>(null);
const [unreadChatCount, setUnreadChatCount] = useState(0);
const [viewMode, setViewMode] = useState<'map' | 'timeline'>('timeline');
const [isHeadingMode, setIsHeadingMode] = useState(false);
const [mapRotation, setMapRotation] = useState(0);
@@ -1137,8 +1144,14 @@ export const TourDetailPage = ({
handleCommentIncrement(data.locationId);
});
socket.on('tourMessageReceived', (data: any) => {
if (data.tourId === currentTour.id && activeTab !== 'chat') {
setUnreadChatCount(prev => prev + 1);
}
});
return () => { socket.disconnect(); };
}, [currentTour?.id]);
}, [currentTour?.id, activeTab]);
// This useEffect is for initial demo loading, might not be needed if tourId is always passed
// useEffect(() => {
@@ -1400,6 +1413,7 @@ export const TourDetailPage = ({
{ id: 'plan', label: 'Lộ trình', icon: MapIcon, visible: true },
{ id: 'expense', label: 'Chi phí', icon: Wallet, visible: ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '') },
{ id: 'photo', label: 'Ảnh', icon: ImageIcon, visible: true },
{ id: 'chat', label: 'Trò chuyện', icon: MessageSquare, visible: !!userRole, hasBadge: unreadChatCount > 0, badgeCount: unreadChatCount },
{ id: 'members', label: 'Thành viên', icon: Users, visible: canManage },
{ id: 'settings', label: 'Cài đặt', icon: Settings, visible: userRole === 'OWNER' },
].filter(t => t.visible);
@@ -1744,8 +1758,11 @@ export const TourDetailPage = ({
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex-1 flex items-center justify-center py-3 rounded-xl text-sm font-semibold transition-all ${tab.id === 'settings' && isPublicView ? 'hidden' : ''} ${ // Hide settings tab in public view
onClick={() => {
setActiveTab(tab.id as any);
if (tab.id === 'chat') setUnreadChatCount(0);
}}
className={`flex-1 flex items-center justify-center py-3 rounded-xl text-sm font-semibold transition-all relative ${tab.id === 'settings' && isPublicView ? 'hidden' : ''} ${ // Hide settings tab in public view
activeTab === tab.id
? 'bg-blue-50 text-blue-600 shadow-sm'
: 'text-gray-400 hover:text-gray-500 hover:bg-gray-50'
@@ -1753,6 +1770,11 @@ export const TourDetailPage = ({
>
<tab.icon className={`w-4 h-4 mr-2 ${activeTab === tab.id ? 'scale-110' : ''}`} />
{tab.label}
{tab.hasBadge && tab.badgeCount !== undefined && tab.badgeCount > 0 && (
<span className="absolute -top-1 -right-1.5 bg-red-500 text-white text-[9px] font-black rounded-full px-1.5 py-0.5 animate-bounce shadow-md">
{tab.badgeCount}
</span>
)}
</button>
))}
</div>
@@ -2667,6 +2689,11 @@ export const TourDetailPage = ({
onOpenAddMember={() => setIsAddMemberOpen(true)}
/>
)}
{activeTab === 'chat' && currentTour && (
<div className="animate-in fade-in slide-in-from-bottom-2">
<TourChat tourId={currentTour.id} />
</div>
)}
</div>
</div>