382 lines
13 KiB
TypeScript
382 lines
13 KiB
TypeScript
import type { LoaderFunctionArgs, ActionFunctionArgs, MetaFunction } from "@remix-run/node";
|
||
import { json, redirect } from "@remix-run/node";
|
||
import { useLoaderData, useSearchParams, useNavigation, useActionData } from "@remix-run/react";
|
||
import { useState, useEffect, useCallback } from "react";
|
||
import { protectUserManagementRoute } from "~/lib/auth-middleware.server";
|
||
import { getUsers, createUser, updateUser, deleteUser, toggleUserStatus } from "~/lib/user-management.server";
|
||
import { DashboardLayout } from "~/components/layout/DashboardLayout";
|
||
import { Text, Card, CardHeader, CardBody, Button, SearchInput, Modal } from "~/components/ui";
|
||
import { UserList } from "~/components/users/UserList";
|
||
import { UserForm } from "~/components/users/UserForm";
|
||
import type { UserWithoutPassword } from "~/types/database";
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "إدارة المستخدمين - نظام إدارة صيانة السيارات" },
|
||
{ name: "description", content: "إدارة حسابات المستخدمين" },
|
||
];
|
||
};
|
||
|
||
export async function loader({ request }: LoaderFunctionArgs) {
|
||
const user = await protectUserManagementRoute(request);
|
||
|
||
const url = new URL(request.url);
|
||
const searchQuery = url.searchParams.get("search") || "";
|
||
const page = parseInt(url.searchParams.get("page") || "1");
|
||
const limit = 10;
|
||
|
||
const { users, total, totalPages } = await getUsers(
|
||
user.authLevel,
|
||
searchQuery,
|
||
page,
|
||
limit
|
||
);
|
||
|
||
return json({
|
||
user,
|
||
users,
|
||
currentPage: page,
|
||
totalPages,
|
||
total,
|
||
searchQuery,
|
||
});
|
||
}
|
||
|
||
export async function action({ request }: ActionFunctionArgs) {
|
||
const user = await protectUserManagementRoute(request);
|
||
const formData = await request.formData();
|
||
const action = formData.get("_action") as string;
|
||
|
||
try {
|
||
switch (action) {
|
||
case "create": {
|
||
const userData = {
|
||
name: formData.get("name") as string,
|
||
username: formData.get("username") as string,
|
||
email: formData.get("email") as string,
|
||
password: formData.get("password") as string,
|
||
authLevel: parseInt(formData.get("authLevel") as string),
|
||
status: formData.get("status") as string,
|
||
};
|
||
|
||
const result = await createUser(userData, user.authLevel);
|
||
|
||
if (result.success) {
|
||
return json({ success: true, message: "تم إنشاء المستخدم بنجاح" });
|
||
} else {
|
||
return json({ success: false, error: result.error }, { status: 400 });
|
||
}
|
||
}
|
||
|
||
case "update": {
|
||
const userId = parseInt(formData.get("userId") as string);
|
||
const userData = {
|
||
name: formData.get("name") as string,
|
||
username: formData.get("username") as string,
|
||
email: formData.get("email") as string,
|
||
authLevel: parseInt(formData.get("authLevel") as string),
|
||
status: formData.get("status") as string,
|
||
};
|
||
|
||
const password = formData.get("password") as string;
|
||
if (password) {
|
||
(userData as any).password = password;
|
||
}
|
||
|
||
const result = await updateUser(userId, userData, user.authLevel);
|
||
|
||
if (result.success) {
|
||
return json({ success: true, message: "تم تحديث المستخدم بنجاح" });
|
||
} else {
|
||
return json({ success: false, error: result.error }, { status: 400 });
|
||
}
|
||
}
|
||
|
||
case "delete": {
|
||
const userId = parseInt(formData.get("userId") as string);
|
||
const result = await deleteUser(userId, user.authLevel);
|
||
|
||
if (result.success) {
|
||
return json({ success: true, message: "تم حذف المستخدم بنجاح" });
|
||
} else {
|
||
return json({ success: false, error: result.error }, { status: 400 });
|
||
}
|
||
}
|
||
|
||
case "toggle-status": {
|
||
const userId = parseInt(formData.get("userId") as string);
|
||
const result = await toggleUserStatus(userId, user.authLevel);
|
||
|
||
if (result.success) {
|
||
return json({ success: true, message: "تم تغيير حالة المستخدم بنجاح" });
|
||
} else {
|
||
return json({ success: false, error: result.error }, { status: 400 });
|
||
}
|
||
}
|
||
|
||
default:
|
||
return json({ success: false, error: "إجراء غير صحيح" }, { status: 400 });
|
||
}
|
||
} catch (error) {
|
||
console.error("User management action error:", error);
|
||
return json({ success: false, error: "حدث خطأ في الخادم" }, { status: 500 });
|
||
}
|
||
}
|
||
|
||
export default function Users() {
|
||
const { user, users, currentPage, totalPages, total, searchQuery } = useLoaderData<typeof loader>();
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
const navigation = useNavigation();
|
||
const actionData = useActionData<typeof action>();
|
||
|
||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
const [editingUser, setEditingUser] = useState<UserWithoutPassword | null>(null);
|
||
const [notification, setNotification] = useState<{
|
||
type: 'success' | 'error';
|
||
message: string;
|
||
} | null>(null);
|
||
|
||
const isLoading = navigation.state === "loading";
|
||
const isSubmitting = navigation.state === "submitting";
|
||
|
||
// Handle action results
|
||
useEffect(() => {
|
||
if (actionData) {
|
||
if (actionData.success) {
|
||
setNotification({
|
||
type: 'success',
|
||
message: actionData.message || 'تم تنفيذ العملية بنجاح',
|
||
});
|
||
setShowCreateModal(false);
|
||
setEditingUser(null);
|
||
} else {
|
||
setNotification({
|
||
type: 'error',
|
||
message: actionData.error || 'حدث خطأ أثناء تنفيذ العملية',
|
||
});
|
||
}
|
||
}
|
||
}, [actionData]);
|
||
|
||
// Clear notification after 5 seconds
|
||
useEffect(() => {
|
||
if (notification) {
|
||
const timer = setTimeout(() => {
|
||
setNotification(null);
|
||
}, 5000);
|
||
return () => clearTimeout(timer);
|
||
}
|
||
}, [notification]);
|
||
|
||
const handleSearch = useCallback((query: string) => {
|
||
const newSearchParams = new URLSearchParams(searchParams);
|
||
if (query) {
|
||
newSearchParams.set("search", query);
|
||
} else {
|
||
newSearchParams.delete("search");
|
||
}
|
||
newSearchParams.delete("page"); // Reset to first page
|
||
setSearchParams(newSearchParams);
|
||
}, [searchParams, setSearchParams]);
|
||
|
||
const handlePageChange = useCallback((page: number) => {
|
||
const newSearchParams = new URLSearchParams(searchParams);
|
||
newSearchParams.set("page", page.toString());
|
||
setSearchParams(newSearchParams);
|
||
}, [searchParams, setSearchParams]);
|
||
|
||
const handleEdit = useCallback((userToEdit: UserWithoutPassword) => {
|
||
setEditingUser(userToEdit);
|
||
}, []);
|
||
|
||
const handleDelete = useCallback((userId: number) => {
|
||
// Create a form and submit it
|
||
const form = document.createElement("form");
|
||
form.method = "POST";
|
||
form.style.display = "none";
|
||
|
||
const actionInput = document.createElement("input");
|
||
actionInput.type = "hidden";
|
||
actionInput.name = "_action";
|
||
actionInput.value = "delete";
|
||
form.appendChild(actionInput);
|
||
|
||
const userIdInput = document.createElement("input");
|
||
userIdInput.type = "hidden";
|
||
userIdInput.name = "userId";
|
||
userIdInput.value = userId.toString();
|
||
form.appendChild(userIdInput);
|
||
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
document.body.removeChild(form);
|
||
}, []);
|
||
|
||
const handleToggleStatus = useCallback((userId: number) => {
|
||
// Create a form and submit it
|
||
const form = document.createElement("form");
|
||
form.method = "POST";
|
||
form.style.display = "none";
|
||
|
||
const actionInput = document.createElement("input");
|
||
actionInput.type = "hidden";
|
||
actionInput.name = "_action";
|
||
actionInput.value = "toggle-status";
|
||
form.appendChild(actionInput);
|
||
|
||
const userIdInput = document.createElement("input");
|
||
userIdInput.type = "hidden";
|
||
userIdInput.name = "userId";
|
||
userIdInput.value = userId.toString();
|
||
form.appendChild(userIdInput);
|
||
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
document.body.removeChild(form);
|
||
}, []);
|
||
|
||
const handleFormSubmit = useCallback((formData: FormData) => {
|
||
// Create a form and submit it
|
||
const form = document.createElement("form");
|
||
form.method = "POST";
|
||
form.style.display = "none";
|
||
|
||
for (const [key, value] of formData.entries()) {
|
||
const input = document.createElement("input");
|
||
input.type = "hidden";
|
||
input.name = key;
|
||
input.value = value as string;
|
||
form.appendChild(input);
|
||
}
|
||
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
document.body.removeChild(form);
|
||
}, []);
|
||
|
||
return (
|
||
<DashboardLayout user={user}>
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex justify-between items-start">
|
||
<div>
|
||
<Text as="h1" size="2xl" weight="bold" className="text-gray-900">
|
||
إدارة المستخدمين
|
||
</Text>
|
||
<Text color="secondary" className="mt-2">
|
||
إدارة حسابات المستخدمين وصلاحيات الوصول ({total} مستخدم)
|
||
</Text>
|
||
</div>
|
||
<Button onClick={() => setShowCreateModal(true)}>
|
||
إضافة مستخدم جديد
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Notification */}
|
||
{notification && (
|
||
<div
|
||
className={`p-4 rounded-md ${notification.type === 'success'
|
||
? 'bg-green-50 text-green-800 border border-green-200'
|
||
: 'bg-red-50 text-red-800 border border-red-200'
|
||
}`}
|
||
>
|
||
<div className="flex">
|
||
<div className="flex-shrink-0">
|
||
{notification.type === 'success' ? (
|
||
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||
</svg>
|
||
) : (
|
||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||
</svg>
|
||
)}
|
||
</div>
|
||
<div className="mr-3">
|
||
<Text size="sm">{notification.message}</Text>
|
||
</div>
|
||
<div className="mr-auto pl-3">
|
||
<button
|
||
onClick={() => setNotification(null)}
|
||
className="inline-flex text-gray-400 hover:text-gray-600"
|
||
>
|
||
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Search and Filters */}
|
||
<Card>
|
||
<CardBody>
|
||
<div className="flex flex-col sm:flex-row gap-4">
|
||
<div className="flex-1">
|
||
<SearchInput
|
||
placeholder="البحث في المستخدمين..."
|
||
onSearch={handleSearch}
|
||
initialValue={searchQuery}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* Users List */}
|
||
<Card>
|
||
<CardHeader>
|
||
<Text weight="medium">قائمة المستخدمين</Text>
|
||
</CardHeader>
|
||
<CardBody padding="none">
|
||
<UserList
|
||
users={users}
|
||
currentPage={currentPage}
|
||
totalPages={totalPages}
|
||
onPageChange={handlePageChange}
|
||
onEdit={handleEdit}
|
||
onDelete={handleDelete}
|
||
onToggleStatus={handleToggleStatus}
|
||
currentUserAuthLevel={user.authLevel}
|
||
loading={isLoading}
|
||
/>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* Create User Modal */}
|
||
<Modal
|
||
isOpen={showCreateModal}
|
||
onClose={() => setShowCreateModal(false)}
|
||
title="إضافة مستخدم جديد"
|
||
size="lg"
|
||
>
|
||
<UserForm
|
||
onSubmit={handleFormSubmit}
|
||
onCancel={() => setShowCreateModal(false)}
|
||
loading={isSubmitting}
|
||
currentUserAuthLevel={user.authLevel}
|
||
/>
|
||
</Modal>
|
||
|
||
{/* Edit User Modal */}
|
||
<Modal
|
||
isOpen={!!editingUser}
|
||
onClose={() => setEditingUser(null)}
|
||
title="تعديل المستخدم"
|
||
size="lg"
|
||
>
|
||
{editingUser && (
|
||
<UserForm
|
||
user={editingUser}
|
||
onSubmit={handleFormSubmit}
|
||
onCancel={() => setEditingUser(null)}
|
||
loading={isSubmitting}
|
||
currentUserAuthLevel={user.authLevel}
|
||
/>
|
||
)}
|
||
</Modal>
|
||
</div>
|
||
</DashboardLayout>
|
||
);
|
||
} |