create user pages crud also update pages ui build order and car ,driver first page interfase create model layer to componants
This commit is contained in:
58
Components/User/DeleteConfirmModal.tsx
Normal file
58
Components/User/DeleteConfirmModal.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { Spinner } from "../UI";
|
||||
import type { User } from "../../types/user";
|
||||
|
||||
interface DeleteConfirmModalProps {
|
||||
user: User;
|
||||
deleting: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function DeleteConfirmModal({ user, deleting, onCancel, onConfirm }: DeleteConfirmModalProps) {
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [onCancel]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alertdialog" aria-modal="true" aria-labelledby="del-title"
|
||||
onClick={e => { if (e.target === e.currentTarget) onCancel(); }}
|
||||
style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem" }}
|
||||
>
|
||||
<div
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{ width: "100%", maxWidth: 400, background: "var(--color-surface)", borderRadius: "var(--radius-xl)", border: "1px solid #FECACA", boxShadow: "0 20px 48px rgba(0,0,0,.18)", padding: "2rem", display: "flex", flexDirection: "column", gap: "1rem", textAlign: "center" }}
|
||||
>
|
||||
{/* أيقونة الحذف */}
|
||||
<div style={{ width: 52, height: 52, margin: "0 auto", borderRadius: "50%", background: "#FEF2F2", border: "1px solid #FECACA", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||
<polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6M14 11v6" /><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="del-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>حذف المستخدم</h2>
|
||||
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||
هل أنت متأكد من حذف <strong style={{ color: "var(--color-text-primary)" }}>{user.name}</strong>؟ لا يمكن التراجع عن هذا الإجراء.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
<button type="button" onClick={onCancel} disabled={deleting}
|
||||
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)" }}>
|
||||
إلغاء
|
||||
</button>
|
||||
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "none", background: "#DC2626", fontSize: 13, fontWeight: 700, color: "#FFF", cursor: deleting ? "not-allowed" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 8, fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1 }}>
|
||||
{deleting && <Spinner size="sm" className="text-white" />}
|
||||
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
Components/User/Toast.tsx
Normal file
56
Components/User/Toast.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Notification } from "../../hooks/useUser";
|
||||
|
||||
interface ToastProps {
|
||||
notification: Notification | null;
|
||||
}
|
||||
|
||||
export function Toast({ notification }: ToastProps) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (notification) {
|
||||
setVisible(true);
|
||||
} else {
|
||||
// dismiss after 250ms to allow exit animation
|
||||
const t = setTimeout(() => setVisible(false), 300);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, [notification]);
|
||||
|
||||
if (!visible && !notification) return null;
|
||||
|
||||
const isSuccess = notification?.type === "success";
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status" aria-live="polite" aria-atomic="true"
|
||||
style={{
|
||||
position: "fixed", bottom: 24, left: "50%",
|
||||
transform: `translateX(-50%) translateY(${notification ? "0" : "16px"})`,
|
||||
zIndex: 9999,
|
||||
transition: "transform 250ms ease, opacity 250ms ease",
|
||||
opacity: notification ? 1 : 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: 10,
|
||||
padding: "0.75rem 1.25rem",
|
||||
borderRadius: "var(--radius-full)",
|
||||
background: isSuccess ? "#065F46" : "#7F1D1D",
|
||||
color: "#FFFFFF",
|
||||
fontSize: 13, fontWeight: 600,
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.25)",
|
||||
maxWidth: "90vw", whiteSpace: "nowrap",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}>
|
||||
{/* icon */}
|
||||
<span style={{ fontSize: 16 }}>{isSuccess ? "✓" : "⚠"}</span>
|
||||
<span>{notification?.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
217
Components/User/UserFormModal.tsx
Normal file
217
Components/User/UserFormModal.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Alert, Spinner } from "../UI";
|
||||
import type { Branch } from "../../types/branch";
|
||||
import type { Role } from "../../types/role";
|
||||
import type { FormErrors, User, UserFormData } from "../../types/user";
|
||||
|
||||
// ── fixed styles ─────────────────────────────────────────────────
|
||||
const S = {
|
||||
input: {
|
||||
width: "100%", height: 40, padding: "0 0.75rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
fontSize: 13, color: "var(--color-text-primary)",
|
||||
outline: "none", fontFamily: "var(--font-sans)",
|
||||
} as React.CSSProperties,
|
||||
label: {
|
||||
display: "flex", flexDirection: "column" as const,
|
||||
gap: 6, fontSize: 12, fontWeight: 600,
|
||||
color: "var(--color-text-secondary)",
|
||||
} as React.CSSProperties,
|
||||
errorText: { fontSize: 11, color: "var(--color-danger)", fontWeight: 500 } as React.CSSProperties,
|
||||
};
|
||||
|
||||
// ── macksure data validation ─────────────────────────────────────────────────────
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const PHONE_RE = /^\+?[0-9]{10,15}$/;
|
||||
|
||||
function validate(data: UserFormData, isNew: boolean): FormErrors {
|
||||
const e: FormErrors = {};
|
||||
if (!data.name.trim()) e.name = "الاسم الكامل مطلوب";
|
||||
if (data.email && !EMAIL_RE.test(data.email)) e.email = "صيغة البريد الإلكتروني غير صحيحة";
|
||||
if (!data.phone.trim()) e.phone = "رقم الهاتف مطلوب";
|
||||
else if (!PHONE_RE.test(data.phone.replace(/\s/g, ""))) e.phone = "رقم هاتف غير صالح (10–15 رقم)";
|
||||
if (isNew && !data.password) e.password = "كلمة المرور مطلوبة";
|
||||
if (isNew && data.password && data.password.length < 6) e.password = "كلمة المرور 6 أحرف على الأقل";
|
||||
if (!data.roleId) e.roleId = "الدور مطلوب";
|
||||
if (!data.branchId) e.branchId = "الفرع مطلوب";
|
||||
return e;
|
||||
}
|
||||
|
||||
// ── Props ─────────────────────────────────────────────────────────────────────
|
||||
interface UserFormModalProps {
|
||||
editUser: User | null;
|
||||
roles: Role[];
|
||||
branches: Branch[];
|
||||
onClose: () => void;
|
||||
onSubmit: (data: UserFormData, isNew: boolean) => Promise<boolean>;
|
||||
}
|
||||
|
||||
// ── main component ────────────────────────────────────────────────────────────
|
||||
export function UserFormModal({ editUser, roles, branches, onClose, onSubmit }: UserFormModalProps) {
|
||||
const isNew = editUser === null;
|
||||
|
||||
const [form, setForm] = useState<UserFormData>({
|
||||
name: editUser?.name ?? "",
|
||||
email: editUser?.email ?? "",
|
||||
phone: editUser?.phone ?? "",
|
||||
password: "",
|
||||
roleId: editUser?.role?.id ?? "",
|
||||
branchId: editUser?.branch?.id ?? "",
|
||||
});
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [apiError, setApiError] = useState("");
|
||||
const firstInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => { firstInputRef.current?.focus(); }, []);
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [onClose]);
|
||||
|
||||
const set = (field: keyof UserFormData) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
setForm(p => ({ ...p, [field]: e.target.value }));
|
||||
if (errors[field]) setErrors(p => ({ ...p, [field]: undefined }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// makesure data is valid before sending to API
|
||||
const errs = validate(form, isNew);
|
||||
if (Object.keys(errs).length) { setErrors(errs); return; }
|
||||
// final check to prevent sending bad data to API (shouldn't happen because of validation, but just in case)
|
||||
if (!form.name.trim() || !form.phone.trim() || !form.roleId || !form.branchId) {
|
||||
setApiError("يرجى التأكد من إدخال جميع البيانات المطلوبة.");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setApiError("");
|
||||
const ok = await onSubmit(form, isNew);
|
||||
setSaving(false);
|
||||
if (ok) onClose();
|
||||
else setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.");
|
||||
};
|
||||
|
||||
// dynamic styles for inputs with errors
|
||||
const inputStyle = (field: keyof FormErrors): React.CSSProperties => ({
|
||||
...S.input,
|
||||
...(errors[field] ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-title"
|
||||
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 50,
|
||||
background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
width: "100%", maxWidth: 520,
|
||||
background: "var(--color-surface)",
|
||||
borderRadius: "var(--radius-2xl)",
|
||||
border: "1px solid var(--color-border)",
|
||||
boxShadow: "0 24px 64px rgba(0,0,0,.18)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* header */}
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
padding: "1.25rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)",
|
||||
}}>
|
||||
<div>
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600, margin: 0 }}>
|
||||
{isNew ? "إضافة مستخدم" : "تعديل مستخدم"}
|
||||
</p>
|
||||
<h2 id="modal-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||
{isNew ? "مستخدم جديد" : editUser?.name}
|
||||
</h2>
|
||||
</div>
|
||||
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||
style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* body*/}
|
||||
<form onSubmit={handleSubmit} noValidate style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||
{apiError && <Alert type="error" message={apiError} onClose={() => setApiError("")} />}
|
||||
|
||||
{/* name field*/}
|
||||
<label style={S.label}>
|
||||
الاسم الكامل *
|
||||
<input ref={firstInputRef} style={inputStyle("name")} value={form.name} onChange={set("name")} placeholder="أحمد الرشيدي" autoComplete="name" dir="rtl" />
|
||||
{errors.name && <span style={S.errorText}>{errors.name}</span>}
|
||||
</label>
|
||||
|
||||
{/* email and phone fields */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
|
||||
<label style={S.label}>
|
||||
البريد الإلكتروني
|
||||
<input style={inputStyle("email")} type="email" value={form.email} onChange={set("email")} placeholder="ahmed@co.sa" autoComplete="email" dir="ltr" />
|
||||
{errors.email && <span style={S.errorText}>{errors.email}</span>}
|
||||
</label>
|
||||
<label style={S.label}>
|
||||
رقم الهاتف *
|
||||
<input style={inputStyle("phone")} type="tel" value={form.phone} onChange={set("phone")} placeholder="+966 5x xxx xxxx" autoComplete="tel" dir="ltr" />
|
||||
{errors.phone && <span style={S.errorText}>{errors.phone}</span>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* password field */}
|
||||
<label style={S.label}>
|
||||
{isNew ? "كلمة المرور *" : "كلمة المرور الجديدة (اتركها فارغة إذا لا تريد تغييرها)"}
|
||||
<input style={inputStyle("password")} type="password" value={form.password} onChange={set("password")} placeholder="••••••••" autoComplete={isNew ? "new-password" : "off"} dir="ltr" />
|
||||
{errors.password && <span style={S.errorText}>{errors.password}</span>}
|
||||
</label>
|
||||
|
||||
{/* role and branch fields */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
|
||||
<label style={S.label}>
|
||||
الدور *
|
||||
<select style={{ ...inputStyle("roleId"), cursor: "pointer" }} value={form.roleId} onChange={set("roleId")} dir="rtl">
|
||||
<option value="">اختر الدور</option>
|
||||
{roles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||
</select>
|
||||
{errors.roleId && <span style={S.errorText}>{errors.roleId}</span>}
|
||||
</label>
|
||||
<label style={S.label}>
|
||||
الفرع *
|
||||
<select style={{ ...inputStyle("branchId"), cursor: "pointer" }} value={form.branchId} onChange={set("branchId")} dir="rtl">
|
||||
<option value="">اختر الفرع</option>
|
||||
{branches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
|
||||
</select>
|
||||
{errors.branchId && <span style={S.errorText}>{errors.branchId}</span>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* action buttons */}
|
||||
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end", paddingTop: "0.5rem" }}>
|
||||
<button type="button" onClick={onClose} disabled={saving}
|
||||
style={{ height: 40, padding: "0 1.25rem", borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: saving ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)" }}>
|
||||
إلغاء
|
||||
</button>
|
||||
<button type="submit" disabled={saving}
|
||||
style={{ height: 40, padding: "0 1.5rem", borderRadius: "var(--radius-md)", border: "none", background: saving ? "var(--color-brand-400)" : "var(--color-brand-600)", fontSize: 13, fontWeight: 700, color: "#FFF", cursor: saving ? "not-allowed" : "pointer", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-sans)" }}>
|
||||
{saving && <Spinner size="sm" className="text-white" />}
|
||||
{saving ? "جارٍ الحفظ…" : isNew ? "إضافة المستخدم" : "حفظ التغييرات"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
157
Components/User/UserTable.tsx
Normal file
157
Components/User/UserTable.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "../UI";
|
||||
import type { User } from "../../types/user";
|
||||
|
||||
// ── role ────────────────────────────────────────────────────────────────
|
||||
function RoleBadge({ name }: { name?: string }) {
|
||||
const isAdmin = name === "مدير النظام";
|
||||
return (
|
||||
<span style={{ display: "inline-flex", alignItems: "center", borderRadius: "var(--radius-full)", border: isAdmin ? "1px solid #BFDBFE" : "1px solid var(--color-border)", background: isAdmin ? "#EFF6FF" : "var(--color-surface-muted)", padding: "0.2rem 0.625rem", fontSize: 11, fontWeight: 600, color: isAdmin ? "#1D4ED8" : "var(--color-text-muted)" }}>
|
||||
{name ?? "—"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ── status ───────────────────────────────────────────────────────────────
|
||||
function StatusBadge({ active }: { active: boolean }) {
|
||||
return (
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 5, borderRadius: "var(--radius-full)", border: active ? "1px solid #BBF7D0" : "1px solid #FECACA", background: active ? "#DCFCE7" : "#FEF2F2", padding: "0.2rem 0.625rem", fontSize: 11, fontWeight: 600, color: active ? "#166534" : "#991B1B" }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: active ? "#16A34A" : "#DC2626" }} />
|
||||
{active ? "نشط" : "معطل"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// icon buton ───────────────────────────────────────────────────────────────
|
||||
function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<button type="button" title={title} aria-label={title} onClick={e => { e.stopPropagation(); onClick(); }}
|
||||
style={{ width: 32, height: 32, borderRadius: "var(--radius-md)", border: `1px solid ${borderColor}`, background: bg, color, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "opacity 150ms" }}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ── title style ───────────────────────────────────────────────────
|
||||
const cardStyle: React.CSSProperties = {
|
||||
borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)", overflow: "hidden", boxShadow: "var(--shadow-card)",
|
||||
};
|
||||
const thStyle: React.CSSProperties = {
|
||||
padding: "0.75rem 1.5rem", fontSize: 11, fontWeight: 700,
|
||||
textTransform: "uppercase", letterSpacing: "0.2em",
|
||||
color: "var(--color-text-muted)", background: "var(--color-surface-muted)",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
};
|
||||
|
||||
// ── Props ──────────────────────────────────────────────────────────────
|
||||
interface UserTableProps {
|
||||
users: User[];
|
||||
loading: boolean;
|
||||
search: string;
|
||||
page: number;
|
||||
pages: number;
|
||||
onEdit: (user: User) => void;
|
||||
onDelete: (user: User) => void;
|
||||
onAddFirst: () => void;
|
||||
onPageChange: (p: number) => void;
|
||||
}
|
||||
|
||||
// ── main table ────────────────────────────────────────────────────────────
|
||||
export function UserTable({ users, loading, search, page, pages, onEdit, onDelete, onAddFirst, onPageChange }: UserTableProps) {
|
||||
return (
|
||||
<div style={cardStyle}>
|
||||
{/* column headers */}
|
||||
<div dir="rtl" style={{ display: "grid", gridTemplateColumns: "2fr 1.5fr 1.5fr 1fr 1fr 1fr 80px", ...thStyle }}>
|
||||
<span>الاسم</span>
|
||||
<span>اسم المستخدم</span>
|
||||
<span>الفرع</span>
|
||||
<span>الدور</span>
|
||||
<span>الحالة</span>
|
||||
<span style={{ textAlign: "center" }}>تاريخ الإنشاء</span>
|
||||
<span style={{ textAlign: "center" }}>إجراءات</span>
|
||||
</div>
|
||||
|
||||
{/* loading state */}
|
||||
{loading ? (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "4rem 0", color: "var(--color-text-muted)" }}>
|
||||
<Spinner size="sm" className="text-blue-600" />
|
||||
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||
</div>
|
||||
) : users.length === 0 ? (
|
||||
// empty state
|
||||
<div style={{ textAlign: "center", padding: "4rem 1rem" }}>
|
||||
<p style={{ fontSize: 13, color: "var(--color-text-muted)" }}>
|
||||
{search ? `لا توجد نتائج لـ "${search}"` : "لا يوجد مستخدمون لعرضهم."}
|
||||
</p>
|
||||
{!search && (
|
||||
<button type="button" onClick={onAddFirst}
|
||||
style={{ marginTop: 12, fontSize: 13, fontWeight: 600, color: "var(--color-brand-600)", background: "none", border: "none", cursor: "pointer", textDecoration: "underline" }}>
|
||||
أضف أول مستخدم
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// data rows
|
||||
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
||||
{users.map((u, i) => (
|
||||
<li key={u.id} style={{
|
||||
display: "grid", gridTemplateColumns: "2fr 1.5fr 1.5fr 1fr 1fr 1fr 80px",
|
||||
alignItems: "center", gap: "0.5rem", padding: "0.875rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent",
|
||||
fontSize: 13,
|
||||
}}>
|
||||
<div>
|
||||
<p style={{ fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>{u.name}</p>
|
||||
<p style={{ marginTop: 2, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-muted)" }}>{u.phone}</p>
|
||||
</div>
|
||||
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>{u.userName ?? "—"}</span>
|
||||
<span style={{ color: "var(--color-text-secondary)" }}>{u.branch?.name ?? "—"}</span>
|
||||
<RoleBadge name={u.role?.name} />
|
||||
<StatusBadge active={u.isActive} />
|
||||
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||
{new Date(u.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })}
|
||||
</span>
|
||||
<div style={{ display: "flex", justifyContent: "center", gap: 6 }}>
|
||||
<IconBtn title={`تعديل ${u.name}`} color="#1D4ED8" bg="#EFF6FF" borderColor="#BFDBFE" onClick={() => onEdit(u)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</IconBtn>
|
||||
<IconBtn title={`حذف ${u.name}`} color="#DC2626" bg="#FEF2F2" borderColor="#FECACA" onClick={() => onDelete(u)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6M14 11v6" /><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||
</svg>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* pagination */}
|
||||
{pages > 1 && (
|
||||
<div dir="rtl" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--color-border)", padding: "0.875rem 1.5rem" }}>
|
||||
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||
صفحة <strong style={{ color: "var(--color-text-primary)" }}>{page}</strong> من <strong style={{ color: "var(--color-text-primary)" }}>{pages}</strong>
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
{[
|
||||
{ label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 },
|
||||
{ label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages },
|
||||
].map(btn => (
|
||||
<button key={btn.label} type="button" onClick={btn.action} disabled={btn.disabled}
|
||||
style={{ borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-surface-muted)", padding: "0.375rem 0.875rem", fontSize: 12, color: "var(--color-text-secondary)", cursor: btn.disabled ? "not-allowed" : "pointer", opacity: btn.disabled ? 0.4 : 1, fontFamily: "var(--font-sans)" }}>
|
||||
{btn.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user