add assign permission to role pages
This commit is contained in:
@@ -54,7 +54,6 @@ const navSections = [
|
||||
{ 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/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" },
|
||||
],
|
||||
},
|
||||
@@ -181,12 +180,12 @@ export function Sidebar() {
|
||||
padding: "20px 12px",
|
||||
zIndex: 50,
|
||||
overflowY: "auto",
|
||||
/* transform بيخفي/يظهر الـ drawer على sm */
|
||||
|
||||
transform: open ? "translateX(0)" : "translateX(100%)",
|
||||
transition: "transform 280ms cubic-bezier(0.4,0,0.2,1)",
|
||||
}}
|
||||
>
|
||||
{/* زرار الإغلاق */}
|
||||
{/*close button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function page() {
|
||||
return (
|
||||
<div>page</div>
|
||||
)
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export default function RolesPage() {
|
||||
page, search,
|
||||
setPage, handleSearch, clearError,
|
||||
createRole, updateRole, deleteRole,
|
||||
assignPermission, removePermission,
|
||||
notification,
|
||||
} = useRoles();
|
||||
|
||||
@@ -54,7 +55,10 @@ export default function RolesPage() {
|
||||
{viewRoleId && (
|
||||
<RoleDetailModal
|
||||
roleId={viewRoleId}
|
||||
permissions={permissions}
|
||||
onClose={() => setViewRoleId(null)}
|
||||
onAssign={assignPermission}
|
||||
onRemove={removePermission}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "logistic_client",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.4.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@tabler/icons-webfont": "^3.44.0",
|
||||
"next": "16.2.7",
|
||||
"react": "19.2.4",
|
||||
@@ -460,15 +460,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz",
|
||||
"integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==",
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",
|
||||
"integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.55.0"
|
||||
"react-hook-form": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
@@ -1344,12 +1341,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.4.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@tabler/icons-webfont": "^3.44.0",
|
||||
"next": "16.2.7",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -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;
|
||||
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,32 +198,78 @@ 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})
|
||||
الصلاحيات ({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>
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
import { useCallback, useEffect, useReducer, useState } from "react";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import { roleService } from "@/src/services/role.service";
|
||||
import { parseApiError } from "@/src/lib/apiError";
|
||||
import { Notification } from "../types/notif";
|
||||
import { Permission, Role, RoleFormData } from "../types/role";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ── Table state / reducer ─────────────────────────────────────────────────────
|
||||
|
||||
interface TableState {
|
||||
@@ -119,7 +116,6 @@ export function useRoles() {
|
||||
roleService
|
||||
.getPermissions(token)
|
||||
.then((res) => {
|
||||
//console.log("permissions response:", JSON.stringify(res, null, 2));
|
||||
const raw = (
|
||||
res as unknown as { data: { premissions: { data: Permission[] } } }
|
||||
).data;
|
||||
@@ -142,7 +138,13 @@ export function useRoles() {
|
||||
const res = await roleService.create(data, token);
|
||||
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);
|
||||
notify({
|
||||
type: "success",
|
||||
@@ -152,7 +154,7 @@ export function useRoles() {
|
||||
} catch (err) {
|
||||
notify({
|
||||
type: "error",
|
||||
message: err instanceof Error ? err.message : "تعذّر إنشاء الدور.",
|
||||
message: parseApiError(err, "تعذّر إنشاء الدور."),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -164,21 +166,17 @@ export function useRoles() {
|
||||
async (
|
||||
id: string,
|
||||
data: RoleFormData,
|
||||
currentPermIds: string[],
|
||||
_currentPermIds: string[],
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
// Update basic fields
|
||||
await roleService.update(id, data, token);
|
||||
|
||||
// Diff permissions: add new, remove removed
|
||||
const toAdd = data.permissionIds.filter(
|
||||
(p) => !currentPermIds.includes(p),
|
||||
);
|
||||
const toRemove = currentPermIds.filter(
|
||||
(p) => !data.permissionIds.includes(p),
|
||||
);
|
||||
|
||||
// Endpoint 2: replace the full permission set in one call rather
|
||||
// than diffing add/remove client-side — the bulk endpoint already
|
||||
// performs the replacement atomically on the backend.
|
||||
await roleService.bulkAssignPermissions(id, data.permissionIds, token);
|
||||
|
||||
await loadRoles(page, search);
|
||||
notify({
|
||||
@@ -189,7 +187,7 @@ export function useRoles() {
|
||||
} catch (err) {
|
||||
notify({
|
||||
type: "error",
|
||||
message: err instanceof Error ? err.message : "تعذّر تحديث الدور.",
|
||||
message: parseApiError(err, "تعذّر تحديث الدور."),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -208,7 +206,7 @@ export function useRoles() {
|
||||
} catch (err) {
|
||||
notify({
|
||||
type: "error",
|
||||
message: err instanceof Error ? err.message : "تعذّر حذف الدور.",
|
||||
message: parseApiError(err, "تعذّر حذف الدور."),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -216,6 +214,43 @@ export function useRoles() {
|
||||
[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) => {
|
||||
setSearch(q);
|
||||
@@ -233,7 +268,8 @@ export function useRoles() {
|
||||
createRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
assignPermission,
|
||||
removePermission,
|
||||
notification,
|
||||
|
||||
};
|
||||
}
|
||||
27
src/lib/apiError.ts
Normal file
27
src/lib/apiError.ts
Normal 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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** Build query string for paginated role listing */
|
||||
function buildRolesQuery(page: number, search: string): string {
|
||||
return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
|
||||
@@ -26,12 +27,10 @@ export const roleService = {
|
||||
create: (data: RoleFormData, token: string | null) =>
|
||||
post<RoleResponse>("role", { name: data.name, description: data.description }, token),
|
||||
|
||||
|
||||
/** Update role name/description */
|
||||
update: (id: string, data: Partial<RoleFormData>, token: string | null) =>
|
||||
put<RoleResponse>(`role/${id}`, { name: data.name, description: data.description }, token),
|
||||
|
||||
|
||||
/** Soft-delete a role */
|
||||
delete: (id: string, token: string | null) =>
|
||||
del<void>(`role/${id}`, token),
|
||||
@@ -40,5 +39,15 @@ export const roleService = {
|
||||
getPermissions: (token: string | null) =>
|
||||
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),
|
||||
};
|
||||
@@ -33,7 +33,6 @@ export interface PermissionsResponse {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//permissions
|
||||
export interface Permission {
|
||||
id: string;
|
||||
@@ -41,3 +40,41 @@ export interface Permission {
|
||||
slug: 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[];
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
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({
|
||||
name: yup
|
||||
.string()
|
||||
@@ -26,3 +29,34 @@ export const updateRoleSchema = yup.object({
|
||||
});
|
||||
|
||||
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>>;
|
||||
Reference in New Issue
Block a user