first action update user pages .

2-update driver pages [fixed image display and notyfi]
3-update car pages [fixed image]
4-build tripe pages crud
5-build some role pages
6-build audit page
7-update ui compounants
8-update saidebar and topbar
9-cheange view to be ar view
10-cheange stractcher to be app,src
11-add validation layer by yup
12-add api image-proxy to broke image corse bloken
This commit is contained in:
m7amedez5511
2026-06-23 15:54:07 +03:00
parent c1b77f89cd
commit db64b79fe3
140 changed files with 9266 additions and 2449 deletions

View File

@@ -0,0 +1,238 @@
"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>
</>
);
}