Files
LogisicsApp_Client/app/dashboard/role_permission/page.tsx
2026-06-24 18:01:01 +03:00

238 lines
11 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 { useCallback, useEffect, useState } from "react";
import { Alert, Spinner } from "@/src/Components/UI";
import { RoleToast } from "@/src/Components/role/RoleToast";
import { useRoles } from "@/src/hooks/useRole";
import type { Role, Permission } from "@/src/services/role.service";
// ── Module label map ───────────────────────────────────────────────────────
const MODULE_LABELS: Record<string, string> = {
Role: "الأدوار", User: "المستخدمون", Branch: "الفروع",
Car: "المركبات", CarImage: "صور المركبات", Driver: "السائقون",
Order: "الطلبات", Trip: "الرحلات", Client: "العملاء",
Permission: "الصلاحيات", Audit: "سجل التدقيق",
Dashboard: "لوحة التحكم", Maintenance: "الصيانة",
};
const moduleLabel = (mod: string) => MODULE_LABELS[mod] ?? mod;
function groupByModule(permissions: Permission[]): Record<string, Permission[]> {
return permissions.reduce<Record<string, Permission[]>>((acc, p) => {
(acc[p.module || "أخرى"] ??= []).push(p);
return acc;
}, {});
}
// ── Permission Matrix ──────────────────────────────────────────────────────
function PermissionMatrix({
role,
allPermissions,
onSave,
}: {
role: Role;
allPermissions: Permission[];
onSave: (roleId: string, permIds: string[]) => Promise<boolean>;
}) {
const currentIds = role.permissions?.map(rp => rp.permission.id) ?? [];
const [selected, setSelected] = useState<Set<string>>(new Set(currentIds));
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const grouped = groupByModule(allPermissions);
const toggle = (id: string) => {
setSelected(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
setSaved(false);
};
const toggleModule = (mod: string) => {
const ids = (grouped[mod] ?? []).map(p => p.id);
const allChecked = ids.every(id => selected.has(id));
setSelected(prev => {
const next = new Set(prev);
allChecked ? ids.forEach(id => next.delete(id)) : ids.forEach(id => next.add(id));
return next;
});
setSaved(false);
};
const handleSave = async () => {
setSaving(true);
const ok = await onSave(role.id, [...selected]);
setSaving(false);
if (ok) setSaved(true);
};
return (
<div style={{
borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)",
background: "var(--color-surface)", boxShadow: "var(--shadow-card)", overflow: "hidden",
}}>
{/* Role header */}
<div style={{
padding: "1rem 1.5rem", borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
display: "flex", alignItems: "center", justifyContent: "space-between",
}}>
<div>
<p style={{ fontSize: 15, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>{role.name}</p>
{role.description && (
<p style={{ fontSize: 12, color: "var(--color-text-muted)", marginTop: 2 }}>{role.description}</p>
)}
<p style={{ fontSize: 11, color: "var(--color-text-hint)", marginTop: 4 }}>
{selected.size} من {allPermissions.length} صلاحية مفعّلة
</p>
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
{saved && (
<span style={{ fontSize: 12, color: "#166534", fontWeight: 600 }}> تم الحفظ</span>
)}
<button type="button" onClick={handleSave} disabled={saving}
style={{
height: 36, padding: "0 1.25rem", 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 ? "جارٍ الحفظ…" : "حفظ"}
</button>
</div>
</div>
{/* Module rows */}
<div style={{ padding: "0.75rem" }}>
{Object.entries(grouped).map(([mod, perms]) => {
const allChecked = perms.every(p => selected.has(p.id));
const someChecked = !allChecked && perms.some(p => selected.has(p.id));
return (
<div key={mod} style={{ marginBottom: "0.5rem" }}>
{/* Module header */}
<div style={{
display: "flex", alignItems: "center", gap: 10,
padding: "0.5rem 0.75rem",
background: "var(--color-surface-muted)",
borderRadius: "var(--radius-md)",
marginBottom: "0.375rem",
}}>
<input type="checkbox" checked={allChecked}
ref={el => { if (el) el.indeterminate = someChecked; }}
onChange={() => toggleModule(mod)}
style={{ width: 15, height: 15, cursor: "pointer", accentColor: "#2563EB" }} />
<span style={{ fontSize: 12, fontWeight: 700, color: "var(--color-text-primary)" }}>
{moduleLabel(mod)}
</span>
<span style={{ fontSize: 10, color: "var(--color-text-muted)", marginRight: "auto" }}>
{perms.filter(p => selected.has(p.id)).length}/{perms.length}
</span>
</div>
{/* Permissions */}
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.375rem", paddingRight: "1.5rem" }}>
{perms.map(perm => {
const checked = selected.has(perm.id);
return (
<label key={perm.id} style={{
display: "inline-flex", alignItems: "center", gap: 6,
padding: "0.3rem 0.625rem",
borderRadius: "var(--radius-md)",
border: `1px solid ${checked ? "#BFDBFE" : "var(--color-border)"}`,
background: checked ? "#EFF6FF" : "var(--color-surface-muted)",
cursor: "pointer", userSelect: "none",
}}>
<input type="checkbox" checked={checked} onChange={() => toggle(perm.id)}
style={{ width: 13, height: 13, cursor: "pointer", accentColor: "#2563EB" }} />
<span style={{ fontSize: 11, fontWeight: 600, color: checked ? "#1D4ED8" : "var(--color-text-primary)" }}>
{perm.name}
</span>
</label>
);
})}
</div>
</div>
);
})}
</div>
</div>
);
}
// ── Page ───────────────────────────────────────────────────────────────────
export default function RolePermissionPage() {
const { roles, loading, error, permissions, clearError, updateRolePermissionsBulk, notification } = useRoles();
const [selectedRoleId, setSelectedRoleId] = useState<string>("");
const selectedRole = roles.find(r => r.id === selectedRoleId) ?? null;
const handleSave = useCallback(async (roleId: string, permIds: string[]): Promise<boolean> => {
return updateRolePermissionsBulk(roleId, permIds);
}, [updateRolePermissionsBulk]);
return (
<>
<RoleToast notification={notification} />
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* Header */}
<header style={{ borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", background: "var(--color-surface)", padding: "1.5rem 2rem", boxShadow: "var(--shadow-card)" }}>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600 }}>
إدارة الصلاحيات
</p>
<div className="mt-3 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
مصفوفة الصلاحيات
</h1>
<p style={{ marginTop: "0.25rem", fontSize: 13, color: "var(--color-text-muted)" }}>
إدارة صلاحيات كل دور بشكل مجمَّع
</p>
</div>
<div style={{ minWidth: 240 }}>
<select value={selectedRoleId} onChange={e => setSelectedRoleId(e.target.value)} dir="rtl"
style={{ width: "100%", height: 40, padding: "0 0.75rem", borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, color: "var(--color-text-primary)", outline: "none", fontFamily: "var(--font-sans)" }}>
<option value="">اختر دوراً لتعديل صلاحياته</option>
{roles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
</select>
</div>
</div>
</header>
{error && <Alert type="error" message={error} onClose={clearError} />}
{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>
) : !selectedRole ? (
<div style={{
borderRadius: "var(--radius-xl)", border: "2px dashed var(--color-border)",
background: "var(--color-surface)", padding: "4rem 2rem", textAlign: "center",
}}>
<p style={{ fontSize: 32, margin: 0 }}>🔐</p>
<p style={{ marginTop: 12, fontSize: 15, fontWeight: 700, color: "var(--color-text-primary)" }}>
اختر دوراً من القائمة أعلاه
</p>
<p style={{ marginTop: 4, fontSize: 13, color: "var(--color-text-muted)" }}>
ستظهر هنا صلاحيات الدور المحدد وبإمكانك تعديلها مباشرة.
</p>
</div>
) : (
<PermissionMatrix
key={selectedRole.id}
role={selectedRole}
allPermissions={permissions}
onSave={handleSave}
/>
)}
</section>
</>
);
}