This commit is contained in:
@@ -57,15 +57,19 @@ interface Field {
|
||||
options?: { label: string; value: string | number }[];
|
||||
}
|
||||
|
||||
interface ApiError extends Error {
|
||||
response?: { data?: { message?: string } };
|
||||
}
|
||||
|
||||
interface GenericCrudTableProps<T> {
|
||||
title: string;
|
||||
description?: string;
|
||||
entityName: string;
|
||||
queryKey: any[];
|
||||
queryKey: string[];
|
||||
fetchFn: () => Promise<T[] | { data: T[] }>;
|
||||
createFn: (data: any) => Promise<any>;
|
||||
updateFn: (id: number, data: any) => Promise<any>;
|
||||
deleteFn: (id: number) => Promise<any>;
|
||||
createFn: (data: Record<string, unknown>) => Promise<unknown>;
|
||||
updateFn: (id: number, data: Record<string, unknown>) => Promise<unknown>;
|
||||
deleteFn: (id: number) => Promise<unknown>;
|
||||
columns: ColumnDef<T>[];
|
||||
fields: Field[];
|
||||
filters?: React.ReactNode;
|
||||
@@ -95,7 +99,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
});
|
||||
|
||||
// ADR-019: Support both direct array or wrapped data object
|
||||
const data: T[] = Array.isArray(rawData) ? rawData : (rawData as any)?.data || [];
|
||||
const data: T[] = Array.isArray(rawData) ? rawData : (rawData as { data?: T[] } | undefined)?.data || [];
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: createFn,
|
||||
@@ -105,13 +109,13 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
setIsDialogOpen(false);
|
||||
reset();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.message || `Failed to create ${entityName}`);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => updateFn(id, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: Record<string, unknown> }) => updateFn(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
toast.success(`${entityName} updated successfully`);
|
||||
@@ -119,7 +123,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
setEditingId(null);
|
||||
reset();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.message || `Failed to update ${entityName}`);
|
||||
},
|
||||
});
|
||||
@@ -131,7 +135,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
toast.success(`${entityName} deleted successfully`);
|
||||
setItemToDelete(null);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.message || `Failed to delete ${entityName}`);
|
||||
},
|
||||
});
|
||||
@@ -184,19 +188,20 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
setEditingId(item.id);
|
||||
reset(item);
|
||||
const handleEdit = (item: T) => {
|
||||
setEditingId(item.id as number);
|
||||
reset(item as Record<string, unknown>);
|
||||
// Ensure select values are strings for Shadcn Select
|
||||
fields.forEach(f => {
|
||||
if (f.type === 'select' && item[f.name]) {
|
||||
setValue(f.name, String(item[f.name]));
|
||||
const record = item as Record<string, unknown>;
|
||||
if (f.type === 'select' && record[f.name]) {
|
||||
setValue(f.name, String(record[f.name]));
|
||||
}
|
||||
});
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const onSubmit = (formData: any) => {
|
||||
const onSubmit = (formData: Record<string, unknown>) => {
|
||||
if (editingItem) {
|
||||
updateMutation.mutate({ id: editingItem, data: formData });
|
||||
} else {
|
||||
|
||||
@@ -35,13 +35,15 @@ interface RbacMatrixProps {
|
||||
}
|
||||
|
||||
const securityService = {
|
||||
getRoles: async () => {
|
||||
const response = await apiClient.get<any>("/users/roles");
|
||||
return response.data?.data || response.data;
|
||||
getRoles: async (): Promise<Role[]> => {
|
||||
const response = await apiClient.get("/users/roles");
|
||||
const data = response.data?.data || response.data;
|
||||
return Array.isArray(data) ? data : [];
|
||||
},
|
||||
getPermissions: async () => {
|
||||
const response = await apiClient.get<any>("/users/permissions");
|
||||
return response.data?.data || response.data;
|
||||
getPermissions: async (): Promise<Permission[]> => {
|
||||
const response = await apiClient.get("/users/permissions");
|
||||
const data = response.data?.data || response.data;
|
||||
return Array.isArray(data) ? data : [];
|
||||
},
|
||||
updateRolePermissions: async (roleId: number, permissionIds: number[]) => {
|
||||
// This endpoint might not exist as a bulk update, usually it's per role
|
||||
@@ -137,9 +139,9 @@ export function RbacMatrix() {
|
||||
<div>{perm.permissionName}</div>
|
||||
<div className="text-xs text-muted-foreground">{perm.description}</div>
|
||||
</TableCell>
|
||||
{roles.map((role: any) => {
|
||||
{roles.map((role) => {
|
||||
// Assume role.permissions is populated
|
||||
const currentRolePerms = role.permissions?.map((p: any) => p.permissionId) || [];
|
||||
const currentRolePerms = role.permissions?.map((p) => p.permissionId) || [];
|
||||
const activePerms = pendingChanges[role.roleId] || currentRolePerms;
|
||||
const isChecked = activePerms.includes(perm.permissionId);
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
isActive: user.isActive,
|
||||
lineId: user.lineId || "",
|
||||
primaryOrganizationId: user.primaryOrganizationId?.toString(),
|
||||
roleIds: user.roles?.map((r: any) => r.roleId) || [],
|
||||
roleIds: user.roles?.map((r: { roleId: number }) => r.roleId) || [],
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
});
|
||||
@@ -158,7 +158,17 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
// Create req: Password mandatory
|
||||
if (!payload.password) return; // Should allow Zod to catch or show error
|
||||
|
||||
createUser.mutate(payload as any, {
|
||||
createUser.mutate({
|
||||
username: payload.username,
|
||||
email: payload.email,
|
||||
firstName: payload.firstName,
|
||||
lastName: payload.lastName,
|
||||
password: payload.password,
|
||||
isActive: payload.isActive ?? true,
|
||||
lineId: payload.lineId,
|
||||
primaryOrganizationId: payload.primaryOrganizationId,
|
||||
roleIds: payload.roleIds ?? [],
|
||||
}, {
|
||||
onSuccess: () => onOpenChange(false),
|
||||
});
|
||||
}
|
||||
@@ -230,7 +240,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
<SelectValue placeholder="Select Organization" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{organizations?.map((org: any) => (
|
||||
{organizations?.map((org: { uuid: string; organizationCode: string; organizationName: string }) => (
|
||||
<SelectItem
|
||||
key={org.uuid}
|
||||
value={org.uuid}
|
||||
@@ -300,7 +310,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
<Label className="mb-3 block">Roles</Label>
|
||||
<div className="space-y-2 border p-3 rounded-md max-h-[200px] overflow-y-auto">
|
||||
{Array.isArray(roles) && roles.length === 0 && <p className="text-sm text-muted-foreground">Loading roles...</p>}
|
||||
{Array.isArray(roles) && roles.map((role: any) => (
|
||||
{Array.isArray(roles) && roles.map((role: { roleId: number; roleName: string; description?: string }) => (
|
||||
<div key={role.roleId} className="flex items-start space-x-2">
|
||||
<Checkbox
|
||||
id={`role-${role.roleId}`}
|
||||
|
||||
Reference in New Issue
Block a user