Thêm tính năng pending khi một thành viên thêm một người khác vào
This commit is contained in:
+57
-35
@@ -1,16 +1,18 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { X, Search, UserPlus, Loader2, Shield, Trash2 } from 'lucide-react';
|
||||
import { X, Search, UserPlus, Loader2, Shield, Trash2, Clock } from 'lucide-react';
|
||||
|
||||
interface AddMemberModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
tourId: string;
|
||||
participants?: Array<{ userId: string; role: string; user?: { id: string; name: string; email: string } }>;
|
||||
joinRequests?: Array<{ id: string; userId: string; user?: { id: string; name: string; email: string }; status: string; requestedById: string }>;
|
||||
onRemoveMember?: (userId: string) => Promise<void>;
|
||||
onMemberAdded?: () => void;
|
||||
userRole?: string;
|
||||
}
|
||||
|
||||
export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose, tourId, participants = [], onRemoveMember, onMemberAdded }) => {
|
||||
export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose, tourId, participants = [], joinRequests = [], onRemoveMember, onMemberAdded, userRole }) => {
|
||||
const [query, setQuery] = useState('');
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -23,8 +25,11 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
const [confirmTarget, setConfirmTarget] = useState<{ userId: string; name: string } | null>(null);
|
||||
|
||||
const participantIds = useMemo(() => new Set(participants.map((p) => p.userId)), [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 fetchUsers = async () => {
|
||||
setLoading(true);
|
||||
setFetchError('');
|
||||
@@ -82,22 +87,26 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
setSubmitError('');
|
||||
try {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const res = await fetch(`${API_BASE}/api/v1/tours/${tourId}/members`, {
|
||||
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({ userId: selectedUser, role }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.message || 'Thêm thành viên thất bại');
|
||||
throw new Error(data.message || data.error || 'Thao tác thất bại');
|
||||
}
|
||||
onMemberAdded?.();
|
||||
await onMemberAdded?.();
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setSubmitError(err.message || 'Thêm thành viên thất bại');
|
||||
setSubmitError(err.message || 'Thao tác thất bại');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -112,9 +121,11 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
<div className="p-5 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
||||
<UserPlus className="w-5 h-5 text-blue-600" /> Thêm thành viên
|
||||
<UserPlus className="w-5 h-5 text-blue-600" /> {canCreateDirectly ? 'Thêm thành viên' : 'Mời tham gia tour'}
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500">Chọn người dùng và phân quyền cho tour này.</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{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.'}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-2 hover:bg-gray-200 rounded-full transition-colors">
|
||||
<X className="w-5 h-5 text-gray-400" />
|
||||
@@ -163,31 +174,41 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
className="w-full pl-9 pr-4 py-2.5 bg-gray-50 border border-gray-100 rounded-xl outline-none text-sm"
|
||||
placeholder="Tìm theo tên hoặc email..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onBlur={fetchUsers}
|
||||
/>
|
||||
</div>
|
||||
{joinRequests.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-gray-500 mb-2 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3 text-amber-500" /> Đang chờ phê duyệt ({joinRequests.length})
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{(joinRequests as any[]).map((req) => (
|
||||
<div key={req.id} className="flex flex-col items-center gap-1">
|
||||
<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">
|
||||
{req.user?.name?.charAt(0) || '?'}
|
||||
</div>
|
||||
<span className="text-[10px] font-semibold text-gray-700 max-w-[72px] truncate">{req.user?.name || req.userId}</span>
|
||||
<span className="text-[9px] font-bold text-amber-600 bg-amber-100 px-1.5 py-0.5 rounded-full border border-amber-200">PENDING</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 ml-1">Phân quyền</label>
|
||||
<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 as any)}
|
||||
>
|
||||
<option value="OWNER">OWNER</option>
|
||||
<option value="MANAGER">MANAGER</option>
|
||||
<option value="MEMBER">MEMBER</option>
|
||||
<option value="MEMBER_NO_FINANCE">MEMBER_NO_FINANCE</option>
|
||||
<option value="VIEWER_ONLY">VIEWER_ONLY</option>
|
||||
</select>
|
||||
</div>
|
||||
{canCreateDirectly && (
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 ml-1">Phân quyền</label>
|
||||
<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 as any)}
|
||||
>
|
||||
<option value="OWNER">OWNER</option>
|
||||
<option value="MANAGER">MANAGER</option>
|
||||
<option value="MEMBER">MEMBER</option>
|
||||
<option value="MEMBER_NO_FINANCE">MEMBER_NO_FINANCE</option>
|
||||
<option value="VIEWER_ONLY">VIEWER_ONLY</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{fetchError && (
|
||||
@@ -210,9 +231,10 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
<button
|
||||
key={u.id}
|
||||
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' : ''}`}
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold">
|
||||
{u.name?.charAt(0) || '?'}
|
||||
@@ -250,7 +272,7 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
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"
|
||||
>
|
||||
{submitting ? 'Đang thêm...' : 'Thêm vào tour'}
|
||||
{submitting ? 'Đang xử lý...' : canCreateDirectly ? 'Thêm vào tour' : 'Gửi lời mời'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user