mirror of
https://github.com/dyzulk/trustlab.git
synced 2026-01-26 13:32:06 +07:00
First commit
This commit is contained in:
262
src/app/dashboard/admin/users/UsersManagementClient.tsx
Normal file
262
src/app/dashboard/admin/users/UsersManagementClient.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import axios from "@/lib/axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||
import ComponentCard from "@/components/common/ComponentCard";
|
||||
import { PlusIcon } from "@/icons";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import Button from "@/components/ui/button/Button";
|
||||
import ConfirmationModal from "@/components/common/ConfirmationModal";
|
||||
import UserModal from "@/components/users/UserModal";
|
||||
import { Edit, Trash, MoreVertical, Shield, User as UserIcon, Mail, Calendar, Search } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { getUserAvatar } from "@/lib/utils";
|
||||
import PageLoader from "@/components/ui/PageLoader";
|
||||
|
||||
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
|
||||
|
||||
export default function UsersManagementClient() {
|
||||
const t = useTranslations("Users");
|
||||
const { user: currentUser } = useAuth();
|
||||
const { addToast } = useToast();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [roleFilter, setRoleFilter] = useState("all");
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<any>(null);
|
||||
|
||||
const { data, error, mutate, isLoading } = useSWR("/api/admin/users", fetcher);
|
||||
|
||||
const hasAdminAccess = ['admin', 'owner'].includes(currentUser?.role || '');
|
||||
const isOwner = currentUser?.role === "owner";
|
||||
const isAdmin = currentUser?.role === "admin";
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await axios.delete(`/api/admin/users/${id}`);
|
||||
mutate();
|
||||
addToast(t("toast_deleted"), "success");
|
||||
setConfirmDeleteId(null);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
addToast(err.response?.data?.message || t("toast_delete_failed"), "error");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveUser = async (formData: any) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
if (editingUser) {
|
||||
await axios.patch(`/api/admin/users/${editingUser.id}`, formData);
|
||||
addToast(t("toast_updated"), "success");
|
||||
} else {
|
||||
await axios.post("/api/admin/users", formData);
|
||||
addToast(t("toast_created"), "success");
|
||||
}
|
||||
mutate();
|
||||
setIsModalOpen(false);
|
||||
setEditingUser(null);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
addToast(err.response?.data?.message || t("toast_save_failed"), "error");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = (u: any) => {
|
||||
setEditingUser(u);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleAddClick = () => {
|
||||
setEditingUser(null);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const users = data?.data || [];
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
return users.filter((u: any) => {
|
||||
const fullName = `${u.first_name || ''} ${u.last_name || ''}`.toLowerCase();
|
||||
const matchesSearch =
|
||||
fullName.includes(searchTerm.toLowerCase()) ||
|
||||
u.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesRole = roleFilter === "all" || u.role === roleFilter;
|
||||
return matchesSearch && matchesRole;
|
||||
});
|
||||
}, [users, searchTerm, roleFilter]);
|
||||
|
||||
if (error) return <div className="p-10 text-center text-error-500">{t("load_failed")}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle={t("management_title")} />
|
||||
|
||||
<div className="space-y-6">
|
||||
<ComponentCard
|
||||
title={t("dashboard_title")}
|
||||
desc={t("dashboard_desc")}
|
||||
headerAction={
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mt-4 sm:mt-0">
|
||||
<div className="relative w-full sm:w-auto">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<Search className="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("search_placeholder")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 pr-4 py-2 text-sm border border-gray-200 dark:border-gray-800 rounded-lg bg-transparent focus:ring-2 focus:ring-brand-500/10 outline-none w-full sm:w-64 dark:text-white/90 dark:placeholder:text-white/30"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||
<select
|
||||
value={roleFilter}
|
||||
onChange={(e) => setRoleFilter(e.target.value)}
|
||||
className="px-3 py-2 text-sm border border-gray-200 dark:border-gray-800 rounded-lg bg-white dark:bg-gray-900 outline-none dark:text-white/90 w-full sm:w-auto cursor-pointer"
|
||||
>
|
||||
<option value="all" className="bg-white dark:bg-gray-900">{t("all_roles")}</option>
|
||||
{isOwner && <option value="owner" className="bg-white dark:bg-gray-900">Owner</option>}
|
||||
<option value="admin" className="bg-white dark:bg-gray-900">{t("admins")}</option>
|
||||
<option value="customer" className="bg-white dark:bg-gray-900">{t("customers")}</option>
|
||||
</select>
|
||||
{hasAdminAccess && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 justify-center w-full sm:w-auto whitespace-nowrap"
|
||||
onClick={handleAddClick}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
{t("add_user")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="relative overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||
<th className="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">{t("user_th")}</th>
|
||||
<th className="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">{t("role_th")}</th>
|
||||
<th className="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">{t("joined_date_th")}</th>
|
||||
<th className="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest text-right">{t("actions_th")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50 dark:divide-gray-800/50">
|
||||
{filteredUsers.length > 0 ? (
|
||||
filteredUsers.map((u: any) => (
|
||||
<tr key={u.id} className="hover:bg-gray-50/50 dark:hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-10 h-10 rounded-full overflow-hidden border border-gray-200 dark:border-gray-700">
|
||||
<Image
|
||||
src={getUserAvatar(u)}
|
||||
alt={`${u.first_name} ${u.last_name}`}
|
||||
fill
|
||||
className="object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-bold text-gray-900 dark:text-white">{u.first_name} {u.last_name}</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||
<Mail className="w-3 h-3" /> {u.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest ${u.role === 'admin' || u.role === 'owner' ? 'bg-brand-50 text-brand-600 dark:bg-brand-500/10 dark:text-brand-400' : 'bg-gray-50 text-gray-600 dark:bg-gray-800 dark:text-gray-400'}`}>
|
||||
{u.role === 'admin' || u.role === 'owner' ? <Shield className="w-3 h-3" /> : <UserIcon className="w-3 h-3" />}
|
||||
{u.role === 'customer' ? t("customers").toLowerCase() : u.role}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 flex items-center gap-1.5">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
{new Date(u.created_at).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{(isOwner || (isAdmin && u.role === 'customer')) && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleEditClick(u)}
|
||||
className="p-2 text-gray-400 hover:text-brand-500 transition-colors"
|
||||
title={t("edit_user")}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDeleteId(u.id)}
|
||||
className={`p-2 text-gray-400 hover:text-red-500 transition-colors ${currentUser?.id === u.id ? 'opacity-20 pointer-events-none' : ''}`}
|
||||
title={t("delete_user")}
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : !isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-5 py-10 text-center text-gray-500 dark:text-gray-400 italic">
|
||||
{t("no_users_found")}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{(isLoading || isDeleting) && (
|
||||
<div className="absolute inset-0 bg-white/50 dark:bg-gray-900/50 flex items-center justify-center z-10 backdrop-blur-sm rounded-2xl">
|
||||
<PageLoader text={t("processing")} className="h-full" />
|
||||
</div>
|
||||
)}
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
isOpen={confirmDeleteId !== null}
|
||||
onClose={() => setConfirmDeleteId(null)}
|
||||
onConfirm={() => confirmDeleteId && handleDelete(confirmDeleteId)}
|
||||
title={t("delete_title")}
|
||||
message={t("delete_message")}
|
||||
isLoading={isDeleting}
|
||||
confirmLabel={t("delete_confirm")}
|
||||
requiredInput="DELETE"
|
||||
/>
|
||||
|
||||
<UserModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onSave={handleSaveUser}
|
||||
user={editingUser}
|
||||
isLoading={isSaving}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user