Handling CRUD for members in frontend
This commit is contained in:
@@ -20,9 +20,9 @@ func HandleGetUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
core.JSONError{
|
core.JSONSuccess{
|
||||||
Status: core.Error,
|
Status: core.Success,
|
||||||
Message: "Not users.",
|
Message: "No users.",
|
||||||
}.Respond(w, http.StatusNotFound)
|
}.Respond(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,6 @@ func HandleGetUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO : Remove password
|
|
||||||
core.JSONSuccess{
|
core.JSONSuccess{
|
||||||
Status: core.Success,
|
Status: core.Success,
|
||||||
Message: "Users found.",
|
Message: "Users found.",
|
||||||
|
|||||||
10
frontend/app/(auth)/dashboard/members/page.tsx
Normal file
10
frontend/app/(auth)/dashboard/members/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"use server";
|
||||||
|
import MembersTable from "@/components/members-table";
|
||||||
|
|
||||||
|
export default async function Page({}) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-10">
|
||||||
|
<MembersTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -54,6 +54,10 @@ const data = {
|
|||||||
icon: Users,
|
icon: Users,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
title: "Liste des membres",
|
||||||
|
url: "/dashboard/members",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Création d'un membre",
|
title: "Création d'un membre",
|
||||||
url: "/dashboard/members/new",
|
url: "/dashboard/members/new",
|
||||||
|
|||||||
152
frontend/components/member-dialog.tsx
Normal file
152
frontend/components/member-dialog.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import * as z from "zod";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
|
||||||
|
const memberSchema = z.object({
|
||||||
|
userId: z.string().optional(),
|
||||||
|
firstname: z.string().min(1, "Prénom est requis."),
|
||||||
|
lastname: z.string().min(1, "Nom de famille est requis."),
|
||||||
|
email: z.string().email("Adresse email invalide."),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(6, "Le mot de passe doit avoir au moins 6 caractères.")
|
||||||
|
.optional(),
|
||||||
|
phone: z.string().regex(/^\d{10}$/, "Le numéro doit avoir 10 chiffres."),
|
||||||
|
role: z.string().min(1, "Le rôle est requis."),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMemberSchema = memberSchema.partial();
|
||||||
|
|
||||||
|
export type Member = z.infer<typeof memberSchema>;
|
||||||
|
|
||||||
|
interface MemberDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
member: Member | null;
|
||||||
|
onSave: (member: Member) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MemberDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
member,
|
||||||
|
onSave,
|
||||||
|
}: MemberDialogProps) {
|
||||||
|
const schema = member?.userId ? updateMemberSchema : memberSchema;
|
||||||
|
const form = useForm<Member>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: member?.userId
|
||||||
|
? member
|
||||||
|
: {
|
||||||
|
userId: "",
|
||||||
|
firstname: "",
|
||||||
|
lastname: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
phone: "",
|
||||||
|
role: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (member) {
|
||||||
|
form.reset(member);
|
||||||
|
} else {
|
||||||
|
form.reset({
|
||||||
|
userId: "",
|
||||||
|
firstname: "",
|
||||||
|
lastname: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
phone: "",
|
||||||
|
role: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [member, form]);
|
||||||
|
|
||||||
|
const onSubmit = (data: Member) => {
|
||||||
|
onSave(data);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{member
|
||||||
|
? "Mis à jour du membre"
|
||||||
|
: "Créer un nouveau membre"}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
"userId",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"email",
|
||||||
|
"password",
|
||||||
|
"phone",
|
||||||
|
"role",
|
||||||
|
] as const
|
||||||
|
).map((field) => (
|
||||||
|
<FormField
|
||||||
|
key={field}
|
||||||
|
control={form.control}
|
||||||
|
name={field}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{field.name
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase() +
|
||||||
|
field.name.slice(1)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
disabled={
|
||||||
|
field.name === "userId"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit">
|
||||||
|
{member ? "Actualiser" : "Ajouter"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,93 +12,28 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { UpdateMemberDialog } from "./UpdateMemberDialog";
|
import MemberDialog, { Member } from "./member-dialog";
|
||||||
import { AddMemberDialog } from "./AddMemberDialog";
|
import * as z from "zod";
|
||||||
|
import { request, useApi } from "@/hooks/use-api";
|
||||||
|
import {
|
||||||
|
CircleX,
|
||||||
|
Loader2,
|
||||||
|
Trash2,
|
||||||
|
UserRoundPen,
|
||||||
|
UserRoundPlus,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface Member {
|
export default function MembersTable() {
|
||||||
user_id: string;
|
const {
|
||||||
firstname: string;
|
data: members,
|
||||||
lastname: string;
|
error,
|
||||||
email: string;
|
mutate,
|
||||||
password: string;
|
success,
|
||||||
phone: string;
|
isLoading,
|
||||||
role: string;
|
} = useApi<Member[]>("/users", undefined, true, false);
|
||||||
}
|
|
||||||
|
|
||||||
const initialMembers: Member[] = [
|
|
||||||
// Add some sample data here
|
|
||||||
{
|
|
||||||
user_id: "1",
|
|
||||||
firstname: "John",
|
|
||||||
lastname: "Doe",
|
|
||||||
email: "john@example.com",
|
|
||||||
password: "********",
|
|
||||||
phone: "1234567890",
|
|
||||||
role: "User",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user_id: "1",
|
|
||||||
firstname: "John",
|
|
||||||
lastname: "Doe",
|
|
||||||
email: "john@example.com",
|
|
||||||
password: "********",
|
|
||||||
phone: "1234567890",
|
|
||||||
role: "User",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user_id: "1",
|
|
||||||
firstname: "John",
|
|
||||||
lastname: "Doe",
|
|
||||||
email: "john@example.com",
|
|
||||||
password: "********",
|
|
||||||
phone: "1234567890",
|
|
||||||
role: "User",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user_id: "1",
|
|
||||||
firstname: "John",
|
|
||||||
lastname: "Doe",
|
|
||||||
email: "john@example.com",
|
|
||||||
password: "********",
|
|
||||||
phone: "1234567890",
|
|
||||||
role: "User",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user_id: "1",
|
|
||||||
firstname: "John",
|
|
||||||
lastname: "Doe",
|
|
||||||
email: "john@example.com",
|
|
||||||
password: "********",
|
|
||||||
phone: "1234567890",
|
|
||||||
role: "User",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user_id: "1",
|
|
||||||
firstname: "John",
|
|
||||||
lastname: "Doe",
|
|
||||||
email: "john@example.com",
|
|
||||||
password: "********",
|
|
||||||
phone: "1234567890",
|
|
||||||
role: "User",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user_id: "1",
|
|
||||||
firstname: "John",
|
|
||||||
lastname: "Doe",
|
|
||||||
email: "john@example.com",
|
|
||||||
password: "********",
|
|
||||||
phone: "1234567890",
|
|
||||||
role: "User",
|
|
||||||
},
|
|
||||||
// Add more sample members...
|
|
||||||
];
|
|
||||||
|
|
||||||
export function MembersTable() {
|
|
||||||
const [members, setMembers] = useState<Member[]>(initialMembers);
|
|
||||||
const [selectMode, setSelectMode] = useState(false);
|
const [selectMode, setSelectMode] = useState(false);
|
||||||
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
|
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
|
||||||
const [updateDialogOpen, setUpdateDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
||||||
const [currentMember, setCurrentMember] = useState<Member | null>(null);
|
const [currentMember, setCurrentMember] = useState<Member | null>(null);
|
||||||
|
|
||||||
const toggleSelectMode = () => {
|
const toggleSelectMode = () => {
|
||||||
@@ -114,124 +49,134 @@ export function MembersTable() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = (member: Member) => {
|
const handleOpenDialog = (member: Member | null) => {
|
||||||
setCurrentMember(member);
|
setCurrentMember(member);
|
||||||
setUpdateDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (userId: string) => {
|
const handleSaveMember = async (savedMember: Member) => {
|
||||||
setMembers((prev) =>
|
if (savedMember.userId) {
|
||||||
prev.filter((member) => member.user_id !== userId),
|
const res = await request<unknown>(
|
||||||
);
|
`/users/${savedMember.userId}/update`,
|
||||||
|
{
|
||||||
|
body: savedMember,
|
||||||
|
requiresAuth: true,
|
||||||
|
method: "PATCH",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.status === "Success") mutate();
|
||||||
|
// Update existing member
|
||||||
|
// setMembers((prev) =>
|
||||||
|
// prev.map((m) =>
|
||||||
|
// m.user_id === savedMember.user_id ? savedMember : m,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
} else {
|
||||||
|
const res = await request<unknown>("/users/new", {
|
||||||
|
body: savedMember,
|
||||||
|
method: "POST",
|
||||||
|
requiresAuth: true,
|
||||||
|
});
|
||||||
|
if (res.status === "Success") mutate();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = (newMember: Member) => {
|
const handleDelete = async (userId: string) => {
|
||||||
setMembers((prev) => [
|
const res = await request<unknown>(`/users/${userId}/delete`, {
|
||||||
...prev,
|
method: "DELETE",
|
||||||
{ ...newMember, user_id: String(prev.length + 1) },
|
requiresAuth: true,
|
||||||
]);
|
});
|
||||||
|
if (res.status === "Success") mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<Button onClick={toggleSelectMode}>
|
<Button onClick={toggleSelectMode}>
|
||||||
{selectMode ? "Cancel Selection" : "Select"}
|
{selectMode ? <CircleX /> : "Selectionner"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setAddDialogOpen(true)}>
|
<Button onClick={() => handleOpenDialog(null)}>
|
||||||
Add New Member
|
<UserRoundPlus />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ScrollArea className="h-[400px] rounded-md border">
|
<ScrollArea className="h-full rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{selectMode && (
|
{selectMode && (
|
||||||
<TableHead className="w-[50px]">
|
<TableHead className="w-[50px]">
|
||||||
Select
|
Selectionner
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)}
|
)}
|
||||||
<TableHead>User ID</TableHead>
|
<TableHead>Prénom</TableHead>
|
||||||
<TableHead>First Name</TableHead>
|
<TableHead>Nom</TableHead>
|
||||||
<TableHead>Last Name</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
<TableHead>Email</TableHead>
|
||||||
<TableHead>Password</TableHead>
|
<TableHead>Téléphone</TableHead>
|
||||||
<TableHead>Phone</TableHead>
|
<TableHead>Rôle</TableHead>
|
||||||
<TableHead>Role</TableHead>
|
|
||||||
<TableHead className="text-right">
|
<TableHead className="text-right">
|
||||||
Actions
|
Actions
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{members.map((member) => (
|
{isLoading && <Loader2 className="animate-spin" />}
|
||||||
<TableRow key={member.user_id}>
|
{members &&
|
||||||
{selectMode && (
|
members.map((member) => (
|
||||||
|
<TableRow key={member.userId}>
|
||||||
|
{selectMode && (
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedMembers.includes(
|
||||||
|
member.userId!,
|
||||||
|
)}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
toggleMemberSelection(
|
||||||
|
member.userId!,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Checkbox
|
{member.firstname}
|
||||||
checked={selectedMembers.includes(
|
|
||||||
member.user_id,
|
|
||||||
)}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
toggleMemberSelection(
|
|
||||||
member.user_id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
<TableCell>{member.lastname}</TableCell>
|
||||||
<TableCell>{member.user_id}</TableCell>
|
<TableCell>{member.email}</TableCell>
|
||||||
<TableCell>{member.firstname}</TableCell>
|
<TableCell>{member.phone}</TableCell>
|
||||||
<TableCell>{member.lastname}</TableCell>
|
<TableCell>{member.role}</TableCell>
|
||||||
<TableCell>{member.email}</TableCell>
|
<TableCell className="text-right">
|
||||||
<TableCell>{member.password}</TableCell>
|
<Button
|
||||||
<TableCell>{member.phone}</TableCell>
|
variant="outline"
|
||||||
<TableCell>{member.role}</TableCell>
|
size="sm"
|
||||||
<TableCell className="text-right">
|
className="mr-2"
|
||||||
<Button
|
onClick={() =>
|
||||||
variant="outline"
|
handleOpenDialog(member)
|
||||||
size="sm"
|
}
|
||||||
className="mr-2"
|
>
|
||||||
onClick={() => handleUpdate(member)}
|
<UserRoundPen />
|
||||||
>
|
</Button>
|
||||||
Modify
|
<Button
|
||||||
</Button>
|
variant="destructive"
|
||||||
<Button
|
size="sm"
|
||||||
variant="destructive"
|
onClick={() =>
|
||||||
size="sm"
|
handleDelete(member.userId!)
|
||||||
onClick={() =>
|
}
|
||||||
handleDelete(member.user_id)
|
>
|
||||||
}
|
<Trash2 />
|
||||||
>
|
</Button>
|
||||||
Delete
|
</TableCell>
|
||||||
</Button>
|
</TableRow>
|
||||||
</TableCell>
|
))}
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
<UpdateMemberDialog
|
<MemberDialog
|
||||||
isOpen={updateDialogOpen}
|
isOpen={dialogOpen}
|
||||||
onClose={() => setUpdateDialogOpen(false)}
|
onClose={() => setDialogOpen(false)}
|
||||||
member={currentMember}
|
member={currentMember}
|
||||||
onUpdate={(updatedMember) => {
|
onSave={handleSaveMember}
|
||||||
setMembers((prev) =>
|
|
||||||
prev.map((m) =>
|
|
||||||
m.user_id === updatedMember.user_id
|
|
||||||
? updatedMember
|
|
||||||
: m,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setUpdateDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AddMemberDialog
|
|
||||||
isOpen={addDialogOpen}
|
|
||||||
onClose={() => setAddDialogOpen(false)}
|
|
||||||
onAdd={handleAdd}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user