Thêm tính năng xóa thành viên ra khỏi tour
This commit is contained in:
+49
-6
@@ -1,13 +1,15 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { X, Search, UserPlus, Loader2, Shield, ShieldAlert } from 'lucide-react';
|
||||
|
||||
interface AddMemberModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
tourId: string;
|
||||
participants?: Array<{ userId: string; role: string; user?: { id: string; name: string; email: string } }>;
|
||||
onRemoveMember?: (userId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose, tourId }) => {
|
||||
export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose, tourId, participants = [], onRemoveMember }) => {
|
||||
const [query, setQuery] = useState('');
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -17,6 +19,9 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
const [fetchError, setFetchError] = useState('');
|
||||
const [submitError, setSubmitError] = useState('');
|
||||
|
||||
const participantIds = useMemo(() => new Set(participants.map((p) => p.userId)), [participants]);
|
||||
const visibleUsers = useMemo(() => users.filter((u) => !participantIds.has(u.id)), [users, participantIds]);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setLoading(true);
|
||||
setFetchError('');
|
||||
@@ -27,7 +32,7 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
});
|
||||
if (!res.ok) throw new Error('Không thể tải danh sách người dùng');
|
||||
const data = await res.json();
|
||||
setUsers(data);
|
||||
setUsers(Array.isArray(data) ? data : []);
|
||||
} catch (err: any) {
|
||||
setFetchError(err.message || 'Không thể tải danh sách người dùng');
|
||||
} finally {
|
||||
@@ -46,12 +51,23 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
setSelectedUser(null);
|
||||
setRole('MEMBER');
|
||||
setFetchError('');
|
||||
setSubmitError('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleRemove = async (userId: string, memberName: string) => {
|
||||
if (!window.confirm(`Xóa ${memberName} khỏi tour này?`) || !onRemoveMember) return;
|
||||
try {
|
||||
await onRemoveMember(userId);
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Không thể xóa thành viên');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!selectedUser) return;
|
||||
setSubmitting(true);
|
||||
setSubmitError('');
|
||||
try {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const res = await fetch(`${API_BASE}/api/v1/tours/${tourId}/members`, {
|
||||
@@ -68,7 +84,7 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
setSubmitError(err.message || 'Thêm thành viên thất bại');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -93,6 +109,28 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-gray-500 mb-2">Đã gán ({participants.length})</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{participants.map((p) => (
|
||||
<span key={p.userId} className="inline-flex items-center gap-1 rounded-full bg-gray-100 border border-gray-200 px-2.5 py-1 text-[11px] font-semibold text-gray-700">
|
||||
{p.user?.name || p.userId}
|
||||
{onRemoveMember && (
|
||||
<button
|
||||
onClick={() => handleRemove(p.userId, p.user?.name || p.userId)}
|
||||
className="leading-none text-gray-400 hover:text-red-500"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
{participants.length === 0 && (
|
||||
<span className="text-xs text-gray-400">Chưa có thành viên nào</span>
|
||||
)}
|
||||
</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
|
||||
@@ -125,11 +163,16 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
{fetchError}
|
||||
</div>
|
||||
)}
|
||||
{submitError && (
|
||||
<div className="p-3 bg-red-50 text-red-600 rounded-xl text-xs font-bold border border-red-100">
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-10"><Loader2 className="w-8 h-8 animate-spin text-blue-600" /></div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-[40vh] overflow-y-auto pr-1">
|
||||
{users.map((u) => {
|
||||
{visibleUsers.map((u) => {
|
||||
const isSelected = selectedUser === u.id;
|
||||
return (
|
||||
<button
|
||||
@@ -158,7 +201,7 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!loading && users.length === 0 && (
|
||||
{!loading && visibleUsers.length === 0 && (
|
||||
<div className="text-center py-8 text-sm text-gray-500">Không tìm thấy người dùng phù hợp</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user