This commit is contained in:
2026-01-23 20:35:40 +03:00
parent cf3b0e48ec
commit 66c151653e
137 changed files with 41495 additions and 0 deletions

382
app/routes/users.tsx Normal file
View File

@@ -0,0 +1,382 @@
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>
);
}