uup
This commit is contained in:
217
app/components/users/UserForm.tsx
Normal file
217
app/components/users/UserForm.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Form } from '@remix-run/react';
|
||||
import { Input, Button, Select, Text } from '~/components/ui';
|
||||
import { validateUser } from '~/lib/validation';
|
||||
import { AUTH_LEVELS, USER_STATUS } from '~/types/auth';
|
||||
import type { UserWithoutPassword } from '~/types/database';
|
||||
|
||||
interface UserFormProps {
|
||||
user?: UserWithoutPassword;
|
||||
onSubmit: (data: FormData) => void;
|
||||
onCancel: () => void;
|
||||
loading?: boolean;
|
||||
currentUserAuthLevel: number;
|
||||
}
|
||||
|
||||
export function UserForm({
|
||||
user,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
loading = false,
|
||||
currentUserAuthLevel,
|
||||
}: UserFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: user?.name || '',
|
||||
username: user?.username || '',
|
||||
email: user?.email || '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
authLevel: user?.authLevel || AUTH_LEVELS.USER,
|
||||
status: user?.status || USER_STATUS.ACTIVE,
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
||||
|
||||
const isEditing = !!user;
|
||||
|
||||
// Validate form data
|
||||
useEffect(() => {
|
||||
const validationData = {
|
||||
name: formData.name,
|
||||
username: formData.username,
|
||||
email: formData.email,
|
||||
authLevel: formData.authLevel,
|
||||
status: formData.status,
|
||||
};
|
||||
|
||||
// Only validate password for new users or when password is provided
|
||||
if (!isEditing || formData.password) {
|
||||
validationData.password = formData.password;
|
||||
}
|
||||
|
||||
const validation = validateUser(validationData);
|
||||
|
||||
// Add confirm password validation
|
||||
const newErrors = { ...validation.errors };
|
||||
if (!isEditing || formData.password) {
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
}, [formData, isEditing]);
|
||||
|
||||
const handleInputChange = (field: string, value: string | number) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
setTouched(prev => ({ ...prev, [field]: true }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Mark all fields as touched
|
||||
const allFields = Object.keys(formData);
|
||||
setTouched(allFields.reduce((acc, field) => ({ ...acc, [field]: true }), {}));
|
||||
|
||||
// Check if form is valid
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create FormData
|
||||
const submitData = new FormData();
|
||||
submitData.append('name', formData.name);
|
||||
submitData.append('username', formData.username);
|
||||
submitData.append('email', formData.email);
|
||||
submitData.append('authLevel', formData.authLevel.toString());
|
||||
submitData.append('status', formData.status);
|
||||
|
||||
if (!isEditing || formData.password) {
|
||||
submitData.append('password', formData.password);
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
submitData.append('userId', user.id.toString());
|
||||
submitData.append('_action', 'update');
|
||||
} else {
|
||||
submitData.append('_action', 'create');
|
||||
}
|
||||
|
||||
onSubmit(submitData);
|
||||
};
|
||||
|
||||
// Get available auth levels based on current user's level
|
||||
const getAuthLevelOptions = () => {
|
||||
const options = [];
|
||||
|
||||
if (currentUserAuthLevel === AUTH_LEVELS.SUPERADMIN) {
|
||||
options.push({ value: AUTH_LEVELS.SUPERADMIN, label: 'مدير عام' });
|
||||
}
|
||||
|
||||
if (currentUserAuthLevel <= AUTH_LEVELS.ADMIN) {
|
||||
options.push({ value: AUTH_LEVELS.ADMIN, label: 'مدير' });
|
||||
}
|
||||
|
||||
options.push({ value: AUTH_LEVELS.USER, label: 'مستخدم' });
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const statusOptions = [
|
||||
{ value: USER_STATUS.ACTIVE, label: 'نشط' },
|
||||
{ value: USER_STATUS.INACTIVE, label: 'غير نشط' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="الاسم"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
error={touched.name ? errors.name : undefined}
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="اسم المستخدم"
|
||||
value={formData.username}
|
||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||
error={touched.username ? errors.username : undefined}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="البريد الإلكتروني"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
error={touched.email ? errors.email : undefined}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label={isEditing ? "كلمة المرور الجديدة (اختياري)" : "كلمة المرور"}
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
error={touched.password ? errors.password : undefined}
|
||||
required={!isEditing}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="تأكيد كلمة المرور"
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
||||
error={touched.confirmPassword ? errors.confirmPassword : undefined}
|
||||
required={!isEditing || !!formData.password}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Select
|
||||
label="مستوى الصلاحية"
|
||||
value={formData.authLevel}
|
||||
onChange={(e) => handleInputChange('authLevel', parseInt(e.target.value))}
|
||||
options={getAuthLevelOptions()}
|
||||
error={touched.authLevel ? errors.authLevel : undefined}
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="الحالة"
|
||||
value={formData.status}
|
||||
onChange={(e) => handleInputChange('status', e.target.value)}
|
||||
options={statusOptions}
|
||||
error={touched.status ? errors.status : undefined}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start space-x-3 space-x-reverse pt-4">
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
loading={loading}
|
||||
disabled={Object.keys(errors).length > 0}
|
||||
>
|
||||
{isEditing ? 'تحديث المستخدم' : 'إنشاء المستخدم'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
إلغاء
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
233
app/components/users/UserList.tsx
Normal file
233
app/components/users/UserList.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import { useState, memo } from 'react';
|
||||
import { Form } from '@remix-run/react';
|
||||
import { DataTable, Pagination, Button, Text, ConfirmModal } from '~/components/ui';
|
||||
import { getAuthLevelName, getStatusName } from '~/lib/user-utils';
|
||||
import { useSettings } from '~/contexts/SettingsContext';
|
||||
import { AUTH_LEVELS } from '~/types/auth';
|
||||
import type { UserWithoutPassword } from '~/types/database';
|
||||
|
||||
interface UserListProps {
|
||||
users: UserWithoutPassword[];
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onEdit: (user: UserWithoutPassword) => void;
|
||||
onDelete: (userId: number) => void;
|
||||
onToggleStatus: (userId: number) => void;
|
||||
currentUserAuthLevel: number;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const UserList = memo(function UserList({
|
||||
users,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onToggleStatus,
|
||||
currentUserAuthLevel,
|
||||
loading = false,
|
||||
}: UserListProps) {
|
||||
const { formatDate } = useSettings();
|
||||
const [deleteModal, setDeleteModal] = useState<{
|
||||
isOpen: boolean;
|
||||
user: UserWithoutPassword | null;
|
||||
}>({ isOpen: false, user: null });
|
||||
|
||||
const [statusModal, setStatusModal] = useState<{
|
||||
isOpen: boolean;
|
||||
user: UserWithoutPassword | null;
|
||||
}>({ isOpen: false, user: null });
|
||||
|
||||
const handleDeleteClick = (user: UserWithoutPassword) => {
|
||||
setDeleteModal({ isOpen: true, user });
|
||||
};
|
||||
|
||||
const handleStatusClick = (user: UserWithoutPassword) => {
|
||||
setStatusModal({ isOpen: true, user });
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (deleteModal.user) {
|
||||
onDelete(deleteModal.user.id);
|
||||
}
|
||||
setDeleteModal({ isOpen: false, user: null });
|
||||
};
|
||||
|
||||
const handleStatusConfirm = () => {
|
||||
if (statusModal.user) {
|
||||
onToggleStatus(statusModal.user.id);
|
||||
}
|
||||
setStatusModal({ isOpen: false, user: null });
|
||||
};
|
||||
|
||||
const canEditUser = (user: UserWithoutPassword) => {
|
||||
// Superadmin can edit anyone
|
||||
if (currentUserAuthLevel === AUTH_LEVELS.SUPERADMIN) return true;
|
||||
|
||||
// Admin cannot edit superadmin
|
||||
if (currentUserAuthLevel === AUTH_LEVELS.ADMIN && user.authLevel === AUTH_LEVELS.SUPERADMIN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const canDeleteUser = (user: UserWithoutPassword) => {
|
||||
// Same rules as edit
|
||||
return canEditUser(user);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
header: 'الاسم',
|
||||
sortable: true,
|
||||
render: (user: UserWithoutPassword) => (
|
||||
<div>
|
||||
<Text weight="medium">{user.name}</Text>
|
||||
<Text size="sm" color="secondary">@{user.username}</Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
header: 'البريد الإلكتروني',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'authLevel',
|
||||
header: 'مستوى الصلاحية',
|
||||
render: (user: UserWithoutPassword) => {
|
||||
const levelName = getAuthLevelName(user.authLevel);
|
||||
const colorClass = user.authLevel === AUTH_LEVELS.SUPERADMIN
|
||||
? 'text-purple-600 bg-purple-100'
|
||||
: user.authLevel === AUTH_LEVELS.ADMIN
|
||||
? 'text-blue-600 bg-blue-100'
|
||||
: 'text-gray-600 bg-gray-100';
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
|
||||
{levelName}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'الحالة',
|
||||
render: (user: UserWithoutPassword) => {
|
||||
const statusName = getStatusName(user.status);
|
||||
const colorClass = user.status === 'active'
|
||||
? 'text-green-600 bg-green-100'
|
||||
: 'text-red-600 bg-red-100';
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
|
||||
{statusName}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'createdDate',
|
||||
header: 'تاريخ الإنشاء',
|
||||
sortable: true,
|
||||
render: (user: UserWithoutPassword) => (
|
||||
<Text size="sm" color="secondary">
|
||||
{formatDate(user.createdDate)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'الإجراءات',
|
||||
render: (user: UserWithoutPassword) => (
|
||||
<div className="flex items-center space-x-2 space-x-reverse">
|
||||
{canEditUser(user) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onEdit(user)}
|
||||
className='w-24'
|
||||
>
|
||||
تعديل
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canEditUser(user) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleStatusClick(user)}
|
||||
className='w-24'
|
||||
>
|
||||
{user.status === 'active' ? 'إلغاء تفعيل' : 'تفعيل'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canDeleteUser(user) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => handleDeleteClick(user)}
|
||||
className='w-24'
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data={users}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
emptyMessage="لا توجد مستخدمين"
|
||||
/>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-6">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<ConfirmModal
|
||||
isOpen={deleteModal.isOpen}
|
||||
onClose={() => setDeleteModal({ isOpen: false, user: null })}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
title="تأكيد الحذف"
|
||||
message={`هل أنت متأكد من حذف المستخدم "${deleteModal.user?.name}"؟ هذا الإجراء لا يمكن التراجع عنه.`}
|
||||
confirmText="حذف"
|
||||
cancelText="إلغاء"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
{/* Status Toggle Confirmation Modal */}
|
||||
<ConfirmModal
|
||||
isOpen={statusModal.isOpen}
|
||||
onClose={() => setStatusModal({ isOpen: false, user: null })}
|
||||
onConfirm={handleStatusConfirm}
|
||||
title={statusModal.user?.status === 'active' ? 'إلغاء تفعيل المستخدم' : 'تفعيل المستخدم'}
|
||||
message={
|
||||
statusModal.user?.status === 'active'
|
||||
? `هل أنت متأكد من إلغاء تفعيل المستخدم "${statusModal.user?.name}"؟`
|
||||
: `هل أنت متأكد من تفعيل المستخدم "${statusModal.user?.name}"؟`
|
||||
}
|
||||
confirmText={statusModal.user?.status === 'active' ? 'إلغاء تفعيل' : 'تفعيل'}
|
||||
cancelText="إلغاء"
|
||||
variant={statusModal.user?.status === 'active' ? 'warning' : 'info'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user