feat: add bulkDelete, tablefilter, rowEdit modal, add Admin modal

This commit is contained in:
Orace.A 2025-03-27 17:44:37 +01:00
parent cbe135ce8a
commit 68d9c2dd5f
10 changed files with 420 additions and 108 deletions

View File

@ -25,6 +25,7 @@
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"sass": "^1.86.0",
"sonner": "^2.0.2",
"zod": "^3.24.2"
},
"devDependencies": {

14
pnpm-lock.yaml generated
View File

@ -56,6 +56,9 @@ importers:
sass:
specifier: ^1.86.0
version: 1.86.0
sonner:
specifier: ^2.0.2
version: 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
zod:
specifier: ^3.24.2
version: 3.24.2
@ -3468,6 +3471,12 @@ packages:
snake-case@3.0.4:
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
sonner@2.0.2:
resolution: {integrity: sha512-xOeXErZ4blqQd11ZnlDmoRmg+ctUJBkTU8H+HVh9rnWi9Ke28xiL39r4iCTeDX31ODTe/s1MaiaY333dUzLCtA==}
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@ -7392,6 +7401,11 @@ snapshots:
dot-case: 3.0.4
tslib: 2.8.1
sonner@2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
source-map-js@1.2.1: {}
stable-hash@0.0.5: {}

View File

@ -1,7 +1,7 @@
"use client";
import Form from "#/components/form/form"
import { loginSchema } from "#/schema/loginSchema"
import { loginSchema } from "#/schema"
import { useMutation } from "@tanstack/react-query"
import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation";
@ -25,6 +25,8 @@ export default function LoginPage() {
: result.error;
console.error(errorMessage)
throw new Error(result.error)
} else {
router.push('/admin/home')
}
return result
} catch (error: any) {
@ -35,9 +37,6 @@ export default function LoginPage() {
}
},
onSuccess: () => {
router.push('/admin/home')
},
onError: (error: Error) => {
console.error(error.message)

View File

@ -3,22 +3,27 @@
import FloatingLabelInput from "#/components/floatingLabelInput"
import { Modal } from "#/components/modal"
import Table from "#/components/table/table"
import { useQuery } from "@tanstack/react-query"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { ColumnDef } from "@tanstack/react-table"
import axios from "axios"
import { table } from "console"
import Image from "next/image"
import { useSession } from "next-auth/react"
export interface Admin {
id: string
email: string
first_name: string
last_name: string
profile: string
}
import { DropdownMenu } from "radix-ui"
import Link from "next/link"
import { icons } from "#/assets/icons"
import { useState } from "react"
import Form from "#/components/form/form"
import { adminSchema } from "#/schema"
import { Admin } from "#/types"
export default function Admins (){
const {data: session, status} = useSession()
const [open, setOpen] = useState(false);
const [openModal, setOpenModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
const [openEditModal, setOpenEditModal] = useState(false);
const [selectedAdminId, setSelectedAdminId] = useState<string | null>(null);
const queryClient = useQueryClient()
const { data: users, refetch, isLoading} = useQuery({
enabled: status === 'authenticated',
@ -42,6 +47,135 @@ export default function Admins (){
}
})
const mutation = useMutation({
mutationFn: async (data: { last_name: string; first_name: string; email: string }) => {
try {
const result = await axios.post(
`https://private-docs-api.intside.co/users/`,
{
last_name: data.last_name,
first_name: data.first_name,
email: data.email,
user_type: "admin"
},
{
headers: {
'Authorization': `Bearer ${session?.user.access_token}`
}
})
if(result.status === 200 || result.status === 201) {
console.log('ajout réussie !')
setOpenModal(false)
}
} catch (error: any) {
if (error.message.includes("Network Error")) {
console.error("Problème de connexion au serveur");
}
console.error("Autre = ", error);
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
refetch()
},
onError: (error: Error) => {
console.error(error.message)
},
})
const mutationUpdate = useMutation({
mutationFn: async (data: { id: string, last_name: string; first_name: string; email: string }) => {
try {
const result = await axios.put(
`https://private-docs-api.intside.co/users/${data.id}/`,
{
last_name: data.last_name,
first_name: data.first_name,
email: data.email,
},
{
headers: {
'Authorization': `Bearer ${session?.user.access_token}`
}
})
if(result.status === 200 || result.status === 201) {
console.log('modification réussie !')
setOpenEditModal(false)
}
} catch (error: any) {
if (error.message.includes("Network Error")) {
console.error("Problème de connexion au serveur");
}
console.error("Autre = ", error);
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
refetch()
},
onError: (error: Error) => {
console.error(error.message)
},
})
const bulkDeleteMutation = useMutation({
mutationFn: async (ids: string[]) => {
try {
const deletePromises = ids.map(id =>
axios.delete(`https://private-docs-api.intside.co/users/${id}/`, {
headers: {
'Authorization': `Bearer ${session?.user.access_token}`
}
})
);
await Promise.all(deletePromises);
} catch (error) {
console.error(error);
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
refetch();
}
});
const { mutate, isPending } = useMutation({
mutationFn: async (id: string) => {
try {
const response = await axios.delete(
`https://private-docs-api.intside.co/users/${id}/`, {
headers: {
'Authorization': `Bearer ${session?.user.access_token}`
}
}
)
if(response.status === 200 || response.status === 201) {
console.log('Suppresion réussie !')
setOpenDeleteModal(false)
}
} catch (error) {
console.error(error)
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
refetch()
}
})
const columns: ColumnDef<Admin>[] = [
{
header: "Administrateurs",
@ -90,52 +224,127 @@ export default function Admins (){
// )
// }
// },
// {
// id: "delete",
// cell: ({ cell }) => {
// const id = String(cell.row.original.id)
// return (
// <div className="relative p-2 cursor-pointer"
// // onClick={() => { mutate(id) }}
// >
// <Modal
// open={open}
// trigger={
// <div onClick={() => setOpen(true)}>
// <Image alt="" src={icons.trash} className="absolute right-2 top-[-50%] transform translate-middle-y hover:text-blue-500" />
// </div>
// }
// title={
// <p className="font-bold text-3xl">Supprimer une organisation</p>
// }
// content={
// <div>
// <p>Voulez-vous vraiment supprimer cette organisation ?</p>
{
id: "delete",
cell: ({ cell }) => {
const admin = cell.row.original
return (
<div className="flex justify-end p-2 cursor-pointer space-x-2"
// onClick={() => { mutate(id) }}
>
// <div className="grid grid-cols-2 gap-3 mt-3">
// <button
// className="bg-blue-100 text-blue-600 py-2 px-4 text-lg rounded-full text-center hover:bg-blue-200"
// onClick={() => { setOpen(false) }}
// >
// Annuler
// </button>
// <button
// className="bg-red-500 text-white py-2 px-4 text-lg rounded-full text-center hover:bg-red-600"
// onClick={() => {
// mutate(id)
// setOpen(false)
// }}
// >
// Supprimer
// </button>
// </div>
// </div>
// }
// />
// </div>
// )
// }
// }
<Modal
open={openEditModal && selectedAdminId === admin.id}
onOpenChange={(isOpen) => {
if (!isOpen) {
setSelectedAdminId(null);
setOpenEditModal(false);
}
}}
trigger={
<div onClick={() =>{
setSelectedAdminId(admin.id);
setOpenEditModal(true)
}}>
<Image alt="" width={24} height={24} src={icons.editIcon} className="hover:text-blue-500" />
</div>
}
title={
<p className="font-bold text-3xl">Modifier un admin</p>
}
content={
<>
<Form
fields={[
{
name: "id", // Ajoutez un champ caché pour l'ID
type: "hidden",
defaultValue: admin.id
},
{
name: "last_name",
label: "Nom",
type: "text",
placeholder: "Entrer le nom de l'admin",
defaultValue: admin.last_name
},
{
name: "first_name",
label: "Prénom",
type: "text",
placeholder: "Entrer le prénom de l'admin",
defaultValue: admin.first_name
},
{
name: "email",
label: "Adresse e-mail",
type: "text",
placeholder: "Entrer l'email de l'admin",
defaultValue: admin.email
},
// {
// name: "statut",
// label: "Adresse e-mail",
// type: "select",
// placeholder: "Entrer l'email de l'admin",
// options
// }
]}
submit={mutationUpdate.mutate}
schema={adminSchema}
child={<button onClick={() => setOpenModal(false)} type="submit" className="btn-auth">{isLoading ? "Chargement..." : "Modifier"}</button>}
/></>
}
/>
<Modal
open={openDeleteModal && selectedAdminId === admin.id}
onOpenChange={(isOpen) => {
if (!isOpen) {
setSelectedAdminId(null);
setOpenDeleteModal(false);
}
}}
trigger={
<div onClick={() => {
setSelectedAdminId(admin.id);
setOpenDeleteModal(true)
}}>
<Image alt="" src={icons.trash} className=" hover:text-blue-500" />
</div>
}
title={
<p className="font-bold text-3xl">Supprimer un admin</p>
}
content={
<div>
<p>Voulez-vous vraiment supprimer cet admin ?</p>
<div className="grid grid-cols-2 gap-3 mt-3">
<button
className="bg-blue-100 text-blue-600 py-2 px-4 text-lg rounded-full text-center hover:bg-blue-200"
onClick={() => { setOpenDeleteModal(false) }}
>
Annuler
</button>
<button
className="bg-red-500 text-white py-2 px-4 text-lg rounded-full text-center hover:bg-red-600"
onClick={() => {
mutate(admin.id)
setOpenDeleteModal(false)
}}
>
Supprimer
</button>
</div>
</div>
}
/>
</div>
)
}
}
]
return (
@ -144,30 +353,82 @@ export default function Admins (){
columns={columns}
data={users || []}
pageSize={5}
header={(table) => (
<div className="grid grid-cols-1 md:grid-cols-2 md:flex-row w-full">
<div className="flex">
<input checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && undefined)
}
header={(table) => {
const ids = table.getRowModel().rows.filter((row) => row.getIsSelected()).map(row => row.original.id )
if(bulkDeleteMutation.isSuccess) {
table.toggleAllPageRowsSelected(false)
}
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 md:flex-row w-full">
<div className="flex items-center space-x-3">
<input checked={table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && undefined)}
onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)}
type="checkbox" name="" id=""
/>
</div>
type="checkbox" name="" id="" />
<div className="flex justify-between md:justify-end space-x-3">
<Modal
trigger={
<button className="p-3 bg-blue-600 text-white rounded-full">
Ajouter un admin
</button>
}
/>
<FloatingLabelInput name="search" placeholder="Effectuer une recherche" type="text" onChange={(value) => table.setGlobalFilter(value)} />
</div>
</div>
)}
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
<DropdownMenu.Trigger asChild>
<p className="cursor-pointer rounded-full bg-gray-300 text-gray-500 p-2">
Sélectionner une action
</p>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="min-w-[150px] shadow-sm bg-white rounded-md p-1" sideOffset={5}>
<DropdownMenu.Item onClick={() => bulkDeleteMutation.mutate(ids)} className="p-2 text-[14px] cursor-pointer hover:bg-blue-100 hover:border-blue-100 hover:text-blue-500 hover:rounded-md outline-none">
Supprimer
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
<div className="flex justify-between md:justify-end space-x-3">
<Modal
title="Ajouter un admin"
open={openModal}
trigger={<div onClick={() => setOpenModal(true)} className="cursor-pointer p-3 bg-blue-600 text-white rounded-full">
Ajouter un admin
</div>}
content={
<>
<Form
fields={[
{
name: "last_name",
label: "Nom",
type: "text",
placeholder: "Entrer le nom de l'admin"
},
{
name: "first_name",
label: "Prénom",
type: "text",
placeholder: "Entrer le prénom de l'admin"
},
{
name: "email",
label: "Adresse e-mail",
type: "text",
placeholder: "Entrer l'email de l'admin"
}
]}
submit={mutation.mutate}
schema={adminSchema}
child={<button type="submit" className="btn-auth">{isLoading ? "Chargement..." : "Créer le compte"}</button>}
/>
</>
}
/>
<FloatingLabelInput name="search" placeholder="Effectuer une recherche" type="text" onChange={(value) => table.setGlobalFilter(value)} />
</div>
</div>
</>
)
}}
/>
</>
)

View File

@ -135,6 +135,11 @@ export default function HomePage () {
>
<Modal
open={open}
onOpenChange={(isOpen)=>{
if(!isOpen) {
setOpen(isOpen)
}
}}
trigger={
<div onClick={() => setOpen(true)}>
<Image alt="" src={icons.trash} className="absolute right-2 top-[-50%] transform translate-middle-y hover:text-blue-500" />

View File

@ -32,8 +32,7 @@ export default function FloatingLabelInput({
return (
<div className="relative w-full">
<select
className="input-form focus:ring-2 focus:ring-blue-500"
required
className="input-form focus:ring-2 focus:ring-blue-500 outline-none"
name={name}
defaultValue={defaultValue}
>
@ -52,9 +51,8 @@ export default function FloatingLabelInput({
name={name}
type={showPassword ? "text" : "password"}
placeholder={placeholder}
className="input-form focus:ring-2 focus:ring-blue-500 pr-10"
className="input-form focus:ring-2 focus:ring-blue-500 pr-10 outline-none"
defaultValue={defaultValue}
required
/>
{showPasswordToggle && (
@ -89,8 +87,7 @@ export default function FloatingLabelInput({
<input
type={type}
placeholder={placeholder}
className="input-form focus:ring-2 focus:ring-blue-500"
required
className="input-form focus:ring-2 focus:ring-blue-500 outline-none"
name={name}
defaultValue={defaultValue}
onChange={handleChange}

View File

@ -5,33 +5,38 @@ export function Modal({
trigger,
title,
content,
open
open,
onOpenChange
}: {
trigger: ReactNode;
title: string | ReactNode;
title?: string | ReactNode;
content: ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const [toggle, setToggle] = useState(open);
return (
<Dialog.Root open={toggle}>
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Trigger asChild>
{trigger}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/25 z-40" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white rounded-lg shadow-xl z-50 p-6">
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white rounded-lg shadow-xl z-50 p-6"
// onPointerDownOutside={(e) => e.preventDefault()}
>
<Dialog.Title className="text-xl font-bold text-center my-4">
{title}
</Dialog.Title>
{content}
{/* <div className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 cursor-pointer"
onClick={() => {setToggle(false)}}
<div className=" justify-center">
{content}
</div>
<div className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 cursor-pointer"
onClick={() => {onOpenChange?.(false)}}
>
Fermer
</div> */}
X
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View File

@ -9,7 +9,7 @@ import {
getFilteredRowModel,
Table as TableType,
} from "@tanstack/react-table"
import { cloneElement, isValidElement, ReactNode, useState } from "react";
import { cloneElement, isValidElement, ReactNode, useEffect, useRef, useState } from "react";
import { clsx, type ClassValue } from "clsx"
import Image from "next/image";
import { icons } from "#/assets/icons";
@ -53,6 +53,22 @@ export default function Table<TData, TValue>({
onColumnFiltersChange: setColumnFilters,
})
const headerCheckboxRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (headerCheckboxRef.current) {
headerCheckboxRef.current.indeterminate =
table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected();
}
console.log("SELECTED ALL = ", table.getSelectedRowModel().rows)
console.log("SELECTED = ", table.getRowModel().rows.filter((row) => row.getIsSelected()).map(row => row.original))
}, [
table.getIsSomePageRowsSelected(),
table.getIsAllPageRowsSelected(),
table.getRowModel()
]);
const totalPages = table.getPageCount()
const currentPage = table.getState().pagination.pageIndex + 1
@ -85,18 +101,17 @@ export default function Table<TData, TValue>({
<thead className="h-10">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="rounded-lg">
<th className="bg-blue-300 p-3 text-start first:rounded-tl-lg">
<input checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && undefined)
}
<th className="bg-[#E9F0FF] p-3 text-start first:rounded-tl-lg">
<input
ref={headerCheckboxRef}
checked={!!table.getIsAllPageRowsSelected()}
onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)}
type="checkbox" name="" id=""
/>
</th>
{headerGroup.headers.map((header) => {
return(
<th key={header.id} className="bg-blue-300 p-3 text-start last:rounded-tr-lg">
<th key={header.id} className="bg-[#E9F0FF] p-3 text-start last:rounded-tr-lg">
{flexRender(
header.column.columnDef.header,
header.getContext()

View File

@ -4,3 +4,10 @@ export const loginSchema = z.object({
email: z.string().email("Email invalide"),
password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères")
});
export const adminSchema = z.object({
id: z.string().optional(),
last_name: z.string(),
first_name: z.string(),
email: z.string().min(1, "L'email est requis").email("Email invalide"),
});

View File

@ -4,7 +4,7 @@ import { ZodSchema } from "zod";
export interface FloatingLabelInputProps {
label?: string;
placeholder?: string;
type: 'text' | 'password' | 'select' | 'email' | 'number';
type: 'text' | 'password' | 'select' | 'email' | 'number' | 'hidden';
options?: string[];
button?: React.ReactNode;
showPasswordToggle?: boolean;
@ -17,7 +17,7 @@ export interface FormProps {
title?: string,
fields: FloatingLabelInputProps[],
submit: (param: any) => unknown,
className: string,
className?: string,
child: ReactNode,
schema: ZodSchema
}
@ -52,3 +52,11 @@ export interface Owner {
email: string
last_name: string
}
export interface Admin {
id: string
email: string
first_name: string
last_name: string
profile: string
}