car_mms/app/routes/users.tsx
2025-09-11 14:22:27 +03:00

382 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}