import React, { useState, useEffect, useMemo } from 'react'; import { X, Search, UserPlus, Loader2, Shield, Trash2, Clock } from 'lucide-react'; import { useConfirm } from '@/hooks/useConfirm'; import { useNotification } from '@/hooks/useNotification'; interface AddMemberModalProps { isOpen: boolean; onClose: () => void; tourId: string; participants?: Array<{ id: string; userId?: string | null; role: string; displayName?: string | null; user?: { id: string; name: string; email: string } | null }>; joinRequests?: Array<{ id: string; userId: string; user?: { id: string; name: string; email: string }; status: string; requestedById: string }>; onRemoveMember?: (userId: string) => Promise; onMemberAdded?: () => void; userRole?: string; // User's role in the tour isPublicView?: boolean; // New prop to indicate public view } export const AddMemberModal: React.FC = ({ isOpen, onClose, tourId, participants = [], joinRequests = [], onRemoveMember, onMemberAdded, userRole, isPublicView }) => { const [activeTab, setActiveTab] = useState<'search' | 'email'>('search'); const [query, setQuery] = useState(''); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [role, setRole] = useState<'OWNER' | 'MANAGER' | 'MEMBER' | 'MEMBER_NO_FINANCE' | 'VIEWER_ONLY'>('MEMBER'); const [submitting, setSubmitting] = useState(false); const [fetchError, setFetchError] = useState(''); const [submitError, setSubmitError] = useState(''); const [actionLoading, setActionLoading] = useState(null); // Trạng thái cho tab mời qua email const [inviteEmail, setInviteEmail] = useState(''); const [inviteRole, setInviteRole] = useState<'OWNER' | 'MANAGER' | 'MEMBER' | 'MEMBER_NO_FINANCE' | 'VIEWER_ONLY'>('MEMBER'); const [inviteLoading, setInviteLoading] = useState(false); const [inviteError, setInviteError] = useState(''); const [inviteSuccess, setInviteSuccess] = useState(''); const confirm = useConfirm(); const notify = useNotification(); const participantIds = useMemo(() => new Set(participants.map((p) => p.userId).filter(Boolean) as string[]), [participants]); const requestUserIds = useMemo(() => new Set(joinRequests.map((r) => (r as any).userId)), [joinRequests]); const visibleUsers = useMemo(() => users.filter((u) => !participantIds.has(u.id)), [users, participantIds]); const canCreateDirectly = !userRole || userRole === 'OWNER' || userRole === 'MANAGER'; const canInviteByEmail = userRole === 'OWNER' || userRole === 'MANAGER'; const handleManualAdd = async () => { const name = query.trim(); if (!name) return; setSubmitting(true); setSubmitError(''); try { const res = await fetch(`/api/v1/tours/${tourId}/members`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, body: JSON.stringify({ displayName: name, role }), }); if (!res.ok) { const data = await res.json(); throw new Error(data.message || data.error || 'Thao tác thất bại'); } await onMemberAdded?.(); onClose(); } catch (err: any) { setSubmitError(err.message || 'Thao tác thất bại'); } finally { setSubmitting(false); } }; const fetchUsers = async () => { setLoading(true); setFetchError(''); try { const res = await fetch(`/api/v1/users?q=${encodeURIComponent(query)}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!res.ok) throw new Error('Không thể tải danh sách người dùng'); const data = await res.json(); setUsers(Array.isArray(data) ? data : []); } catch (err: any) { setFetchError(err.message || 'Không thể tải danh sách người dùng'); } finally { setLoading(false); } }; const handleSendInvite = async (e: React.FormEvent) => { e.preventDefault(); if (!inviteEmail.trim()) return; setInviteLoading(true); setInviteError(''); setInviteSuccess(''); try { const res = await fetch(`/api/v1/tours/${tourId}/invitations`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, body: JSON.stringify({ email: inviteEmail.trim(), role: inviteRole }), }); const data = await res.json(); if (!res.ok) { throw new Error(data.message || 'Gửi lời mời thất bại'); } setInviteSuccess(`Lời mời đã được gửi thành công đến ${inviteEmail}!`); setInviteEmail(''); notify({ title: 'Thành công', message: `Lời mời đã gửi tới ${inviteEmail}`, type: 'success' }); await onMemberAdded?.(); } catch (err: any) { setInviteError(err.message || 'Thao tác thất bại'); } finally { setInviteLoading(false); } }; useEffect(() => { if (!isOpen || activeTab !== 'search') return; const delayDebounceFn = setTimeout(() => { fetchUsers(); }, 300); return () => clearTimeout(delayDebounceFn); }, [query, isOpen, activeTab]); useEffect(() => { if (!isOpen) { setQuery(''); setSelectedUser(null); setRole('MEMBER'); setFetchError(''); setSubmitError(''); setInviteEmail(''); setInviteRole('MEMBER'); setInviteError(''); setInviteSuccess(''); setActiveTab('search'); } }, [isOpen]); const handleRemove = async (memberIdOrUserId: string, memberName: string) => { if (!onRemoveMember) return; const isConfirmed = await confirm({ title: 'Xóa thành viên', message: `Bạn có chắc chắn muốn xóa ${memberName} khỏi tour?` }); if (isConfirmed) { try { await onRemoveMember(memberIdOrUserId); } catch (err: any) { setSubmitError(err.message || 'Không thể xóa thành viên'); } } }; const handleRequestAction = async (reqId: string, action: 'accept' | 'reject') => { if (!onMemberAdded) return; setActionLoading(reqId); try { const endpoint = action === 'accept' ? `/api/v1/tours/${tourId}/join-requests/${reqId}/accept` : `/api/v1/tours/${tourId}/join-requests/${reqId}/reject`; const res = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }, }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.message || data.error || (action === 'accept' ? 'Không thể chấp nhận' : 'Không thể từ chối')); } await onMemberAdded(); } catch (err: any) { notify({ title: 'Lỗi', message: err.message || 'Thao tác thất bại', type: 'error' }); } finally { setActionLoading(null); } }; const handleAdd = async () => { if (!selectedUser) return; setSubmitting(true); setSubmitError(''); try { const endpoint = canCreateDirectly ? `/api/v1/tours/${tourId}/members` : `/api/v1/tours/${tourId}/join-requests`; const body = canCreateDirectly ? { userId: selectedUser, role } : { userId: selectedUser }; const res = await fetch(`${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, body: JSON.stringify(body), }); if (!res.ok) { const data = await res.json(); throw new Error(data.message || data.error || 'Thao tác thất bại'); } await onMemberAdded?.(); onClose(); } catch (err: any) { setSubmitError(err.message || 'Thao tác thất bại'); } finally { setSubmitting(false); } }; if (!isOpen || isPublicView) return null; // Do not render if public view return (

{canCreateDirectly ? 'Thành viên hành trình' : 'Mời tham gia tour'}

{canCreateDirectly ? 'Quản lý, thêm thành viên và mời người khác tham gia.' : 'Mời sẽ được gửi và chờ người quản lý phê duyệt.'}

{canInviteByEmail && (
)}
{/* Danh sách thành viên hiện tại */}

Thành viên của tour ({participants.filter(p => p.user || p.displayName).length})

{participants.filter(p => p.user || p.displayName).map((p) => { const rawToken = localStorage.getItem('token'); let currentUserId: string | null = null; try { const payload = JSON.parse(atob((rawToken || '').split('.')[1])); currentUserId = payload.sub; } catch { currentUserId = null; } const isCurrentUser = currentUserId && p.userId === currentUserId; const isOwner = p.role === 'OWNER'; const canRemove = onRemoveMember && !isCurrentUser && !isOwner; const memberName = p.user?.name || p.displayName || p.userId || 'Thành viên'; return (
{memberName.charAt(0)}
{canRemove && ( )}
{memberName}
); })} {participants.length === 0 && ( Chưa có thành viên nào )}
{/* Danh sách chờ duyệt */} {joinRequests.length > 0 && (

Đang chờ phê duyệt ({joinRequests.length})

{(joinRequests as any[]).map((req) => (
{req.user?.name?.charAt(0) || '?'}
{req.user?.name || req.userId} PENDING
))}
)}
{activeTab === 'search' ? (
{canCreateDirectly && (
)}
setQuery(e.target.value)} className="w-full pl-10 pr-10 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-gray-800" /> {query && ( )}
{fetchError && (
{fetchError}
)} {submitError && (
{submitError}
)} {loading ? (
) : (
{query.trim() && canCreateDirectly && ( )} {visibleUsers.map((u) => { const isSelected = selectedUser === u.id; return ( ); })} {!loading && visibleUsers.length === 0 && (
Không tìm thấy người dùng phù hợp
)}
)}
) : (
setInviteEmail(e.target.value)} className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-gray-800" />
{inviteError && (
{inviteError}
)} {inviteSuccess && (
{inviteSuccess}
)}
)}
); };