Files

154 lines
15 KiB
JavaScript

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState, useEffect, useMemo } from 'react';
import { X, UserPlus, Loader2, Shield, Trash2, Clock } from 'lucide-react';
export const AddMemberModal = ({ isOpen, onClose, tourId, participants = [], joinRequests = [], onRemoveMember, onMemberAdded, userRole }) => {
const [query, setQuery] = useState('');
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const [role, setRole] = useState('MEMBER');
const [submitting, setSubmitting] = useState(false);
const [fetchError, setFetchError] = useState('');
const [submitError, setSubmitError] = useState('');
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [confirmTarget, setConfirmTarget] = useState(null);
const [actionLoading, setActionLoading] = useState(null);
const participantIds = useMemo(() => new Set(participants.map((p) => p.userId)), [participants]);
const requestUserIds = useMemo(() => new Set(joinRequests.map((r) => r.userId)), [joinRequests]);
const visibleUsers = useMemo(() => users.filter((u) => !participantIds.has(u.id)), [users, participantIds]);
const canCreateDirectly = !userRole || userRole === 'OWNER' || userRole === 'MANAGER';
const fetchUsers = async () => {
setLoading(true);
setFetchError('');
try {
const API_BASE = `http://${window.location.hostname}:3001`;
const res = await fetch(`${API_BASE}/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) {
setFetchError(err.message || 'Không thể tải danh sách người dùng');
}
finally {
setLoading(false);
}
};
useEffect(() => {
if (!isOpen)
return;
fetchUsers();
}, [isOpen]);
useEffect(() => {
if (!isOpen) {
setQuery('');
setSelectedUser(null);
setRole('MEMBER');
setFetchError('');
setSubmitError('');
}
}, [isOpen]);
const handleRemove = async (userId, memberName) => {
if (!onRemoveMember)
return;
setConfirmTarget({ userId, name: memberName });
setIsConfirmOpen(true);
};
const confirmRemove = async () => {
if (!confirmTarget || !onRemoveMember)
return;
try {
await onRemoveMember(confirmTarget.userId);
}
catch (err) {
setSubmitError(err.message || 'Không thể xóa thành viên');
}
finally {
setIsConfirmOpen(false);
setConfirmTarget(null);
}
};
const handleRequestAction = async (reqId, action, userName) => {
if (!onMemberAdded)
return;
setActionLoading(reqId);
try {
const API_BASE = `http://${window.location.hostname}:3001`;
const endpoint = action === 'accept'
? `${API_BASE}/api/v1/tours/${tourId}/join-requests/${reqId}/accept`
: `${API_BASE}/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) {
alert(err.message || 'Thao tác thất bại');
}
finally {
setActionLoading(null);
}
};
const handleAdd = async () => {
if (!selectedUser)
return;
setSubmitting(true);
setSubmitError('');
try {
const API_BASE = `http://${window.location.hostname}:3001`;
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(`${API_BASE}${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) {
setSubmitError(err.message || 'Thao tác thất bại');
}
finally {
setSubmitting(false);
}
};
if (!isOpen)
return null;
return (_jsxs("div", { className: "fixed inset-0 z-[2000] flex items-center justify-center p-4", children: [_jsx("div", { className: "absolute inset-0 bg-gray-900/60 backdrop-blur-sm", onClick: onClose }), _jsxs("div", { className: "relative w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]", children: [_jsxs("div", { className: "p-5 border-b border-gray-100 flex justify-between items-center bg-gray-50/50", children: [_jsxs("div", { children: [_jsxs("h2", { className: "text-lg font-bold text-gray-900 flex items-center gap-2", children: [_jsx(UserPlus, { className: "w-5 h-5 text-blue-600" }), " ", canCreateDirectly ? 'Thêm thành viên' : 'Mời tham gia tour'] }), _jsx("p", { className: "text-xs text-gray-500", children: canCreateDirectly ? 'Chọn người dùng và phân quyền cho tour này.' : 'Mời sẽ được gửi và chờ người quản lý phê duyệt.' })] }), _jsx("button", { onClick: onClose, className: "p-2 hover:bg-gray-200 rounded-full transition-colors", children: _jsx(X, { className: "w-5 h-5 text-gray-400" }) })] }), _jsxs("div", { className: "p-5 space-y-4", children: [_jsxs("div", { children: [_jsxs("p", { className: "text-[11px] font-bold text-gray-500 mb-2", children: ["Th\u00E0nh vi\u00EAn c\u1EE7a tour (", participants.length, ")"] }), _jsxs("div", { className: "flex flex-wrap gap-3", children: [participants.map((p) => {
const rawToken = localStorage.getItem('token');
let currentUserId = 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;
return (_jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsxs("div", { className: "relative", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-blue-100 border border-blue-200 flex items-center justify-center text-blue-700 font-bold text-sm overflow-hidden", children: p.user?.name?.charAt(0) || '?' }), canRemove && (_jsx("button", { onClick: () => handleRemove(p.userId, p.user?.name || p.userId), className: "absolute -top-1 -right-1 w-4 h-4 bg-gray-500 hover:bg-red-600 rounded-full flex items-center justify-center text-white", "aria-label": "Remove item", children: _jsx(Trash2, { size: 10 }) }))] }), _jsx("span", { className: "text-[10px] font-semibold text-gray-700 max-w-[72px] truncate", children: p.user?.name || p.userId })] }, p.userId));
}), participants.length === 0 && (_jsx("span", { className: "text-xs text-gray-400", children: "Ch\u01B0a c\u00F3 th\u00E0nh vi\u00EAn n\u00E0o" }))] })] }), joinRequests.length > 0 && (_jsxs("div", { children: [_jsxs("p", { className: "text-[11px] font-bold text-gray-500 mb-2 flex items-center gap-1", children: [_jsx(Clock, { className: "w-3 h-3 text-amber-500" }), " \u0110ang ch\u1EDD ph\u00EA duy\u1EC7t (", joinRequests.length, ")"] }), _jsx("div", { className: "flex flex-wrap gap-3", children: joinRequests.map((req) => (_jsxs("div", { className: "flex flex-col items-center gap-1 relative", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-amber-50 border border-amber-200 flex items-center justify-center text-amber-600 font-bold text-sm overflow-hidden", children: req.user?.name?.charAt(0) || '?' }), _jsxs("div", { className: "absolute -top-1 -right-1 flex", children: [_jsx("button", { type: "button", disabled: actionLoading === req.id, onClick: () => handleRequestAction(req.id, 'accept', req.user?.name || req.userId), className: "w-4 h-4 bg-green-500 hover:bg-green-600 rounded-full flex items-center justify-center text-white text-[10px] leading-none disabled:opacity-50", "aria-label": "Accept", children: "+" }), _jsx("button", { type: "button", disabled: actionLoading === req.id, onClick: () => handleRequestAction(req.id, 'reject', req.user?.name || req.userId), className: "w-4 h-4 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center text-white text-[10px] leading-none disabled:opacity-50 -ml-1", "aria-label": "Reject", children: "x" })] }), _jsx("span", { className: "text-[10px] font-semibold text-gray-700 max-w-[72px] truncate", children: req.user?.name || req.userId }), _jsx("span", { className: "text-[9px] font-bold text-amber-600 bg-amber-100 px-1.5 py-0.5 rounded-full border border-amber-200", children: "PENDING" })] }, req.id))) })] })), canCreateDirectly && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-xs font-bold text-gray-700 ml-1", children: "Ph\u00E2n quy\u1EC1n" }), _jsxs("select", { className: "w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm", value: role, onChange: (e) => setRole(e.target.value), children: [_jsx("option", { value: "OWNER", children: "OWNER" }), _jsx("option", { value: "MANAGER", children: "MANAGER" }), _jsx("option", { value: "MEMBER", children: "MEMBER" }), _jsx("option", { value: "MEMBER_NO_FINANCE", children: "MEMBER_NO_FINANCE" }), _jsx("option", { value: "VIEWER_ONLY", children: "VIEWER_ONLY" })] })] })), _jsxs("div", { className: "space-y-2", children: [fetchError && (_jsx("div", { className: "p-3 bg-red-50 text-red-600 rounded-xl text-xs font-bold border border-red-100", children: fetchError })), submitError && (_jsx("div", { className: "p-3 bg-red-50 text-red-600 rounded-xl text-xs font-bold border border-red-100", children: submitError })), loading ? (_jsx("div", { className: "flex justify-center py-10", children: _jsx(Loader2, { className: "w-8 h-8 animate-spin text-blue-600" }) })) : (_jsxs("div", { className: "space-y-2 max-h-[40vh] overflow-y-auto pr-1", children: [visibleUsers.map((u) => {
const isSelected = selectedUser === u.id;
return (_jsxs("button", { onClick: () => setSelectedUser(u.id), disabled: requestUserIds.has(u.id), className: `w-full flex items-center gap-3 p-3 rounded-2xl border transition-all ${isSelected ? 'border-blue-500 bg-blue-50/60' : 'border-gray-100 hover:border-blue-200'} ${requestUserIds.has(u.id) ? 'opacity-60' : ''}`, children: [_jsx("div", { className: "w-9 h-9 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold", children: u.name?.charAt(0) || '?' }), _jsxs("div", { className: "flex-1 text-left", children: [_jsx("div", { className: "text-sm font-bold text-gray-900", children: u.name || 'Chưa đặt tên' }), _jsx("div", { className: "text-[11px] text-gray-500", children: u.email }), _jsxs("div", { className: "text-[11px] text-gray-400", children: [u.phone || '', " ", u.address ? `• ${u.address}` : ''] })] }), _jsxs("div", { className: "flex items-center gap-1 text-[10px] font-bold text-gray-500", children: [u.isAdmin ? (_jsx("span", { className: "px-2 py-1 bg-purple-50 text-purple-600 rounded-md border border-purple-100", children: "ADMIN" })) : (_jsx("span", { className: "px-2 py-1 bg-gray-50 text-gray-500 rounded-md border border-gray-100", children: "USER" })), isSelected && _jsx(Shield, { className: "w-3 h-3 text-blue-600" })] })] }, u.id));
}), !loading && visibleUsers.length === 0 && (_jsx("div", { className: "text-center py-8 text-sm text-gray-500", children: "Kh\u00F4ng t\u00ECm th\u1EA5y ng\u01B0\u1EDDi d\u00F9ng ph\u00F9 h\u1EE3p" }))] }))] })] }), _jsxs("div", { className: "p-5 border-t border-gray-100 flex justify-end gap-2", children: [_jsx("button", { onClick: onClose, className: "px-4 py-2.5 rounded-xl text-sm font-bold text-gray-600 hover:bg-gray-100 transition-colors", children: "H\u1EE7y" }), _jsx("button", { disabled: !selectedUser || submitting, onClick: handleAdd, className: "px-4 py-2.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded-xl text-sm font-bold transition-all", children: submitting ? 'Đang xử lý...' : canCreateDirectly ? 'Thêm vào tour' : 'Gửi lời mời' })] })] }), isConfirmOpen && (_jsxs("div", { className: "fixed inset-0 z-[2100] flex items-center justify-center p-4", children: [_jsx("div", { className: "absolute inset-0 bg-gray-900/70 backdrop-blur-sm", onClick: () => setIsConfirmOpen(false) }), _jsxs("div", { className: "relative w-full max-w-sm bg-white rounded-2xl shadow-2xl p-5", children: [_jsx("h3", { className: "text-base font-bold text-gray-900", children: "X\u00E1c nh\u1EADn x\u00F3a th\u00E0nh vi\u00EAn" }), _jsxs("p", { className: "mt-2 text-sm text-gray-600", children: ["B\u1EA1n c\u00F3 ch\u1EAFc mu\u1ED1n x\u00F3a ", _jsx("span", { className: "font-semibold text-gray-800", children: confirmTarget?.name }), " kh\u1ECFi tour n\u00E0y?"] }), _jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [_jsx("button", { onClick: () => setIsConfirmOpen(false), className: "px-3 py-2 rounded-xl text-sm font-bold text-gray-600 hover:bg-gray-100 transition-colors", children: "H\u1EE7y" }), _jsx("button", { onClick: confirmRemove, className: "px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-bold transition-colors", children: "X\u00F3a" })] })] })] }))] }));
};
//# sourceMappingURL=AddMemberModal.js.map