Files
LogisicsApp_Client/app/users/[userId]/page.tsx

429 lines
16 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.
"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { PageLoader } from "../../../Components/UI/Spinner";
import { Alert } from "../../../Components/UI/Alert";
import { fmtDate } from "../../../utils/helperFun";
import { getStoredToken } from "../../../lib/auth";
import { userService } from "../../../services/user.service";
import type { UserDetail } from "../../../types/user";
// ── helpers ───────────────────────────────────────────────────────────────────
function safeFmtDate(iso: string | null | undefined): string {
if (!iso) return "—";
try {
const d = new Date(iso);
if (isNaN(d.getTime())) return "—";
return fmtDate(iso);
} catch {
return "—";
}
}
// ── sub-components ────────────────────────────────────────────────────────────
function SectionCard({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-xl)",
boxShadow: "var(--shadow-card)",
overflow: "hidden",
}}>
<div style={{
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
}}>
<p style={{
margin: 0,
fontSize: 11,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.2em",
color: "var(--color-text-muted)",
}}>
{title}
</p>
</div>
<div style={{ padding: "1.25rem 1.5rem" }}>
{children}
</div>
</div>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{
fontSize: 11,
fontWeight: 600,
color: "var(--color-text-muted)",
textTransform: "uppercase",
letterSpacing: "0.15em",
}}>
{label}
</span>
<span style={{
fontSize: 13,
color: "var(--color-text-primary)",
fontFamily: "var(--font-sans)",
}}>
{children}
</span>
</div>
);
}
function ActiveBadge({ 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.75rem",
fontSize: 12, fontWeight: 600,
color: active ? "#166534" : "#991B1B",
}}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: active ? "#16A34A" : "#DC2626" }} />
{active ? "نشط" : "معطل"}
</span>
);
}
function DeletedBadge({ deleted }: { deleted: boolean }) {
if (!deleted) return (
<span style={{
display: "inline-flex", alignItems: "center", gap: 5,
borderRadius: "var(--radius-full)",
border: "1px solid #BBF7D0",
background: "#DCFCE7",
padding: "0.2rem 0.75rem",
fontSize: 12, fontWeight: 600,
color: "#166534",
}}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "#16A34A" }} />
موجود
</span>
);
return (
<span style={{
display: "inline-flex", alignItems: "center", gap: 5,
borderRadius: "var(--radius-full)",
border: "1px solid #FECACA",
background: "#FEF2F2",
padding: "0.2rem 0.75rem",
fontSize: 12, fontWeight: 600,
color: "#991B1B",
}}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "#DC2626" }} />
محذوف
</span>
);
}
// eye icon
function EyeIcon({ open }: { open: boolean }) {
if (open) return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94" />
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" />
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
);
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
// silhouette fallback avatar
function AvatarFallback() {
return (
<div style={{
width: 80, height: 80, borderRadius: "50%",
background: "var(--color-brand-100)",
border: "2px solid var(--color-brand-200)",
display: "flex", alignItems: "center", justifyContent: "center",
flexShrink: 0,
}}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#2563EB" strokeWidth="1.5">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</div>
);
}
// ── main page ─────────────────────────────────────────────────────────────────
export default function UserDetailPage() {
const params = useParams();
const router = useRouter();
const userId = typeof params?.userId === "string" ? params.userId : null;
const [user, setUser] = useState<UserDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showToken, setShowToken] = useState(false);
useEffect(() => {
if (!userId) {
router.replace("/users");
return;
}
let cancelled = false;
async function fetchUser() {
try {
const token = getStoredToken();
const res = await userService.getById(userId as string, token);
if (!cancelled) {
setUser((res as { data: UserDetail }).data ?? null);
}
} catch {
if (!cancelled) setError("تعذّر تحميل بيانات المستخدم. يرجى المحاولة لاحقاً.");
} finally {
if (!cancelled) setLoading(false);
}
}
fetchUser();
return () => { cancelled = true; };
}, [userId, router]);
// reset token visibility on unmount
useEffect(() => () => { setShowToken(false); }, []);
if (loading) return <PageLoader message="جارٍ تحميل بيانات المستخدم…" />;
if (error || !user) {
return (
<div dir="rtl" style={{ padding: "2rem", maxWidth: 560 }}>
<Alert
type="error"
title="خطأ في التحميل"
message={error ?? "المستخدم غير موجود."}
/>
<a
href="/users"
style={{
display: "inline-block", marginTop: "1rem",
fontSize: 13, fontWeight: 600,
color: "var(--color-brand-600)",
textDecoration: "underline",
}}
>
العودة إلى قائمة المستخدمين
</a>
</div>
);
}
return (
<div dir="rtl" style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* soft-deleted warning banner */}
{user.isDeleted && (
<div style={{
display: "flex", alignItems: "center", gap: 10,
background: "#FEF2F2", border: "1px solid #FECACA",
borderRadius: "var(--radius-lg)", padding: "0.875rem 1.25rem",
fontSize: 13, fontWeight: 600, color: "#991B1B",
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
هذا المستخدم محذوف تم حذفه بتاريخ {safeFmtDate(user.deletedAt)}.
</div>
)}
{/* page header */}
<header style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-xl)",
boxShadow: "var(--shadow-card)",
padding: "1.5rem 2rem",
}}>
<a
href="/users"
style={{
display: "inline-flex", alignItems: "center", gap: 6,
fontSize: 12, fontWeight: 600,
color: "var(--color-brand-600)",
textDecoration: "none", marginBottom: "1rem",
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="15 18 9 12 15 6" />
</svg>
العودة إلى المستخدمين
</a>
<div style={{ display: "flex", alignItems: "center", gap: "1.25rem", flexWrap: "wrap" }}>
{/* avatar */}
{user.photo
? <img src={user.photo} alt={user.name} style={{ width: 80, height: 80, borderRadius: "50%", objectFit: "cover", border: "2px solid var(--color-brand-200)", flexShrink: 0 }} />
: <AvatarFallback />
}
<div>
<p style={{ margin: 0, fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600 }}>
ملف المستخدم
</p>
<h1 style={{ margin: "4px 0 6px", fontSize: "1.4rem", fontWeight: 700, color: "var(--color-text-primary)" }}>
{user.name}
</h1>
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
{user.userName && (
<span style={{
fontFamily: "var(--font-mono)", fontSize: 12,
color: "#2563EB", fontWeight: 600,
background: "#EFF6FF", border: "1px solid #BFDBFE",
borderRadius: "var(--radius-sm)", padding: "2px 8px",
}}>
@{user.userName}
</span>
)}
<ActiveBadge active={user.isActive} />
<DeletedBadge deleted={user.isDeleted} />
</div>
</div>
</div>
</header>
{/* grid layout */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.25rem",
}}>
{/* Basic Info */}
<SectionCard title="المعلومات الأساسية">
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<Field label="الاسم الكامل">{user.name}</Field>
<Field label="البريد الإلكتروني">
{user.email
? <span style={{ direction: "ltr", display: "inline-block" }}>{user.email}</span>
: "—"
}
</Field>
<Field label="رقم الهاتف">
<span style={{ direction: "ltr", display: "inline-block", fontFamily: "var(--font-mono)" }}>
{user.phone || "—"}
</span>
</Field>
<Field label="المعرف">
<span style={{
fontFamily: "var(--font-mono)", fontSize: 11,
color: "var(--color-text-muted)", wordBreak: "break-all",
}}>
{user.id}
</span>
</Field>
</div>
</SectionCard>
{/* Role & Branch */}
<SectionCard title="الدور والفرع">
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<Field label="الدور">
<span style={{
display: "inline-flex", alignItems: "center",
borderRadius: "var(--radius-full)",
border: "1px solid #BFDBFE",
background: "#EFF6FF",
padding: "0.2rem 0.75rem",
fontSize: 12, fontWeight: 600,
color: "#1D4ED8",
}}>
{user.role?.name ?? "—"}
</span>
</Field>
{user.role?.description && (
<Field label="وصف الدور">
<span style={{ color: "var(--color-text-secondary)" }}>
{user.role.description}
</span>
</Field>
)}
<Field label="الفرع">{user.branch?.name ?? "—"}</Field>
</div>
</SectionCard>
{/* Account Status */}
<SectionCard title="حالة الحساب">
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<Field label="الحالة"><ActiveBadge active={user.isActive} /></Field>
<Field label="حالة الحذف"><DeletedBadge deleted={user.isDeleted} /></Field>
<Field label="تاريخ الإنشاء">{safeFmtDate(user.createdAt)}</Field>
<Field label="آخر تحديث">{safeFmtDate(user.updatedAt)}</Field>
<Field label="تاريخ الحذف">{safeFmtDate(user.deletedAt)}</Field>
<Field label="آخر تغيير لكلمة المرور">{safeFmtDate(user.passwordChangedAt)}</Field>
</div>
</SectionCard>
{/* Refresh Token */}
<SectionCard title="رمز التحديث">
{user.refreshToken ? (
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<div style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
background: "var(--color-surface-muted)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-md)",
padding: "0.625rem 0.875rem",
gap: "0.75rem",
}}>
<span style={{
fontFamily: "var(--font-mono)", fontSize: 12,
color: "var(--color-text-secondary)",
wordBreak: "break-all", flex: 1,
letterSpacing: showToken ? "normal" : "0.15em",
}}>
{showToken ? user.refreshToken : "••••••••••••••••••••••••"}
</span>
<button
type="button"
onClick={() => setShowToken(p => !p)}
title={showToken ? "إخفاء الرمز" : "إظهار الرمز"}
aria-label={showToken ? "إخفاء رمز التحديث" : "إظهار رمز التحديث"}
style={{
flexShrink: 0,
width: 30, height: 30,
borderRadius: "var(--radius-sm)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
color: "var(--color-text-muted)",
cursor: "pointer",
display: "inline-flex", alignItems: "center", justifyContent: "center",
transition: "opacity 150ms",
}}
>
<EyeIcon open={showToken} />
</button>
</div>
<p style={{ margin: 0, fontSize: 11, color: "var(--color-text-hint)" }}>
هذا الرمز حساس لا تشاركه مع أي أحد.
</p>
</div>
) : (
<p style={{
margin: 0, fontSize: 13,
color: "var(--color-text-muted)",
fontStyle: "italic",
}}>
غير متاح
</p>
)}
</SectionCard>
</div>
</div>
);
}