Initial Release

This commit is contained in:
Tola Leng
2025-05-09 21:28:07 +07:00
commit 0c6cd18e6f
244 changed files with 50633 additions and 0 deletions
@@ -0,0 +1,46 @@
import React from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { UseFormReturn } from "react-hook-form";
import AddUserForm from "./form-fields/AddUserForm";
interface AddUserDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
form: UseFormReturn<any>;
onSubmit: (data: any) => void;
isSubmitting: boolean;
}
const AddUserDialog = ({ isOpen, setIsOpen, form, onSubmit, isSubmitting }: AddUserDialogProps) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[700px] w-[95vw]">
<DialogHeader>
<DialogTitle>Add New User</DialogTitle>
<DialogDescription>
Create a new user account
</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[60vh]">
<div className="p-4">
<AddUserForm
form={form}
onSubmit={onSubmit}
isSubmitting={isSubmitting}
/>
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);
};
export default AddUserDialog;
@@ -0,0 +1,81 @@
import React, { useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { User } from "@/services/userService";
import { Loader2 } from "lucide-react";
interface DeleteUserDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
user: User | null;
onDelete: () => void;
isDeleting?: boolean;
}
const DeleteUserDialog = ({
isOpen,
setIsOpen,
user,
onDelete,
isDeleting = false,
}: DeleteUserDialogProps) => {
if (!user) return null;
const handleCancel = () => {
if (!isDeleting) {
setIsOpen(false);
}
};
const handleConfirm = () => {
if (!isDeleting) {
onDelete();
}
};
return (
<AlertDialog open={isOpen} onOpenChange={(newOpen) => {
// Only allow closing if not currently deleting
if (!isDeleting || !newOpen) {
setIsOpen(newOpen);
}
}}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete user</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {user.full_name || user.username}? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCancel} disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirm}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
'Delete'
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default DeleteUserDialog;
@@ -0,0 +1,124 @@
import React from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Form } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { UseFormReturn } from "react-hook-form";
import { User } from "@/services/userService";
import { Loader2, AlertCircle } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import UserTextField from "./form-fields/UserTextField";
import UserToggleField from "./form-fields/UserToggleField";
import UserProfilePictureField from "./form-fields/UserProfilePictureField";
interface EditUserDialogProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
form: UseFormReturn<any>;
user: User | null;
onSubmit: (data: any) => void;
isSubmitting?: boolean;
error?: string | null;
}
const EditUserDialog = ({
isOpen,
setIsOpen,
form,
user,
onSubmit,
isSubmitting = false,
error = null
}: EditUserDialogProps) => {
if (!user) return null;
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[700px] w-[95vw]">
<DialogHeader>
<DialogTitle>Edit User</DialogTitle>
<DialogDescription>
Update user information
</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[60vh]">
<div className="p-4">
{error && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<UserProfilePictureField control={form.control} />
<div className="grid grid-cols-2 gap-4">
<UserTextField
control={form.control}
name="full_name"
label="Full Name"
placeholder="Enter full name"
/>
<UserTextField
control={form.control}
name="email"
label="Email"
placeholder="Enter email"
type="email"
/>
<UserTextField
control={form.control}
name="username"
label="Username"
placeholder="Enter username"
/>
<UserTextField
control={form.control}
name="role"
label="Role"
placeholder="Enter role (e.g. admin, user)"
/>
</div>
<UserToggleField
control={form.control}
name="isActive"
label="Active Status"
description="User will be able to access the system"
/>
<DialogFooter className="pt-4">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating...
</>
) : (
"Update User"
)}
</Button>
</DialogFooter>
</form>
</Form>
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);
};
export default EditUserDialog;
@@ -0,0 +1,117 @@
import React from "react";
import UserTable from "./UserTable";
import AddUserDialog from "./AddUserDialog";
import EditUserDialog from "./EditUserDialog";
import DeleteUserDialog from "./DeleteUserDialog";
import { useUserManagement } from "./hooks";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger
} from "@/components/ui/accordion";
import { UserCog, Loader2, AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
const UserManagement = () => {
const {
users,
loading,
error,
newUserForm,
isAddUserDialogOpen,
setIsAddUserDialogOpen,
isDialogOpen,
setIsDialogOpen,
isDeleting,
setIsDeleting,
isSubmitting,
updateError,
form,
currentUser,
userToDelete,
handleEditUser,
handleDeletePrompt,
handleDeleteUser,
onSubmit,
onAddUser,
fetchUsers
} = useUserManagement();
return (
<div className="space-y-4">
<Accordion type="single" collapsible className="w-full" defaultValue="user-management">
<AccordionItem value="user-management">
<AccordionTrigger className="py-4 px-5 bg-card hover:bg-card/90 hover:no-underline rounded-lg text-lg font-medium flex items-center w-full">
<div className="flex items-center">
<UserCog className="h-5 w-5 mr-2 text-green-500" />
<span>User Management</span>
</div>
</AccordionTrigger>
<AccordionContent className="p-4 pt-6 bg-background rounded-b-lg">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">User Management</h2>
<button
onClick={() => setIsAddUserDialogOpen(true)}
className="bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md flex items-center"
>
<span className="mr-1">+</span> Add User
</button>
</div>
{loading ? (
<div className="p-8 flex flex-col items-center justify-center text-center">
<Loader2 className="h-8 w-8 animate-spin mb-2 text-primary" />
<p className="text-muted-foreground">Loading users...</p>
</div>
) : error ? (
<div className="p-8 flex flex-col items-center justify-center text-center">
<AlertCircle className="h-8 w-8 text-destructive mb-2" />
<p className="text-destructive font-medium mb-2">Failed to load users</p>
<p className="text-muted-foreground mb-4">{error}</p>
<Button onClick={fetchUsers} variant="outline">
Retry
</Button>
</div>
) : (
<UserTable
users={users}
onUserUpdate={handleEditUser}
onUserDelete={handleDeletePrompt}
/>
)}
<AddUserDialog
isOpen={isAddUserDialogOpen}
setIsOpen={setIsAddUserDialogOpen}
form={newUserForm}
onSubmit={onAddUser}
isSubmitting={isSubmitting}
/>
<EditUserDialog
isOpen={isDialogOpen}
setIsOpen={setIsDialogOpen}
form={form}
user={currentUser}
onSubmit={onSubmit}
isSubmitting={isSubmitting}
error={updateError}
/>
<DeleteUserDialog
isOpen={isDeleting}
setIsOpen={setIsDeleting}
user={userToDelete}
onDelete={handleDeleteUser}
isDeleting={isSubmitting}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};
export default UserManagement;
@@ -0,0 +1,112 @@
import React from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Edit, Trash2 } from "lucide-react";
import { User } from "@/services/userService";
export interface UserTableProps {
users: User[];
onUserUpdate: (user: User) => void;
onUserDelete: (user: User) => void;
}
const UserTable = ({ users, onUserUpdate, onUserDelete }: UserTableProps) => {
// Helper function to get the user's initials for the avatar fallback
const getUserInitials = (user: User): string => {
return (user.full_name || user.username || "").substring(0, 2).toUpperCase();
};
return (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Username</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8">
No users found
</TableCell>
</TableRow>
) : (
users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8">
{user.avatar ? (
<AvatarImage
src={user.avatar}
alt={user.full_name || user.username}
/>
) : (
<AvatarFallback>
{getUserInitials(user)}
</AvatarFallback>
)}
</Avatar>
<span>{user.full_name || "-"}</span>
</div>
</TableCell>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role || "user"}</TableCell>
<TableCell>
{user.isActive !== false ? (
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
Active
</Badge>
) : (
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200">
Inactive
</Badge>
)}
</TableCell>
<TableCell className="text-right space-x-1">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => onUserUpdate(user)}
>
<Edit className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-red-500 hover:text-red-600 hover:bg-red-50"
onClick={() => onUserDelete(user)}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
};
export default UserTable;
@@ -0,0 +1,13 @@
export const avatarOptions = [
{ url: "https://api.dicebear.com/7.x/bottts/svg?seed=Felix", label: "Avatar 1" },
{ url: "https://api.dicebear.com/7.x/bottts/svg?seed=Aneka", label: "Avatar 2" },
{ url: "https://api.dicebear.com/7.x/bottts/svg?seed=Milo", label: "Avatar 3" },
{ url: "https://api.dicebear.com/7.x/fun-emoji/svg?seed=Cuddles", label: "Avatar 4" },
{ url: "https://api.dicebear.com/7.x/fun-emoji/svg?seed=Bella", label: "Avatar 5" },
{ url: "https://api.dicebear.com/7.x/fun-emoji/svg?seed=Shadow", label: "Avatar 6" },
{ url: "https://api.dicebear.com/7.x/lorelei/svg?seed=Jasper", label: "Avatar 7" },
{ url: "https://api.dicebear.com/7.x/lorelei/svg?seed=Willow", label: "Avatar 8" },
{ url: "https://api.dicebear.com/7.x/notionists/svg?seed=Mittens", label: "Avatar 9" },
{ url: "https://api.dicebear.com/7.x/notionists/svg?seed=Oliver", label: "Avatar 10" },
];
@@ -0,0 +1,95 @@
import React from "react";
import { Form } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { UseFormReturn } from "react-hook-form";
import UserProfilePictureField from "./UserProfilePictureField";
import UserTextField from "./UserTextField";
import UserToggleField from "./UserToggleField";
import { DialogFooter } from "@/components/ui/dialog";
interface AddUserFormProps {
form: UseFormReturn<any>;
onSubmit: (data: any) => void;
isSubmitting: boolean;
}
const AddUserForm = ({ form, onSubmit, isSubmitting }: AddUserFormProps) => {
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<UserProfilePictureField control={form.control} />
<div className="grid grid-cols-2 gap-4">
<UserTextField
control={form.control}
name="full_name"
label="Full Name"
placeholder="Enter full name"
/>
<UserTextField
control={form.control}
name="email"
label="Email"
placeholder="Enter email"
type="email"
/>
<UserTextField
control={form.control}
name="username"
label="Username"
placeholder="Enter username"
/>
<UserTextField
control={form.control}
name="role"
label="Role"
placeholder="Enter role (e.g. admin, user)"
/>
<UserTextField
control={form.control}
name="password"
label="Password"
placeholder="Enter password"
type="password"
/>
<UserTextField
control={form.control}
name="passwordConfirm"
label="Confirm Password"
placeholder="Confirm password"
type="password"
/>
</div>
<UserToggleField
control={form.control}
name="isActive"
label="Active Status"
description="User will be able to access the system"
/>
<DialogFooter className="pt-4">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create User"
)}
</Button>
</DialogFooter>
</form>
</Form>
);
};
export default AddUserForm;
@@ -0,0 +1,76 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Control } from "react-hook-form";
// Custom interface for local profile images
interface ProfileImage {
url: string;
label: string;
}
// Define local profile image paths - include all uploaded SVG images
const localProfileImages: ProfileImage[] = [
{ url: "/upload/profile/profile1.svg", label: "Profile 1" },
{ url: "/upload/profile/profile2.svg", label: "Profile 2" },
{ url: "/upload/profile/profile3.svg", label: "Profile 3" },
{ url: "/upload/profile/profile4.svg", label: "Profile 4" },
{ url: "/upload/profile/profile5.svg", label: "Profile 5" },
{ url: "/upload/profile/profile6.svg", label: "Profile 6" },
{ url: "/upload/profile/profile7.svg", label: "Profile 7" },
{ url: "/upload/profile/profile8.svg", label: "Profile 8" }
];
interface UserProfilePictureFieldProps {
control: Control<any>;
}
const UserProfilePictureField = ({ control }: UserProfilePictureFieldProps) => {
return (
<FormField
control={control}
name="avatar"
render={({ field }) => (
<FormItem>
<FormLabel>Profile Picture</FormLabel>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="grid grid-cols-3 gap-4"
>
{localProfileImages.map((avatar) => (
<FormItem key={avatar.url} className="flex flex-col items-center justify-center space-y-1">
<FormControl>
<RadioGroupItem
value={avatar.url}
id={`new-${avatar.url}`}
className="sr-only"
/>
</FormControl>
<label
htmlFor={`new-${avatar.url}`}
className={`cursor-pointer rounded-md p-1 ${
field.value === avatar.url
? "ring-2 ring-primary ring-offset-2"
: ""
}`}
>
<Avatar className="h-16 w-16">
<AvatarImage src={avatar.url} alt={avatar.label} />
<AvatarFallback>?</AvatarFallback>
</Avatar>
</label>
<div className="text-xs text-center">{avatar.label}</div>
</FormItem>
))}
</RadioGroup>
<FormMessage />
</FormItem>
)}
/>
);
};
export default UserProfilePictureField;
@@ -0,0 +1,54 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Control } from "react-hook-form";
interface UserTextFieldProps {
control: Control<any>;
name: string;
label: string;
placeholder: string;
type?: string;
required?: boolean;
}
const UserTextField = ({
control,
name,
label,
placeholder,
type = "text",
required = false
}: UserTextFieldProps) => {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>
{label}
{required && <span className="text-destructive ml-1">*</span>}
</FormLabel>
<FormControl>
<Input
type={type}
placeholder={placeholder}
{...field}
value={field.value || ''}
onChange={e => {
// Handle the value change properly
const value = e.target.value;
field.onChange(value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
export default UserTextField;
@@ -0,0 +1,39 @@
import React from "react";
import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { Control } from "react-hook-form";
interface UserToggleFieldProps {
control: Control<any>;
name: string;
label: string;
description: string;
}
const UserToggleField = ({ control, name, label, description }: UserToggleFieldProps) => {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>{label}</FormLabel>
<FormDescription>
{description}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
);
};
export default UserToggleField;
@@ -0,0 +1,5 @@
export { default as UserProfilePictureField } from './UserProfilePictureField';
export { default as UserTextField } from './UserTextField';
export { default as UserToggleField } from './UserToggleField';
export { default as AddUserForm } from './AddUserForm';
@@ -0,0 +1,6 @@
export * from './useUserManagement';
export * from './useUsersList';
export * from './useUserForm';
export * from './useUserDialogs';
export * from './useUserOperations';
@@ -0,0 +1,51 @@
import { useState } from "react";
import { User } from "@/services/userService";
export const useUserDialogs = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isAddUserDialogOpen, setIsAddUserDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [userToDelete, setUserToDelete] = useState<User | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [updateError, setUpdateError] = useState<string | null>(null);
const handleEditUser = (user: User, form: any) => {
setUpdateError(null);
setCurrentUser(user);
form.reset({
full_name: user.full_name || "",
email: user.email,
username: user.username,
isActive: user.isActive !== undefined ? user.isActive : true,
role: user.role || "user",
avatar: user.avatar || "",
});
setIsDialogOpen(true);
};
const handleDeletePrompt = (user: User) => {
setUserToDelete(user);
setIsDeleting(true);
};
return {
isDialogOpen,
setIsDialogOpen,
isAddUserDialogOpen,
setIsAddUserDialogOpen,
isDeleting,
setIsDeleting,
isSubmitting,
setIsSubmitting,
updateError,
setUpdateError,
currentUser,
setCurrentUser,
userToDelete,
setUserToDelete,
handleEditUser,
handleDeletePrompt,
};
};
@@ -0,0 +1,35 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { userFormSchema, newUserFormSchema, UserFormValues, NewUserFormValues } from "../userForms";
import { avatarOptions } from "../avatarOptions";
export const useUserForm = () => {
const form = useForm<UserFormValues>({
resolver: zodResolver(userFormSchema),
defaultValues: {
full_name: "",
email: "",
username: "",
isActive: true,
role: "user",
avatar: "",
},
});
const newUserForm = useForm<NewUserFormValues>({
resolver: zodResolver(newUserFormSchema),
defaultValues: {
full_name: "",
email: "",
username: "",
password: "",
passwordConfirm: "",
isActive: true,
role: "user",
avatar: avatarOptions[0].url,
},
});
return { form, newUserForm };
};
@@ -0,0 +1,86 @@
import { useUsersList } from "./useUsersList";
import { useUserForm } from "./useUserForm";
import { useUserDialogs } from "./useUserDialogs";
import { useUserOperations } from "./useUserOperations";
import { User } from "@/services/userService";
import { UserFormValues, NewUserFormValues } from "../userForms";
export const useUserManagement = () => {
const { users, loading, error, fetchUsers } = useUsersList();
const { form, newUserForm } = useUserForm();
const {
isDialogOpen,
setIsDialogOpen,
isAddUserDialogOpen,
setIsAddUserDialogOpen,
isDeleting,
setIsDeleting,
isSubmitting,
setIsSubmitting,
updateError,
setUpdateError,
currentUser,
setCurrentUser,
userToDelete,
setUserToDelete,
handleEditUser: baseHandleEditUser,
handleDeletePrompt,
} = useUserDialogs();
const {
handleDeleteUser: baseHandleDeleteUser,
onSubmit: baseOnSubmit,
onAddUser: baseOnAddUser,
} = useUserOperations(
fetchUsers,
setIsDialogOpen,
setIsAddUserDialogOpen,
setIsSubmitting,
setIsDeleting,
setUpdateError,
newUserForm.reset
);
// Wrapper functions to provide the needed arguments
const handleEditUser = (user: User) => {
baseHandleEditUser(user, form);
};
const handleDeleteUser = async () => {
await baseHandleDeleteUser(userToDelete);
setUserToDelete(null);
};
const onSubmit = (data: UserFormValues) => {
baseOnSubmit(data, currentUser);
};
const onAddUser = (data: NewUserFormValues) => {
baseOnAddUser(data);
};
return {
users,
loading,
error,
isDialogOpen,
setIsDialogOpen,
isAddUserDialogOpen,
setIsAddUserDialogOpen,
isDeleting,
setIsDeleting,
isSubmitting,
updateError,
form,
newUserForm,
currentUser,
userToDelete,
fetchUsers,
handleEditUser,
handleDeletePrompt,
handleDeleteUser,
onSubmit,
onAddUser,
};
};
@@ -0,0 +1,158 @@
import { useState } from "react";
import { useToast } from "@/hooks/use-toast";
import { userService, User, UpdateUserData, CreateUserData } from "@/services/userService";
import { UserFormValues, NewUserFormValues } from "../userForms";
import { avatarOptions } from "../avatarOptions";
export const useUserOperations = (
fetchUsers: () => Promise<void>,
setIsDialogOpen: (isOpen: boolean) => void,
setIsAddUserDialogOpen: (isOpen: boolean) => void,
setIsSubmitting: (isSubmitting: boolean) => void,
setIsDeleting: (isDeleting: boolean) => void,
setUpdateError: (error: string | null) => void,
newUserFormReset: (values: any) => void
) => {
const { toast } = useToast();
const handleDeleteUser = async (userToDelete: User | null) => {
if (!userToDelete) return;
try {
const success = await userService.deleteUser(userToDelete.id);
if (success) {
toast({
title: "User deleted",
description: `${userToDelete.full_name || userToDelete.username} has been deleted.`,
});
fetchUsers();
} else {
throw new Error("Failed to delete user");
}
} catch (error) {
toast({
title: "Error",
description: "Could not delete user. Please try again later.",
variant: "destructive",
});
} finally {
setIsDeleting(false);
}
};
const onSubmit = async (data: UserFormValues, currentUser: User | null) => {
if (!currentUser) return;
setIsSubmitting(true);
setUpdateError(null);
try {
// Create update object with only the fields we want to update
const updateData: UpdateUserData = {
full_name: data.full_name,
email: data.email,
username: data.username,
role: data.role,
isActive: data.isActive,
};
// For avatar, only include if it's different from current one
// Note: We're still sending the avatar path, but our updated userService will handle it properly
if (data.avatar && data.avatar !== currentUser.avatar) {
updateData.avatar = data.avatar;
}
console.log("Submitting user update with data:", updateData);
await userService.updateUser(currentUser.id, updateData);
// After successful update, refresh the auth user data if this is the current user
// In a real app, you'd check if the updated user is the current logged-in user
toast({
title: "User updated",
description: `${data.full_name || data.username}'s profile has been updated.`,
});
setIsDialogOpen(false);
await fetchUsers();
} catch (error: any) {
console.error("Error updating user:", error);
let errorMessage = "Failed to update user. Please check your inputs and try again.";
if (error.data?.message) {
errorMessage = error.data.message;
} else if (error.message) {
errorMessage = error.message;
}
setUpdateError(errorMessage);
toast({
title: "Error updating user",
description: errorMessage,
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
};
const onAddUser = async (data: NewUserFormValues) => {
setIsSubmitting(true);
try {
const newUserData: CreateUserData = {
username: data.username,
email: data.email,
password: data.password,
passwordConfirm: data.passwordConfirm,
full_name: data.full_name,
role: data.role,
isActive: data.isActive,
avatar: data.avatar,
emailVisibility: true,
};
await userService.createUser(newUserData);
toast({
title: "User created",
description: `${data.full_name || data.username} has been added successfully.`,
});
setIsAddUserDialogOpen(false);
newUserFormReset({
full_name: "",
email: "",
username: "",
password: "",
passwordConfirm: "",
isActive: true,
role: "user",
avatar: avatarOptions[0].url,
});
await fetchUsers();
} catch (error: any) {
let errorMessage = "Could not create user. Please try again later.";
if (error.data?.message) {
errorMessage = error.data.message;
} else if (error.message) {
errorMessage = error.message;
}
toast({
title: "Error creating user",
description: errorMessage,
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
};
return {
handleDeleteUser,
onSubmit,
onAddUser,
};
};
@@ -0,0 +1,53 @@
import { useState, useEffect } from "react";
import { useToast } from "@/hooks/use-toast";
import { userService, User } from "@/services/userService";
export const useUsersList = () => {
const { toast } = useToast();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchUsers = async () => {
setLoading(true);
setError(null);
try {
console.log("Fetching users list");
const data = await userService.getUsers();
console.log("Received users data:", data);
if (Array.isArray(data) && data.length >= 0) {
setUsers(data);
// Clear any previous errors
setError(null);
} else {
console.error("Invalid users data format:", data);
setUsers([]);
setError("Invalid data format received from server");
toast({
title: "Warning",
description: "No users found or could not load users.",
variant: "destructive",
});
}
} catch (error) {
console.error("Error fetching users:", error);
setError(error instanceof Error ? error.message : "Unknown error");
toast({
title: "Error fetching users",
description: "Could not load users. Please try again later.",
variant: "destructive",
});
setUsers([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, []);
return { users, loading, error, fetchUsers };
};
@@ -0,0 +1,3 @@
import UserManagement from "./UserManagement";
export default UserManagement;
@@ -0,0 +1,32 @@
import * as z from "zod";
export const userFormSchema = z.object({
full_name: z.string().min(2, {
message: "Name must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
username: z.string().min(3, {
message: "Username must be at least 3 characters.",
}),
isActive: z.boolean().optional(),
role: z.string().optional(),
avatar: z.string().optional(),
});
export const newUserFormSchema = userFormSchema.extend({
password: z.string().min(8, {
message: "Password must be at least 8 characters.",
}),
passwordConfirm: z.string().min(8, {
message: "Password confirmation must be at least 8 characters.",
}),
}).refine((data) => data.password === data.passwordConfirm, {
message: "Passwords don't match",
path: ["passwordConfirm"],
});
export type UserFormValues = z.infer<typeof userFormSchema>;
export type NewUserFormValues = z.infer<typeof newUserFormSchema>;