add assign permission to role pages

This commit is contained in:
m7amedez5511
2026-06-30 14:17:50 +03:00
parent eaae6810bd
commit f5882f3791
12 changed files with 304 additions and 85 deletions

View File

@@ -4,12 +4,14 @@ import { useEffect, useState } from "react";
import { Spinner } from "../UI";
import { getStoredToken } from "@/src/lib/auth";
import { roleService } from "@/src/services/role.service";
import { Role } from "@/src/types/role";
import { Permission, Role } from "@/src/types/role";
interface RoleDetailModalProps {
roleId: string;
onClose: () => void;
roleId: string;
permissions: Permission[]; // full catalog, for the "add permission" select
onClose: () => void;
onAssign: (roleId: string, permissionId: string) => Promise<boolean>;
onRemove: (roleId: string, permissionId: string) => Promise<boolean>;
}
function DetailRow({ label, value }: { label: string; value?: string | null }) {
@@ -44,17 +46,33 @@ function StatusBadge({ active }: { active: boolean }) {
);
}
export function RoleDetailModal({ roleId, onClose }: RoleDetailModalProps) {
export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemove }: RoleDetailModalProps) {
const [role, setRole] = useState<Role | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// ── Inline permission mutation state ─────────────────────────────────────────
const [pendingPermId, setPendingPermId] = useState("");
const [mutating, setMutating] = useState(false);
useEffect(() => {
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [onClose]);
const loadRole = async () => {
try {
const token = getStoredToken();
const res = await roleService.getById(roleId, token);
setRole((res as unknown as { data: Role }).data);
} catch {
setError("تعذّر تحميل بيانات الدور. يرجى المحاولة لاحقاً.");
} finally {
setLoading(false);
}
};
useEffect(() => {
let cancelled = false;
(async () => {
@@ -71,9 +89,34 @@ export function RoleDetailModal({ roleId, onClose }: RoleDetailModalProps) {
return () => { cancelled = true; };
}, [roleId]);
// Endpoint 1: POST /role/{id}/permissions
const handleAssign = async () => {
if (!pendingPermId || mutating) return;
setMutating(true);
const ok = await onAssign(roleId, pendingPermId);
if (ok) {
setPendingPermId("");
await loadRole();
}
setMutating(false);
};
// Endpoint 3: DELETE /role/{id}/permissions/{permissionId}
const handleRemove = async (permissionId: string) => {
if (mutating) return;
setMutating(true);
const ok = await onRemove(roleId, permissionId);
if (ok) await loadRole();
setMutating(false);
};
const fmt = (iso?: string | null) =>
iso ? new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric" }) : null;
const assignablePermissions = permissions.filter(
(p) => !role?.permissions?.some((rp) => rp.permission.id === p.id),
);
return (
<div
role="dialog" aria-modal="true" aria-labelledby="role-detail-title"
@@ -155,31 +198,77 @@ export function RoleDetailModal({ roleId, onClose }: RoleDetailModalProps) {
{role.updatedAt && <DetailRow label="آخر تحديث" value={fmt(role.updatedAt)} />}
{/* Permissions list */}
{role.permissions && role.permissions.length > 0 && (
<div style={{ paddingTop: "0.75rem" }}>
<p style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.15em", color: "var(--color-text-muted)", margin: "0 0 10px" }}>
الصلاحيات ({role.permissions.length})
</p>
<div style={{ paddingTop: "0.75rem" }}>
<p style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.15em", color: "var(--color-text-muted)", margin: "0 0 10px" }}>
الصلاحيات ({role.permissions?.length ?? 0})
</p>
{/* Assign new permission — Endpoint 1 */}
{assignablePermissions.length > 0 && (
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<select
value={pendingPermId}
onChange={(e) => setPendingPermId(e.target.value)}
disabled={mutating}
style={{
flex: 1, height: 36, borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)", fontSize: 12,
padding: "0 0.5rem", fontFamily: "var(--font-sans)",
color: "var(--color-text-primary)", background: "var(--color-surface)",
}}
>
<option value="">اختر صلاحية لإضافتها</option>
{assignablePermissions.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
<button
type="button" onClick={handleAssign} disabled={!pendingPermId || mutating}
style={{
height: 36, padding: "0 0.875rem", borderRadius: "var(--radius-md)",
border: "none", background: "var(--color-brand-600)", color: "#FFF",
fontSize: 12, fontWeight: 700,
cursor: pendingPermId && !mutating ? "pointer" : "not-allowed",
opacity: !pendingPermId || mutating ? 0.6 : 1,
fontFamily: "var(--font-sans)",
display: "flex", alignItems: "center", gap: 6,
}}
>
{mutating && <Spinner size="sm" className="text-white" />}
إضافة
</button>
</div>
)}
{role.permissions && role.permissions.length > 0 ? (
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem" }}>
{role.permissions.map(({ permission }) => (
<span key={permission.id} style={{
display: "inline-flex", alignItems: "center",
padding: "0.25rem 0.6rem", borderRadius: "var(--radius-md)",
display: "inline-flex", alignItems: "center", gap: 6,
padding: "0.25rem 0.4rem 0.25rem 0.6rem", borderRadius: "var(--radius-md)",
background: "#EFF6FF", border: "1px solid #BFDBFE",
fontSize: 11, fontWeight: 600, color: "#1D4ED8",
}}>
{permission.name}
{/* Endpoint 3: remove this permission */}
<button
type="button" onClick={() => handleRemove(permission.id)} disabled={mutating}
aria-label={`إزالة ${permission.name}`}
style={{
background: "none", border: "none",
cursor: mutating ? "not-allowed" : "pointer",
color: "#1D4ED8", fontSize: 13, lineHeight: 1, padding: 0,
}}
>
×
</button>
</span>
))}
</div>
</div>
)}
{(!role.permissions || role.permissions.length === 0) && (
<div style={{ paddingTop: "0.75rem" }}>
) : (
<p style={{ fontSize: 13, color: "var(--color-text-hint)", fontStyle: "italic" }}>لا توجد صلاحيات مسندة لهذا الدور</p>
</div>
)}
)}
</div>
</div>
)}
</div>

View File

@@ -43,7 +43,7 @@ async function validate(data: RoleFormData, isNew: boolean): Promise<RoleFormErr
}
// ── Permission checkbox group ─────────────────────────────────────────────────
/*function PermissionGroup({
function PermissionGroup({
module, perms, selected, onToggle,
}: {
module: string; perms: Permission[]; selected: string[]; onToggle: (id: string) => void;
@@ -77,7 +77,7 @@ async function validate(data: RoleFormData, isNew: boolean): Promise<RoleFormErr
</div>
</div>
);
}*/
}
// ── Props ─────────────────────────────────────────────────────────────────────
interface RoleFormModalProps {
@@ -225,7 +225,7 @@ export function RoleFormModal({ editRole, permissions, onClose, onSubmit }: Role
</label>
{/* Permissions */}
{/*{permissions.length > 0 && (
{permissions.length > 0 && (
<div dir="rtl">
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.75rem" }}>
<span style={{ fontSize: 12, fontWeight: 600, color: "var(--color-text-secondary)" }}>
@@ -258,7 +258,7 @@ export function RoleFormModal({ editRole, permissions, onClose, onSubmit }: Role
{form.permissionIds.length} صلاحية محددة من أصل {permissions.length}
</p>
</div>
)}*/}
)}
{/* Actions */}
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end", paddingTop: "0.5rem", flexShrink: 0 }}>