create middleware block and update proxy file also make token stored in cookes and up date user page to can show user details also fixed delete user error and delete car error and broken corse inginx image block now we can uplode image to server and but it in my app
This commit is contained in:
429
app/users/[userId]/page.tsx
Normal file
429
app/users/[userId]/page.tsx
Normal file
@@ -0,0 +1,429 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { Alert } from "../../Components/UI";
|
||||
import { UserTable } from "../../Components/User/UserTable";
|
||||
import { UserFormModal } from "../../Components/User/UserFormModal";
|
||||
import { UserDetailModal } from "../../Components/User/UserDetailModal";
|
||||
import { DeleteConfirmModal } from "../../Components/User/DeleteConfirmModal";
|
||||
import { Toast } from "../../Components/User/Toast";
|
||||
import { useUsers } from "../../hooks/useUser";
|
||||
@@ -14,6 +15,8 @@ export default function UsersPage() {
|
||||
// false = closed | null = create mode | User = edit mode
|
||||
const [formTarget, setFormTarget] = useState<User | null | false>(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<User | null>(null);
|
||||
// ID of user whose detail modal is open; null = closed
|
||||
const [viewUserId, setViewUserId] = useState<string | null>(null);
|
||||
// Local submitting flag shown in DeleteConfirmModal spinner
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
@@ -35,24 +38,14 @@ export default function UsersPage() {
|
||||
};
|
||||
|
||||
// ── Delete handler ──────────────────────────────────────────────────────────
|
||||
// FIX: We guard against double-click with local `deleting` flag,
|
||||
// then call deleteUser which:
|
||||
// 1. Calls the API
|
||||
// 2. Dispatches { type: "DELETE", id } → reducer removes user from array
|
||||
// 3. Calls notify() → Toast appears immediately
|
||||
// Result: the row disappears from the table and a success toast shows — all
|
||||
// without a page refresh.
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!deleteTarget || deleting) return;
|
||||
setDeleting(true);
|
||||
console.log(setDeleting(true))
|
||||
const ok = await deleteUser(deleteTarget.id);
|
||||
console.log("Delete result:", ok);
|
||||
console.log(ok)
|
||||
setDeleting(false);
|
||||
if (ok) {
|
||||
// Close modal only on success; on failure the user sees the error toast
|
||||
// and can retry or cancel manually.
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
if (ok) setDeleteTarget(null);
|
||||
};
|
||||
|
||||
// ── Render ──────────────────────────────────────────────────────────────────
|
||||
@@ -61,6 +54,14 @@ export default function UsersPage() {
|
||||
{/* Global success / error toast — fires on create, update, AND delete */}
|
||||
<Toast notification={notification} />
|
||||
|
||||
{/* User detail modal */}
|
||||
{viewUserId && (
|
||||
<UserDetailModal
|
||||
userId={viewUserId}
|
||||
onClose={() => setViewUserId(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Create / Edit modal */}
|
||||
{formTarget !== false && (
|
||||
<UserFormModal
|
||||
@@ -78,7 +79,6 @@ export default function UsersPage() {
|
||||
user={deleteTarget}
|
||||
deleting={deleting}
|
||||
onCancel={() => {
|
||||
// Only allow cancel when not mid-flight
|
||||
if (!deleting) setDeleteTarget(null);
|
||||
}}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
@@ -172,6 +172,7 @@ export default function UsersPage() {
|
||||
search={search}
|
||||
page={page}
|
||||
pages={pages}
|
||||
onView={user => setViewUserId(user.id)}
|
||||
onEdit={user => setFormTarget(user)}
|
||||
onDelete={user => setDeleteTarget(user)}
|
||||
onAddFirst={() => setFormTarget(null)}
|
||||
|
||||
Reference in New Issue
Block a user