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

@@ -54,7 +54,6 @@ const navSections = [
{ href: "/dashboard/clients", label: "Clients", icon: "ti-users-group", permission: "read-client" }, { href: "/dashboard/clients", label: "Clients", icon: "ti-users-group", permission: "read-client" },
{ href: "/dashboard/branches", label: "Branches", icon: "ti-building", permission: "read-branch" }, { href: "/dashboard/branches", label: "Branches", icon: "ti-building", permission: "read-branch" },
{ href: "/dashboard/roles", label: "Roles", icon: "ti-shield", permission: "read-role" }, { href: "/dashboard/roles", label: "Roles", icon: "ti-shield", permission: "read-role" },
{ href: "/dashboard/role_permission", label: "Roles & Permission", icon: "ti-shield", permission: "manage-role-permissions" },
{ href: "/dashboard/audit", label: "Audit", icon: "ti-clipboard-list", permission: "read-audit" }, { href: "/dashboard/audit", label: "Audit", icon: "ti-clipboard-list", permission: "read-audit" },
], ],
}, },
@@ -181,12 +180,12 @@ export function Sidebar() {
padding: "20px 12px", padding: "20px 12px",
zIndex: 50, zIndex: 50,
overflowY: "auto", overflowY: "auto",
/* transform بيخفي/يظهر الـ drawer على sm */
transform: open ? "translateX(0)" : "translateX(100%)", transform: open ? "translateX(0)" : "translateX(100%)",
transition: "transform 280ms cubic-bezier(0.4,0,0.2,1)", transition: "transform 280ms cubic-bezier(0.4,0,0.2,1)",
}} }}
> >
{/* زرار الإغلاق */} {/*close button */}
<button <button
type="button" type="button"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}

View File

@@ -1,7 +0,0 @@
import React from 'react'
export default function page() {
return (
<div>page</div>
)
}

View File

@@ -25,6 +25,7 @@ export default function RolesPage() {
page, search, page, search,
setPage, handleSearch, clearError, setPage, handleSearch, clearError,
createRole, updateRole, deleteRole, createRole, updateRole, deleteRole,
assignPermission, removePermission,
notification, notification,
} = useRoles(); } = useRoles();
@@ -54,7 +55,10 @@ export default function RolesPage() {
{viewRoleId && ( {viewRoleId && (
<RoleDetailModal <RoleDetailModal
roleId={viewRoleId} roleId={viewRoleId}
permissions={permissions}
onClose={() => setViewRoleId(null)} onClose={() => setViewRoleId(null)}
onAssign={assignPermission}
onRemove={removePermission}
/> />
)} )}

19
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "logistic_client", "name": "logistic_client",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.4.0", "@hookform/resolvers": "^3.9.1",
"@tabler/icons-webfont": "^3.44.0", "@tabler/icons-webfont": "^3.44.0",
"next": "16.2.7", "next": "16.2.7",
"react": "19.2.4", "react": "19.2.4",
@@ -460,15 +460,12 @@
} }
}, },
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "5.4.0", "version": "3.9.1",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",
"integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==", "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==",
"license": "MIT", "license": "MIT",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
},
"peerDependencies": { "peerDependencies": {
"react-hook-form": "^7.55.0" "react-hook-form": "^7.0.0"
} }
}, },
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
@@ -1344,12 +1341,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",

View File

@@ -9,7 +9,7 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.4.0", "@hookform/resolvers": "^3.9.1",
"@tabler/icons-webfont": "^3.44.0", "@tabler/icons-webfont": "^3.44.0",
"next": "16.2.7", "next": "16.2.7",
"react": "19.2.4", "react": "19.2.4",

View File

@@ -4,12 +4,14 @@ import { useEffect, useState } from "react";
import { Spinner } from "../UI"; import { Spinner } from "../UI";
import { getStoredToken } from "@/src/lib/auth"; import { getStoredToken } from "@/src/lib/auth";
import { roleService } from "@/src/services/role.service"; import { roleService } from "@/src/services/role.service";
import { Role } from "@/src/types/role"; import { Permission, Role } from "@/src/types/role";
interface RoleDetailModalProps { interface RoleDetailModalProps {
roleId: string; roleId: string;
permissions: Permission[]; // full catalog, for the "add permission" select
onClose: () => void; 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 }) { 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 [role, setRole] = useState<Role | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// ── Inline permission mutation state ─────────────────────────────────────────
const [pendingPermId, setPendingPermId] = useState("");
const [mutating, setMutating] = useState(false);
useEffect(() => { useEffect(() => {
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", handler); window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler);
}, [onClose]); }, [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(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
(async () => { (async () => {
@@ -71,9 +89,34 @@ export function RoleDetailModal({ roleId, onClose }: RoleDetailModalProps) {
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [roleId]); }, [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) => const fmt = (iso?: string | null) =>
iso ? new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric" }) : 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 ( return (
<div <div
role="dialog" aria-modal="true" aria-labelledby="role-detail-title" role="dialog" aria-modal="true" aria-labelledby="role-detail-title"
@@ -155,32 +198,78 @@ export function RoleDetailModal({ roleId, onClose }: RoleDetailModalProps) {
{role.updatedAt && <DetailRow label="آخر تحديث" value={fmt(role.updatedAt)} />} {role.updatedAt && <DetailRow label="آخر تحديث" value={fmt(role.updatedAt)} />}
{/* Permissions list */} {/* Permissions list */}
{role.permissions && role.permissions.length > 0 && (
<div style={{ paddingTop: "0.75rem" }}> <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" }}> <p style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.15em", color: "var(--color-text-muted)", margin: "0 0 10px" }}>
الصلاحيات ({role.permissions.length}) الصلاحيات ({role.permissions?.length ?? 0})
</p> </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" }}> <div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem" }}>
{role.permissions.map(({ permission }) => ( {role.permissions.map(({ permission }) => (
<span key={permission.id} style={{ <span key={permission.id} style={{
display: "inline-flex", alignItems: "center", display: "inline-flex", alignItems: "center", gap: 6,
padding: "0.25rem 0.6rem", borderRadius: "var(--radius-md)", padding: "0.25rem 0.4rem 0.25rem 0.6rem", borderRadius: "var(--radius-md)",
background: "#EFF6FF", border: "1px solid #BFDBFE", background: "#EFF6FF", border: "1px solid #BFDBFE",
fontSize: 11, fontWeight: 600, color: "#1D4ED8", fontSize: 11, fontWeight: 600, color: "#1D4ED8",
}}> }}>
{permission.name} {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> </span>
))} ))}
</div> </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> <p style={{ fontSize: 13, color: "var(--color-text-hint)", fontStyle: "italic" }}>لا توجد صلاحيات مسندة لهذا الدور</p>
</div>
)} )}
</div> </div>
</div>
)} )}
</div> </div>

View File

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

View File

@@ -3,13 +3,10 @@
import { useCallback, useEffect, useReducer, useState } from "react"; import { useCallback, useEffect, useReducer, useState } from "react";
import { getStoredToken } from "@/src/lib/auth"; import { getStoredToken } from "@/src/lib/auth";
import { roleService } from "@/src/services/role.service"; import { roleService } from "@/src/services/role.service";
import { parseApiError } from "@/src/lib/apiError";
import { Notification } from "../types/notif"; import { Notification } from "../types/notif";
import { Permission, Role, RoleFormData } from "../types/role"; import { Permission, Role, RoleFormData } from "../types/role";
// ── Table state / reducer ───────────────────────────────────────────────────── // ── Table state / reducer ─────────────────────────────────────────────────────
interface TableState { interface TableState {
@@ -119,7 +116,6 @@ export function useRoles() {
roleService roleService
.getPermissions(token) .getPermissions(token)
.then((res) => { .then((res) => {
//console.log("permissions response:", JSON.stringify(res, null, 2));
const raw = ( const raw = (
res as unknown as { data: { premissions: { data: Permission[] } } } res as unknown as { data: { premissions: { data: Permission[] } } }
).data; ).data;
@@ -142,7 +138,13 @@ export function useRoles() {
const res = await roleService.create(data, token); const res = await roleService.create(data, token);
const role = (res as unknown as { data: Role }).data; const role = (res as unknown as { data: Role }).data;
// Re-fetch to get fresh data with permissions attached // Endpoint 2 (PATCH /role/{id}/permissions/bulk): attach selected
// permissions right after creation, since POST /role only accepts
// name/description.
if (data.permissionIds.length) {
await roleService.bulkAssignPermissions(role.id, data.permissionIds, token);
}
await loadRoles(page, search); await loadRoles(page, search);
notify({ notify({
type: "success", type: "success",
@@ -152,7 +154,7 @@ export function useRoles() {
} catch (err) { } catch (err) {
notify({ notify({
type: "error", type: "error",
message: err instanceof Error ? err.message : "تعذّر إنشاء الدور.", message: parseApiError(err, "تعذّر إنشاء الدور."),
}); });
return false; return false;
} }
@@ -164,21 +166,17 @@ export function useRoles() {
async ( async (
id: string, id: string,
data: RoleFormData, data: RoleFormData,
currentPermIds: string[], _currentPermIds: string[],
): Promise<boolean> => { ): Promise<boolean> => {
try { try {
const token = getStoredToken(); const token = getStoredToken();
// Update basic fields // Update basic fields
await roleService.update(id, data, token); await roleService.update(id, data, token);
// Diff permissions: add new, remove removed // Endpoint 2: replace the full permission set in one call rather
const toAdd = data.permissionIds.filter( // than diffing add/remove client-side — the bulk endpoint already
(p) => !currentPermIds.includes(p), // performs the replacement atomically on the backend.
); await roleService.bulkAssignPermissions(id, data.permissionIds, token);
const toRemove = currentPermIds.filter(
(p) => !data.permissionIds.includes(p),
);
await loadRoles(page, search); await loadRoles(page, search);
notify({ notify({
@@ -189,7 +187,7 @@ export function useRoles() {
} catch (err) { } catch (err) {
notify({ notify({
type: "error", type: "error",
message: err instanceof Error ? err.message : "تعذّر تحديث الدور.", message: parseApiError(err, "تعذّر تحديث الدور."),
}); });
return false; return false;
} }
@@ -208,7 +206,7 @@ export function useRoles() {
} catch (err) { } catch (err) {
notify({ notify({
type: "error", type: "error",
message: err instanceof Error ? err.message : "تعذّر حذف الدور.", message: parseApiError(err, "تعذّر حذف الدور."),
}); });
return false; return false;
} }
@@ -216,6 +214,43 @@ export function useRoles() {
[notify], [notify],
); );
// ── Endpoint 1: assign a single permission (used from the detail modal) ─────
const assignPermission = useCallback(
async (roleId: string, permissionId: string): Promise<boolean> => {
try {
const token = getStoredToken();
await roleService.assignPermission(roleId, permissionId, token);
notify({ type: "success", message: "تم إضافة الصلاحية للدور بنجاح." });
return true;
} catch (err) {
notify({
type: "error",
message: parseApiError(err, "تعذّر إضافة الصلاحية."),
});
return false;
}
},
[notify],
);
// ── Endpoint 3: remove a single permission (used from the detail modal) ─────
const removePermission = useCallback(
async (roleId: string, permissionId: string): Promise<boolean> => {
try {
const token = getStoredToken();
await roleService.removePermission(roleId, permissionId, token);
notify({ type: "success", message: "تم إزالة الصلاحية من الدور بنجاح." });
return true;
} catch (err) {
notify({
type: "error",
message: parseApiError(err, "تعذّر إزالة الصلاحية."),
});
return false;
}
},
[notify],
);
const handleSearch = useCallback((q: string) => { const handleSearch = useCallback((q: string) => {
setSearch(q); setSearch(q);
@@ -233,7 +268,8 @@ export function useRoles() {
createRole, createRole,
updateRole, updateRole,
deleteRole, deleteRole,
assignPermission,
removePermission,
notification, notification,
}; };
} }

27
src/lib/apiError.ts Normal file
View File

@@ -0,0 +1,27 @@
import { ApiErrorResponse } from "@/src/types/role";
/**
* Extracts a human-readable Arabic message from a backend error payload
* shaped like ApiErrorResponse (validation_failed or other error codes).
* Falls back to a generic message when the shape doesn't match.
*/
export function parseApiError(err: unknown, fallback: string): string {
const body = (err as { body?: unknown })?.body ?? err;
if (
body &&
typeof body === "object" &&
"error" in body &&
(body as ApiErrorResponse).error?.details?.length
) {
const { details } = (body as ApiErrorResponse).error;
// Surface the first field-level validation message
return details.map((d) => `${d.field}: ${d.code}`).join(" — ");
}
if (body && typeof body === "object" && "message" in body) {
return String((body as { message: string }).message);
}
return err instanceof Error ? err.message : fallback;
}

View File

@@ -1,13 +1,14 @@
import { ApiPaginatedResponse, PermissionsResponse, Role, RoleFormData, RoleResponse } from "../types/role"; import {
ApiPaginatedResponse,
AssignPermissionResponse,
BulkAssignPermissionsResponse,
PermissionsResponse,
Role,
RoleFormData,
RoleResponse,
} from "../types/role";
import { get, post, put, del, patch } from "./api"; import { get, post, put, del, patch } from "./api";
/** Build query string for paginated role listing */ /** Build query string for paginated role listing */
function buildRolesQuery(page: number, search: string): string { function buildRolesQuery(page: number, search: string): string {
return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`; return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
@@ -26,12 +27,10 @@ export const roleService = {
create: (data: RoleFormData, token: string | null) => create: (data: RoleFormData, token: string | null) =>
post<RoleResponse>("role", { name: data.name, description: data.description }, token), post<RoleResponse>("role", { name: data.name, description: data.description }, token),
/** Update role name/description */ /** Update role name/description */
update: (id: string, data: Partial<RoleFormData>, token: string | null) => update: (id: string, data: Partial<RoleFormData>, token: string | null) =>
put<RoleResponse>(`role/${id}`, { name: data.name, description: data.description }, token), put<RoleResponse>(`role/${id}`, { name: data.name, description: data.description }, token),
/** Soft-delete a role */ /** Soft-delete a role */
delete: (id: string, token: string | null) => delete: (id: string, token: string | null) =>
del<void>(`role/${id}`, token), del<void>(`role/${id}`, token),
@@ -40,5 +39,15 @@ export const roleService = {
getPermissions: (token: string | null) => getPermissions: (token: string | null) =>
get<PermissionsResponse>("premission?limit=200", token), get<PermissionsResponse>("premission?limit=200", token),
/** POST /role/{id}/permissions — assign a single permission to a role */
assignPermission: (roleId: string, permissionId: string, token: string | null) =>
post<AssignPermissionResponse>(`role-permissions/${roleId}/permissions`, { permissionId }, token),
/** PATCH /role/{id}/permissions/bulk — replace all permissions for a role */
bulkAssignPermissions: (roleId: string, permissionIds: string[], token: string | null) =>
patch<BulkAssignPermissionsResponse>(`role-permissions/${roleId}/permissions`, { permissionIds }, token),
/** DELETE /role/{id}/permissions/{permissionId} — remove a single permission */
removePermission: (roleId: string, permissionId: string, token: string | null) =>
del<void>(`role-permissions/${roleId}/permissions/${permissionId}`, token),
}; };

View File

@@ -33,7 +33,6 @@ export interface PermissionsResponse {
}; };
} }
//permissions //permissions
export interface Permission { export interface Permission {
id: string; id: string;
@@ -41,3 +40,41 @@ export interface Permission {
slug: string; slug: string;
module: string; module: string;
} }
// ── Permission mutation responses ───────────────────────────────────────────
export interface RolePermissionLink {
id: string;
roleId: string;
permissionId: string;
}
export interface AssignPermissionResponse {
success: true;
message: string;
responseAt: string;
data: RolePermissionLink;
}
export interface BulkAssignPermissionsResponse {
success: true;
message: string;
responseAt: string;
data: Role;
}
export interface ApiErrorDetail {
field: string;
code: string;
}
export interface ApiErrorResponse {
success: false;
message: string;
responseAt: string;
error: {
code: string;
path: string;
details: ApiErrorDetail[];
};
}

View File

@@ -1,5 +1,8 @@
import * as yup from "yup"; import * as yup from "yup";
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export const createRoleSchema = yup.object({ export const createRoleSchema = yup.object({
name: yup name: yup
.string() .string()
@@ -26,3 +29,34 @@ export const updateRoleSchema = yup.object({
}); });
export type RoleFormErrors = Partial<Record<"name" | "description", string>>; export type RoleFormErrors = Partial<Record<"name" | "description", string>>;
// ── Mirrors backend rolePremisson.validators.js ────────────────────────────
const UUID_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** POST /role/{id}/permissions */
export const assignPermissionSchema = yup.object({
permissionId: yup
.string()
.required("Permission ID is required")
.matches(UUID_REGEX, "Invalid Permission ID format"),
});
/** PATCH /role/{id}/permissions/bulk */
export const bulkAssignPermissionSchema = yup.object({
permissionIds: yup
.array(
yup
.string()
.matches(UUID_REGEX, "Each permission ID must be a valid UUID")
.required()
)
.test("unique", "Permission IDs must be unique", function (arr) {
if (!arr) return true;
return new Set(arr).size === arr.length;
})
.required(),
});
export type PermissionFormErrors = Partial<Record<"permissionId" | "permissionIds", string>>;