224 lines
10 KiB
TypeScript
224 lines
10 KiB
TypeScript
import React, { useState, useEffect, useMemo } from 'react';
|
|
import { ItineraryTimeline } from './ItineraryTimeline.js';
|
|
import { ExpenseManager } from './ExpenseManager.js';
|
|
import { useTourStore } from './useTourStore.js';
|
|
import { AddLocationModal } from './AddLocationModal.js';
|
|
import {
|
|
Map as MapIcon,
|
|
Wallet,
|
|
Image as ImageIcon,
|
|
Calendar,
|
|
Users,
|
|
ChevronLeft,
|
|
Settings,
|
|
Quote,
|
|
Plus
|
|
} from 'lucide-react';
|
|
|
|
export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings'>('plan');
|
|
const [isAddLocationOpen, setIsAddLocationOpen] = useState(false);
|
|
const { currentTour, fetchTour, fetchPublicTours, publicTours, userRole } = useTourStore();
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
// Nếu chưa có tour nào trong store, thử tải danh sách public trước
|
|
if (publicTours.length === 0) {
|
|
await fetchPublicTours();
|
|
}
|
|
};
|
|
loadData();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
// Lấy tour đầu tiên từ danh sách khám phá để hiển thị demo
|
|
if (publicTours.length > 0 && !currentTour) {
|
|
fetchTour(publicTours[0].id);
|
|
}
|
|
}, [publicTours, currentTour, fetchTour]);
|
|
|
|
// Định nghĩa các tab dựa trên quyền hạn (RBAC) từ store
|
|
const tabs = [
|
|
{ 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: 'settings', label: 'Cài đặt', icon: Settings, visible: ['OWNER', 'MANAGER'].includes(userRole || '') },
|
|
].filter(t => t.visible);
|
|
|
|
const hasFinanceAccess = ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
|
|
|
|
const travelQuotes = [
|
|
"Đừng nghe họ nói, hãy tự mình đi xem.",
|
|
"Thế giới là một cuốn sách, và ai không đi du lịch thì chỉ mới đọc được một trang.",
|
|
"Hành trình ngàn dặm bắt đầu từ một bước chân.",
|
|
"Đi là để trở về, nhưng với một tâm hồn mới."
|
|
];
|
|
|
|
const randomQuote = useMemo(() => travelQuotes[Math.floor(Math.random() * travelQuotes.length)], []);
|
|
|
|
const tourInfo = {
|
|
title: currentTour?.title || "Hành trình khám phá TP.HCM",
|
|
date: currentTour?.startDate ? `${new Date(currentTour.startDate).toLocaleDateString('vi-VN')} - ${new Date(currentTour.endDate).toLocaleDateString('vi-VN')}` : "Chưa xác định ngày",
|
|
membersCount: currentTour?.participants?.length || 0,
|
|
budget: currentTour?.totalCost ? `${Number(currentTour.totalCost).toLocaleString()} VND` : "0 VND",
|
|
coverImage: currentTour?.photos?.[0]?.imageUrl || "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?q=80&w=2070&auto=format&fit=crop"
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 pb-20">
|
|
{/* Top Navigation Bar */}
|
|
<div className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-gray-100 px-4 py-3 flex items-center justify-between">
|
|
<button onClick={onBack} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
|
<ChevronLeft className="w-6 h-6 text-gray-600" />
|
|
</button>
|
|
<h1 className="text-lg font-bold text-gray-800 truncate px-4">
|
|
{tourInfo.title}
|
|
</h1>
|
|
<div className="w-10" /> {/* Spacer */}
|
|
</div>
|
|
|
|
{/* Tour Header Info */}
|
|
<div className="relative h-64 w-full bg-blue-900">
|
|
<img
|
|
src={tourInfo.coverImage}
|
|
className="w-full h-full object-cover opacity-60"
|
|
alt="Tour Cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
|
|
|
<div className="absolute bottom-16 left-0 right-0 p-6 text-white">
|
|
<div className="max-w-2xl mx-auto space-y-2">
|
|
<h2 className="text-3xl font-black tracking-tight drop-shadow-md">{tourInfo.title}</h2>
|
|
<div className="flex flex-wrap gap-4 text-sm font-medium opacity-90">
|
|
<div className="flex items-center bg-black/20 backdrop-blur-sm px-3 py-1 rounded-full border border-white/10">
|
|
<Calendar className="w-4 h-4 mr-1.5" />
|
|
{tourInfo.date}
|
|
</div>
|
|
<div className="flex items-center bg-black/20 backdrop-blur-sm px-3 py-1 rounded-full border border-white/10">
|
|
<Users className="w-4 h-4 mr-1.5" />
|
|
{tourInfo.membersCount} thành viên
|
|
</div>
|
|
</div>
|
|
|
|
{/* Member Avatars Stack */}
|
|
<div className="flex items-center gap-2 mt-4">
|
|
<div className="flex -space-x-3">
|
|
{currentTour?.participants?.slice(0, 5).map((p: any, i: number) => (
|
|
<div key={i} className="w-10 h-10 rounded-full border-2 border-white bg-blue-100 flex items-center justify-center text-blue-600 font-bold overflow-hidden shadow-lg">
|
|
<img src={`https://i.pravatar.cc/100?u=${p.userId}`} alt="Avatar" />
|
|
</div>
|
|
))}
|
|
{tourInfo.membersCount > 5 && (
|
|
<div className="w-10 h-10 rounded-full border-2 border-white bg-gray-800 flex items-center justify-center text-[10px] font-bold text-white shadow-lg">
|
|
+{tourInfo.membersCount - 5}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button className="p-2 bg-white/10 hover:bg-white/20 rounded-full border border-white/20 backdrop-blur-sm transition-all ml-2">
|
|
<Plus className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Financial Quick-View Widget or Quote */}
|
|
<div className="max-w-2xl mx-auto -mt-10 px-4 relative z-10">
|
|
<div className={`rounded-3xl p-6 shadow-2xl transition-all duration-500 ${hasFinanceAccess ? 'bg-indigo-600 text-white shadow-indigo-200' : 'bg-white text-gray-600 border border-gray-100'}`}>
|
|
<div className="flex justify-between items-center">
|
|
{hasFinanceAccess ? (
|
|
<>
|
|
<div>
|
|
<p className="text-indigo-100 text-xs font-black uppercase tracking-widest mb-1">Tổng chi tiêu hiện tại</p>
|
|
<h3 className="text-3xl font-black">{tourInfo.budget}</h3>
|
|
</div>
|
|
<div className="p-4 bg-white/10 rounded-2xl backdrop-blur-md"><Wallet className="w-8 h-8" /></div>
|
|
</>
|
|
) : (
|
|
<div className="flex items-start gap-4 py-2">
|
|
<Quote className="w-8 h-8 text-blue-500 opacity-30 flex-shrink-0" />
|
|
<p className="italic text-lg font-medium leading-relaxed">"{randomQuote}"</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content Area */}
|
|
<div className="max-w-2xl mx-auto mt-6 px-4 pb-24">
|
|
{/* Tab Switcher */}
|
|
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-1 flex mb-6 sticky top-20 z-20">
|
|
{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 ${
|
|
activeTab === tab.id
|
|
? 'bg-blue-50 text-blue-600 shadow-sm'
|
|
: 'text-gray-400 hover:text-gray-500 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<tab.icon className={`w-4 h-4 mr-2 ${activeTab === tab.id ? 'scale-110' : ''}`} />
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tab Panels */}
|
|
<div className="transition-opacity duration-300">
|
|
{activeTab === 'plan' && (
|
|
<div className="animate-in fade-in slide-in-from-bottom-4">
|
|
<ItineraryTimeline />
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'expense' && (
|
|
<div className="animate-in fade-in slide-in-from-bottom-4">
|
|
<ExpenseManager />
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'photo' && (
|
|
<div className="grid grid-cols-3 gap-1.5 animate-in fade-in">
|
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
<div key={i} className="aspect-square bg-gray-200 rounded-xl overflow-hidden relative group border border-white">
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
|
<img
|
|
src={`https://picsum.photos/seed/${i + 10}/400/400`}
|
|
alt="Tour photo"
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'settings' && (
|
|
<div className="p-8 text-center bg-white rounded-3xl border border-dashed border-gray-200 animate-in zoom-in-95">
|
|
<Settings className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
|
<p className="text-gray-500 font-medium">Tính năng quản lý thành viên đang được cập nhật...</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 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}
|
|
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>
|
|
</div>
|
|
|
|
{/* Add Location Modal */}
|
|
{currentTour && (
|
|
<AddLocationModal
|
|
isOpen={isAddLocationOpen}
|
|
onClose={() => setIsAddLocationOpen(false)}
|
|
tourId={currentTour.id}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}; |