251209:1453 Frontend: progress nest = UAT & Bug Fixing
This commit is contained in:
249
frontend/components/admin/reference/generic-crud-table.tsx
Normal file
249
frontend/components/admin/reference/generic-crud-table.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { DataTable } from "@/components/common/data-table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Plus, Pencil, Trash2, RefreshCw } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface FieldConfig {
|
||||
name: string;
|
||||
label: string;
|
||||
type: "text" | "textarea" | "checkbox";
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface GenericCrudTableProps {
|
||||
entityName: string;
|
||||
queryKey: string[];
|
||||
fetchFn: () => Promise<any>;
|
||||
createFn: (data: any) => Promise<any>;
|
||||
updateFn: (id: number, data: any) => Promise<any>;
|
||||
deleteFn: (id: number) => Promise<any>;
|
||||
columns: ColumnDef<any>[];
|
||||
fields: FieldConfig[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function GenericCrudTable({
|
||||
entityName,
|
||||
queryKey,
|
||||
fetchFn,
|
||||
createFn,
|
||||
updateFn,
|
||||
deleteFn,
|
||||
columns,
|
||||
fields,
|
||||
title,
|
||||
description,
|
||||
}: GenericCrudTableProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey,
|
||||
queryFn: fetchFn,
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: createFn,
|
||||
onSuccess: () => {
|
||||
toast.success(`${entityName} created successfully`);
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
handleClose();
|
||||
},
|
||||
onError: () => toast.error(`Failed to create ${entityName}`),
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => updateFn(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success(`${entityName} updated successfully`);
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
handleClose();
|
||||
},
|
||||
onError: () => toast.error(`Failed to update ${entityName}`),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: deleteFn,
|
||||
onSuccess: () => {
|
||||
toast.success(`${entityName} deleted successfully`);
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
},
|
||||
onError: () => toast.error(`Failed to delete ${entityName}`),
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingItem(null);
|
||||
setFormData({});
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
setEditingItem(item);
|
||||
setFormData({ ...item });
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
if (confirm(`Are you sure you want to delete this ${entityName}?`)) {
|
||||
deleteMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
setEditingItem(null);
|
||||
setFormData({});
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (editingItem) {
|
||||
updateMutation.mutate({ id: editingItem.id, data: formData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field: string, value: any) => {
|
||||
setFormData((prev: any) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// Add default Actions column if not present
|
||||
const tableColumns = [
|
||||
...columns,
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }: { row: any }) => (
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive"
|
||||
onClick={() => handleDelete(row.original.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
{title && <h2 className="text-xl font-bold">{title}</h2>}
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add {entityName}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable columns={tableColumns} data={data || []} />
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingItem ? `Edit ${entityName}` : `New ${entityName}`}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{fields.map((field) => (
|
||||
<div key={field.name} className="space-y-2">
|
||||
<Label htmlFor={field.name}>{field.label}</Label>
|
||||
{field.type === "textarea" ? (
|
||||
<Textarea
|
||||
id={field.name}
|
||||
value={formData[field.name] || ""}
|
||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||
required={field.required}
|
||||
/>
|
||||
) : field.type === "checkbox" ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={field.name}
|
||||
checked={!!formData[field.name]}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(field.name, checked)
|
||||
}
|
||||
/>
|
||||
<label htmlFor={field.name} className="text-sm">
|
||||
Enabled
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
id={field.name}
|
||||
type="text"
|
||||
value={formData[field.name] || ""}
|
||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||
required={field.required}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
{editingItem ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
frontend/components/admin/security/rbac-matrix.tsx
Normal file
162
frontend/components/admin/security/rbac-matrix.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw, Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import apiClient from "@/lib/api/client";
|
||||
|
||||
interface Role {
|
||||
roleId: number;
|
||||
roleName: string;
|
||||
permissions?: Permission[];
|
||||
}
|
||||
|
||||
interface Permission {
|
||||
permissionId: number;
|
||||
permissionName: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface RbacMatrixProps {
|
||||
roles: Role[];
|
||||
permissions: Permission[];
|
||||
rolePermissions: Record<number, number[]>; // roleId -> permissionIds[]
|
||||
}
|
||||
|
||||
const securityService = {
|
||||
getRoles: async () => {
|
||||
const response = await apiClient.get<any>("/users/roles");
|
||||
return response.data?.data || response.data;
|
||||
},
|
||||
getPermissions: async () => {
|
||||
const response = await apiClient.get<any>("/users/permissions");
|
||||
return response.data?.data || response.data;
|
||||
},
|
||||
updateRolePermissions: async (roleId: number, permissionIds: number[]) => {
|
||||
// This endpoint might not exist as a bulk update, usually it's per role
|
||||
// Assuming backend supports: PATCH /users/roles/:id/permissions { permissionIds: [] }
|
||||
return (await apiClient.patch(`/users/roles/${roleId}/permissions`, { permissionIds })).data;
|
||||
},
|
||||
};
|
||||
|
||||
export function RbacMatrix() {
|
||||
const queryClient = useQueryClient();
|
||||
const [pendingChanges, setPendingChanges] = useState<Record<number, number[]>>({});
|
||||
|
||||
const { data: roles = [], isLoading: rolesLoading } = useQuery<Role[]>({
|
||||
queryKey: ["roles"],
|
||||
queryFn: securityService.getRoles,
|
||||
});
|
||||
|
||||
const { data: permissions = [], isLoading: permsLoading } = useQuery<Permission[]>({
|
||||
queryKey: ["permissions"],
|
||||
queryFn: securityService.getPermissions,
|
||||
});
|
||||
|
||||
// Fetch current assignments - this logic assumes we can get a map or list
|
||||
// For now, let's assume we can fetch matrix or individual role calls
|
||||
// In a real implementation this is heavier. For implementation speed, I'll mock the state logic assumption
|
||||
// that we load initial state from roles (if roles include permissions relation).
|
||||
|
||||
// TODO: Fetch existing role_permissions. Assuming roles endpoint returns `permissions` array.
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async (changes: Record<number, number[]>) => {
|
||||
const promises = Object.entries(changes).map(([roleId, perms]) =>
|
||||
securityService.updateRolePermissions(parseInt(roleId), perms)
|
||||
);
|
||||
return Promise.all(promises);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Permissions updated successfully");
|
||||
setPendingChanges({});
|
||||
queryClient.invalidateQueries({ queryKey: ["roles"] });
|
||||
},
|
||||
onError: () => toast.error("Failed to update permissions"),
|
||||
});
|
||||
|
||||
const handleToggle = (roleId: number, permId: number, currentPerms: number[]) => {
|
||||
const roleChanges = pendingChanges[roleId] || currentPerms;
|
||||
const newPerms = roleChanges.includes(permId)
|
||||
? roleChanges.filter((id) => id !== permId)
|
||||
: [...roleChanges, permId];
|
||||
|
||||
setPendingChanges({ ...pendingChanges, [roleId]: newPerms });
|
||||
};
|
||||
|
||||
const hasChanges = Object.keys(pendingChanges).length > 0;
|
||||
|
||||
if (rolesLoading || permsLoading) {
|
||||
return (
|
||||
<div className="flex justify-center p-8">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Simplified: Permissions grouped by module/resource would be better, but flat list for now
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={() => updateMutation.mutate(pendingChanges)}
|
||||
disabled={!hasChanges || updateMutation.isPending}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[300px]">Permission</TableHead>
|
||||
{roles.map((role) => (
|
||||
<TableHead key={role.roleId} className="text-center min-w-[100px]">
|
||||
{role.roleName}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{permissions.map((perm) => (
|
||||
<TableRow key={perm.permissionId}>
|
||||
<TableCell className="font-medium">
|
||||
<div>{perm.permissionName}</div>
|
||||
<div className="text-xs text-muted-foreground">{perm.description}</div>
|
||||
</TableCell>
|
||||
{roles.map((role: any) => {
|
||||
// Assume role.permissions is populated
|
||||
const currentRolePerms = role.permissions?.map((p: any) => p.permissionId) || [];
|
||||
const activePerms = pendingChanges[role.roleId] || currentRolePerms;
|
||||
const isChecked = activePerms.includes(perm.permissionId);
|
||||
|
||||
return (
|
||||
<TableCell key={`${role.roleId}-${perm.permissionId}`} className="text-center">
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onCheckedChange={() => handleToggle(role.roleId, perm.permissionId, currentRolePerms)}
|
||||
/>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,16 +3,20 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Users, Building2, Settings, FileText, Activity, GitGraph } from "lucide-react";
|
||||
import { Users, Building2, Settings, FileText, Activity, GitGraph, Shield, BookOpen } from "lucide-react";
|
||||
|
||||
const menuItems = [
|
||||
{ href: "/admin/users", label: "Users", icon: Users },
|
||||
{ href: "/admin/organizations", label: "Organizations", icon: Building2 },
|
||||
{ href: "/admin/projects", label: "Projects", icon: FileText },
|
||||
{ href: "/admin/settings", label: "Settings", icon: Settings },
|
||||
{ href: "/admin/audit-logs", label: "Audit Logs", icon: Activity },
|
||||
{ href: "/admin/workflows", label: "Workflows", icon: GitGraph },
|
||||
{ href: "/admin/reference", label: "Reference Data", icon: BookOpen },
|
||||
{ href: "/admin/numbering", label: "Numbering", icon: FileText },
|
||||
{ href: "/admin/workflows", label: "Workflows", icon: GitGraph },
|
||||
{ href: "/admin/security/roles", label: "Security Roles", icon: Shield },
|
||||
{ href: "/admin/security/sessions", label: "Active Sessions", icon: Users },
|
||||
{ href: "/admin/system-logs/numbering", label: "System Logs", icon: Activity },
|
||||
{ href: "/admin/audit-logs", label: "Audit Logs", icon: Activity },
|
||||
{ href: "/admin/settings", label: "Settings", icon: Settings },
|
||||
];
|
||||
|
||||
export function AdminSidebar() {
|
||||
|
||||
@@ -13,9 +13,17 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useCreateUser, useUpdateUser } from "@/hooks/use-users";
|
||||
import { useCreateUser, useUpdateUser, useRoles } from "@/hooks/use-users";
|
||||
import { useOrganizations } from "@/hooks/use-master-data";
|
||||
import { useEffect } from "react";
|
||||
import { User } from "@/types/user";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const userSchema = z.object({
|
||||
username: z.string().min(3),
|
||||
@@ -24,7 +32,9 @@ const userSchema = z.object({
|
||||
last_name: z.string().min(1),
|
||||
password: z.string().min(6).optional(),
|
||||
is_active: z.boolean().default(true),
|
||||
role_ids: z.array(z.number()).default([]), // Using role_ids array
|
||||
line_id: z.string().optional(),
|
||||
primary_organization_id: z.coerce.number().optional(),
|
||||
role_ids: z.array(z.number()).default([]),
|
||||
});
|
||||
|
||||
type UserFormData = z.infer<typeof userSchema>;
|
||||
@@ -38,6 +48,8 @@ interface UserDialogProps {
|
||||
export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
const createUser = useCreateUser();
|
||||
const updateUser = useUpdateUser();
|
||||
const { data: roles = [] } = useRoles();
|
||||
const { data: organizations = [] } = useOrganizations();
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -47,53 +59,65 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<UserFormData>({
|
||||
resolver: zodResolver(userSchema),
|
||||
resolver: zodResolver(userSchema) as any,
|
||||
defaultValues: {
|
||||
is_active: true,
|
||||
role_ids: []
|
||||
}
|
||||
username: "",
|
||||
email: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
is_active: true,
|
||||
role_ids: [] as number[],
|
||||
line_id: "",
|
||||
primary_organization_id: undefined as number | undefined,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
is_active: user.is_active,
|
||||
role_ids: user.roles?.map(r => r.role_id) || []
|
||||
});
|
||||
reset({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
is_active: user.is_active,
|
||||
line_id: user.line_id || "",
|
||||
primary_organization_id: user.primary_organization_id,
|
||||
role_ids: user.roles?.map((r: any) => r.roleId) || [],
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
username: "",
|
||||
email: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
is_active: true,
|
||||
role_ids: []
|
||||
});
|
||||
reset({
|
||||
username: "",
|
||||
email: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
is_active: true,
|
||||
line_id: "",
|
||||
primary_organization_id: undefined,
|
||||
role_ids: [],
|
||||
});
|
||||
}
|
||||
}, [user, reset, open]); // Reset when open changes or user changes
|
||||
|
||||
const availableRoles = [
|
||||
{ role_id: 1, role_name: "ADMIN", description: "System Administrator" },
|
||||
{ role_id: 2, role_name: "USER", description: "Regular User" },
|
||||
{ role_id: 3, role_name: "APPROVER", description: "Document Approver" },
|
||||
];
|
||||
}, [user, reset, open]);
|
||||
|
||||
const selectedRoleIds = watch("role_ids") || [];
|
||||
|
||||
const onSubmit = (data: UserFormData) => {
|
||||
// If password is empty (and editing), exclude it
|
||||
if (user && !data.password) {
|
||||
delete data.password;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
updateUser.mutate({ id: user.user_id, data }, {
|
||||
onSuccess: () => onOpenChange(false)
|
||||
});
|
||||
updateUser.mutate(
|
||||
{ id: user.user_id, data },
|
||||
{
|
||||
onSuccess: () => onOpenChange(false),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createUser.mutate(data as any, {
|
||||
onSuccess: () => onOpenChange(false)
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createUser.mutate(data as any, {
|
||||
onSuccess: () => onOpenChange(false),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -109,13 +133,17 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
<div>
|
||||
<Label>Username *</Label>
|
||||
<Input {...register("username")} disabled={!!user} />
|
||||
{errors.username && <p className="text-sm text-red-500">{errors.username.message}</p>}
|
||||
{errors.username && (
|
||||
<p className="text-sm text-red-500">{errors.username.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Email *</Label>
|
||||
<Input type="email" {...register("email")} />
|
||||
{errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
|
||||
{errors.email && (
|
||||
<p className="text-sm text-red-500">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,37 +159,76 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Line ID</Label>
|
||||
<Input {...register("line_id")} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Primary Organization</Label>
|
||||
<Select
|
||||
value={watch("primary_organization_id")?.toString()}
|
||||
onValueChange={(val) =>
|
||||
setValue("primary_organization_id", parseInt(val))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Organization" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{organizations?.map((org: any) => (
|
||||
<SelectItem
|
||||
key={org.id}
|
||||
value={org.id.toString()}
|
||||
>
|
||||
{org.organizationCode} - {org.organizationName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<div>
|
||||
<Label>Password *</Label>
|
||||
<Input type="password" {...register("password")} />
|
||||
{errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
|
||||
{errors.password && (
|
||||
<p className="text-sm text-red-500">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label className="mb-3 block">Roles</Label>
|
||||
<div className="space-y-2 border p-3 rounded-md">
|
||||
{availableRoles.map((role) => (
|
||||
<div key={role.role_id} className="flex items-start space-x-2">
|
||||
<div className="space-y-2 border p-3 rounded-md max-h-[200px] overflow-y-auto">
|
||||
{roles.length === 0 && <p className="text-sm text-muted-foreground">Loading roles...</p>}
|
||||
{roles.map((role: any) => (
|
||||
<div key={role.roleId} className="flex items-start space-x-2">
|
||||
<Checkbox
|
||||
id={`role-${role.role_id}`}
|
||||
checked={selectedRoleIds.includes(role.role_id)}
|
||||
id={`role-${role.roleId}`}
|
||||
checked={selectedRoleIds.includes(role.roleId)}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = selectedRoleIds;
|
||||
if (checked) {
|
||||
setValue("role_ids", [...current, role.role_id]);
|
||||
setValue("role_ids", [...current, role.roleId]);
|
||||
} else {
|
||||
setValue("role_ids", current.filter(id => id !== role.role_id));
|
||||
setValue(
|
||||
"role_ids",
|
||||
current.filter((id) => id !== role.roleId)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="grid gap-1.5 leading-none">
|
||||
<label
|
||||
htmlFor={`role-${role.role_id}`}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
htmlFor={`role-${role.roleId}`}
|
||||
className="text-sm font-medium leading-none cursor-pointer"
|
||||
>
|
||||
{role.role_name}
|
||||
{role.roleName}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{role.description}
|
||||
@@ -174,17 +241,17 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
|
||||
{user && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={watch("is_active")}
|
||||
onCheckedChange={(chk) => setValue("is_active", chk === true)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="is_active"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Active User
|
||||
</label>
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={watch("is_active")}
|
||||
onCheckedChange={(chk) => setValue("is_active", chk === true)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="is_active"
|
||||
className="text-sm font-medium leading-none cursor-pointer"
|
||||
>
|
||||
Active User
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -196,7 +263,10 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={createUser.isPending || updateUser.isPending}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createUser.isPending || updateUser.isPending}
|
||||
>
|
||||
{user ? "Update User" : "Create User"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user