251208:0010 Backend & Frontend Debug
This commit is contained in:
@@ -3,12 +3,10 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Users, Building2, Settings, FileText, Activity, Network, Hash } from "lucide-react";
|
||||
import { Users, Building2, Settings, FileText, Activity } from "lucide-react";
|
||||
|
||||
const menuItems = [
|
||||
{ href: "/admin/users", label: "Users", icon: Users },
|
||||
{ href: "/admin/workflows", label: "Workflows", icon: Network },
|
||||
{ href: "/admin/numbering", label: "Numbering", icon: Hash },
|
||||
{ href: "/admin/organizations", label: "Organizations", icon: Building2 },
|
||||
{ href: "/admin/projects", label: "Projects", icon: FileText },
|
||||
{ href: "/admin/settings", label: "Settings", icon: Settings },
|
||||
@@ -19,12 +17,16 @@ export function AdminSidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside className="w-64 border-r bg-muted/20 p-4 hidden md:block h-full">
|
||||
<h2 className="text-lg font-bold mb-6 px-3">Admin Panel</h2>
|
||||
<aside className="w-64 border-r bg-card p-4 hidden md:block">
|
||||
<div className="mb-8 px-2">
|
||||
<h2 className="text-xl font-bold tracking-tight">Admin Console</h2>
|
||||
<p className="text-sm text-muted-foreground">LCBP3 DMS</p>
|
||||
</div>
|
||||
|
||||
<nav className="space-y-1">
|
||||
{menuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname === item.href;
|
||||
const isActive = pathname.startsWith(item.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -33,8 +35,8 @@ export function AdminSidebar() {
|
||||
className={cn(
|
||||
"flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm font-medium",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||
? "bg-primary text-primary-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
@@ -43,6 +45,12 @@ export function AdminSidebar() {
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto pt-8 px-2 fixed bottom-4 w-56">
|
||||
<Link href="/dashboard" className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-2">
|
||||
← Back to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,19 +13,18 @@ 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 { User, CreateUserDto } from "@/types/admin";
|
||||
import { useEffect, useState } from "react";
|
||||
import { adminApi } from "@/lib/api/admin";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useCreateUser, useUpdateUser } from "@/hooks/use-users";
|
||||
import { useEffect } from "react";
|
||||
import { User } from "@/types/user";
|
||||
|
||||
const userSchema = z.object({
|
||||
username: z.string().min(3, "Username must be at least 3 characters"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
first_name: z.string().min(1, "First name is required"),
|
||||
last_name: z.string().min(1, "Last name is required"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters").optional(),
|
||||
username: z.string().min(3),
|
||||
email: z.string().email(),
|
||||
first_name: z.string().min(1),
|
||||
last_name: z.string().min(1),
|
||||
password: z.string().min(6).optional(),
|
||||
is_active: z.boolean().default(true),
|
||||
roles: z.array(z.number()).min(1, "At least one role is required"),
|
||||
role_ids: z.array(z.number()).default([]), // Using role_ids array
|
||||
});
|
||||
|
||||
type UserFormData = z.infer<typeof userSchema>;
|
||||
@@ -34,11 +33,11 @@ interface UserDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
user?: User | null;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function UserDialog({ open, onOpenChange, user, onSuccess }: UserDialogProps) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
const createUser = useCreateUser();
|
||||
const updateUser = useUpdateUser();
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -50,32 +49,32 @@ export function UserDialog({ open, onOpenChange, user, onSuccess }: UserDialogPr
|
||||
} = useForm<UserFormData>({
|
||||
resolver: zodResolver(userSchema),
|
||||
defaultValues: {
|
||||
is_active: true,
|
||||
roles: [],
|
||||
},
|
||||
is_active: true,
|
||||
role_ids: []
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
roles: 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,
|
||||
role_ids: user.roles?.map(r => r.role_id) || []
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
username: "",
|
||||
email: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
is_active: true,
|
||||
roles: [],
|
||||
});
|
||||
reset({
|
||||
username: "",
|
||||
email: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
is_active: true,
|
||||
role_ids: []
|
||||
});
|
||||
}
|
||||
}, [user, reset, open]);
|
||||
}, [user, reset, open]); // Reset when open changes or user changes
|
||||
|
||||
const availableRoles = [
|
||||
{ role_id: 1, role_name: "ADMIN", description: "System Administrator" },
|
||||
@@ -83,32 +82,18 @@ export function UserDialog({ open, onOpenChange, user, onSuccess }: UserDialogPr
|
||||
{ role_id: 3, role_name: "APPROVER", description: "Document Approver" },
|
||||
];
|
||||
|
||||
const selectedRoles = watch("roles") || [];
|
||||
const selectedRoleIds = watch("role_ids") || [];
|
||||
|
||||
const handleRoleChange = (roleId: number, checked: boolean) => {
|
||||
const currentRoles = selectedRoles;
|
||||
const newRoles = checked
|
||||
? [...currentRoles, roleId]
|
||||
: currentRoles.filter((id) => id !== roleId);
|
||||
setValue("roles", newRoles, { shouldValidate: true });
|
||||
};
|
||||
|
||||
const onSubmit = async (data: UserFormData) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (user) {
|
||||
// await adminApi.updateUser(user.user_id, data);
|
||||
console.log("Update user", user.user_id, data);
|
||||
} else {
|
||||
await adminApi.createUser(data as CreateUserDto);
|
||||
}
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Failed to save user");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
const onSubmit = (data: UserFormData) => {
|
||||
if (user) {
|
||||
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)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -122,114 +107,96 @@ export function UserDialog({ open, onOpenChange, user, onSuccess }: UserDialogPr
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="username">Username *</Label>
|
||||
<Input id="username" {...register("username")} disabled={!!user} />
|
||||
{errors.username && (
|
||||
<p className="text-sm text-destructive mt-1">
|
||||
{errors.username.message}
|
||||
</p>
|
||||
)}
|
||||
<Label>Username *</Label>
|
||||
<Input {...register("username")} disabled={!!user} />
|
||||
{errors.username && <p className="text-sm text-red-500">{errors.username.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">Email *</Label>
|
||||
<Input id="email" type="email" {...register("email")} />
|
||||
{errors.email && (
|
||||
<p className="text-sm text-destructive mt-1">
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
<Label>Email *</Label>
|
||||
<Input type="email" {...register("email")} />
|
||||
{errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="first_name">First Name *</Label>
|
||||
<Input id="first_name" {...register("first_name")} />
|
||||
{errors.first_name && (
|
||||
<p className="text-sm text-destructive mt-1">
|
||||
{errors.first_name.message}
|
||||
</p>
|
||||
)}
|
||||
<Label>First Name *</Label>
|
||||
<Input {...register("first_name")} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="last_name">Last Name *</Label>
|
||||
<Input id="last_name" {...register("last_name")} />
|
||||
{errors.last_name && (
|
||||
<p className="text-sm text-destructive mt-1">
|
||||
{errors.last_name.message}
|
||||
</p>
|
||||
)}
|
||||
<Label>Last Name *</Label>
|
||||
<Input {...register("last_name")} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<div>
|
||||
<Label htmlFor="password">Password *</Label>
|
||||
<Input id="password" type="password" {...register("password")} />
|
||||
{errors.password && (
|
||||
<p className="text-sm text-destructive mt-1">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
<Label>Password *</Label>
|
||||
<Input type="password" {...register("password")} />
|
||||
{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 rounded-md p-4">
|
||||
<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 gap-3"
|
||||
>
|
||||
<div key={role.role_id} className="flex items-start space-x-2">
|
||||
<Checkbox
|
||||
id={`role-${role.role_id}`}
|
||||
checked={selectedRoles.includes(role.role_id)}
|
||||
onCheckedChange={(checked) => handleRoleChange(role.role_id, checked as boolean)}
|
||||
checked={selectedRoleIds.includes(role.role_id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = selectedRoleIds;
|
||||
if (checked) {
|
||||
setValue("role_ids", [...current, role.role_id]);
|
||||
} else {
|
||||
setValue("role_ids", current.filter(id => id !== role.role_id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="grid gap-1.5 leading-none">
|
||||
<Label
|
||||
<label
|
||||
htmlFor={`role-${role.role_id}`}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{role.role_name}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{errors.roles && (
|
||||
<p className="text-sm text-destructive mt-1">
|
||||
{errors.roles.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={watch("is_active")}
|
||||
onCheckedChange={(checked) => setValue("is_active", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="is_active">Active</Label>
|
||||
</div>
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
<Button type="submit" disabled={createUser.isPending || updateUser.isPending}>
|
||||
{user ? "Update User" : "Create User"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user