Thêm tính năng xóa thành viên ra khỏi tour

This commit is contained in:
2026-06-14 17:43:28 +07:00
parent 4bec095a40
commit bb15b2bf15
15 changed files with 154 additions and 21 deletions
+49 -6
View File
@@ -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 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>