Thêm thành viên bằng địa chỉ email
This commit is contained in:
+72
-20
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { X, Search, UserPlus, Loader2, Shield, ShieldAlert } from 'lucide-react';
|
||||
import { X, Search, UserPlus, Loader2, Shield, Trash2 } from 'lucide-react';
|
||||
|
||||
interface AddMemberModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -7,9 +7,10 @@ interface AddMemberModalProps {
|
||||
tourId: string;
|
||||
participants?: Array<{ userId: string; role: string; user?: { id: string; name: string; email: string } }>;
|
||||
onRemoveMember?: (userId: string) => Promise<void>;
|
||||
onMemberAdded?: () => void;
|
||||
}
|
||||
|
||||
export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose, tourId, participants = [], onRemoveMember }) => {
|
||||
export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose, tourId, participants = [], onRemoveMember, onMemberAdded }) => {
|
||||
const [query, setQuery] = useState('');
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -18,6 +19,8 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [fetchError, setFetchError] = useState('');
|
||||
const [submitError, setSubmitError] = useState('');
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||
const [confirmTarget, setConfirmTarget] = useState<{ userId: string; name: string } | null>(null);
|
||||
|
||||
const participantIds = useMemo(() => new Set(participants.map((p) => p.userId)), [participants]);
|
||||
const visibleUsers = useMemo(() => users.filter((u) => !participantIds.has(u.id)), [users, participantIds]);
|
||||
@@ -56,11 +59,20 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
}, [isOpen]);
|
||||
|
||||
const handleRemove = async (userId: string, memberName: string) => {
|
||||
if (!window.confirm(`Xóa ${memberName} khỏi tour này?`) || !onRemoveMember) return;
|
||||
if (!onRemoveMember) return;
|
||||
setConfirmTarget({ userId, name: memberName });
|
||||
setIsConfirmOpen(true);
|
||||
};
|
||||
|
||||
const confirmRemove = async () => {
|
||||
if (!confirmTarget || !onRemoveMember) return;
|
||||
try {
|
||||
await onRemoveMember(userId);
|
||||
await onRemoveMember(confirmTarget.userId);
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Không thể xóa thành viên');
|
||||
setSubmitError(err.message || 'Không thể xóa thành viên');
|
||||
} finally {
|
||||
setIsConfirmOpen(false);
|
||||
setConfirmTarget(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,6 +94,7 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
const data = await res.json();
|
||||
throw new Error(data.message || 'Thêm thành viên thất bại');
|
||||
}
|
||||
onMemberAdded?.();
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setSubmitError(err.message || 'Thêm thành viên thất bại');
|
||||
@@ -110,21 +123,40 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
|
||||
<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>
|
||||
))}
|
||||
<p className="text-[11px] font-bold text-gray-500 mb-2">Thành viên của tour ({participants.length})</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{participants.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;
|
||||
return (
|
||||
<div key={p.userId} className="flex flex-col items-center gap-1">
|
||||
<div className="relative">
|
||||
<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">
|
||||
{p.user?.name?.charAt(0) || '?'}
|
||||
</div>
|
||||
{canRemove && (
|
||||
<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"
|
||||
>
|
||||
<Trash2 size={10} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] font-semibold text-gray-700 max-w-[72px] truncate">{p.user?.name || p.userId}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{participants.length === 0 && (
|
||||
<span className="text-xs text-gray-400">Chưa có thành viên nào</span>
|
||||
)}
|
||||
@@ -222,6 +254,26 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isConfirmOpen && (
|
||||
<div className="fixed inset-0 z-[2100] flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-gray-900/70 backdrop-blur-sm" onClick={() => setIsConfirmOpen(false)} />
|
||||
<div className="relative w-full max-w-sm bg-white rounded-2xl shadow-2xl p-5">
|
||||
<h3 className="text-base font-bold text-gray-900">Xác nhận xóa thành viên</h3>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Bạn có chắc muốn xóa <span className="font-semibold text-gray-800">{confirmTarget?.name}</span> khỏi tour này?
|
||||
</p>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button onClick={() => setIsConfirmOpen(false)} className="px-3 py-2 rounded-xl text-sm font-bold text-gray-600 hover:bg-gray-100 transition-colors">
|
||||
Hủy
|
||||
</button>
|
||||
<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">
|
||||
Xóa
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user