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

This commit is contained in:
Orace.A 2025-03-27 17:44:37 +01:00 committed by Ruben
parent c0f022dc10
commit d56e33ece0
10 changed files with 405 additions and 108 deletions

View File

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

14
pnpm-lock.yaml generated
View File

@ -56,6 +56,9 @@ importers:
sass: sass:
specifier: ^1.86.0 specifier: ^1.86.0
version: 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: zod:
specifier: ^3.24.2 specifier: ^3.24.2
version: 3.24.2 version: 3.24.2
@ -3468,6 +3471,12 @@ packages:
snake-case@3.0.4: snake-case@3.0.4:
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} 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: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -7392,6 +7401,11 @@ snapshots:
dot-case: 3.0.4 dot-case: 3.0.4
tslib: 2.8.1 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: {} source-map-js@1.2.1: {}
stable-hash@0.0.5: {} stable-hash@0.0.5: {}

View File

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

View File

@ -3,22 +3,27 @@
import FloatingLabelInput from "#/components/floatingLabelInput" import FloatingLabelInput from "#/components/floatingLabelInput"
import { Modal } from "#/components/modal" import { Modal } from "#/components/modal"
import Table from "#/components/table/table" 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 { ColumnDef } from "@tanstack/react-table"
import axios from "axios" import axios from "axios"
import { table } from "console" import Image from "next/image"
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import { DropdownMenu } from "radix-ui"
export interface Admin { import Link from "next/link"
id: string import { icons } from "#/assets/icons"
email: string import { useState } from "react"
first_name: string import Form from "#/components/form/form"
last_name: string import { adminSchema } from "#/schema"
profile: string import { Admin } from "#/types"
}
export default function Admins (){ export default function Admins (){
const {data: session, status} = useSession() 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({ const { data: users, refetch, isLoading} = useQuery({
enabled: status === 'authenticated', 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>[] = [ const columns: ColumnDef<Admin>[] = [
{ {
header: "Administrateurs", header: "Administrateurs",
@ -90,52 +224,127 @@ export default function Admins (){
// ) // )
// } // }
// }, // },
// { {
// id: "delete", id: "delete",
// cell: ({ cell }) => { cell: ({ cell }) => {
// const id = String(cell.row.original.id) const admin = cell.row.original
// return ( return (
// <div className="relative p-2 cursor-pointer" <div className="flex justify-end p-2 cursor-pointer space-x-2"
// // onClick={() => { mutate(id) }} // onClick={() => { mutate(id) }}
// > >
// <Modal
// open={open} <Modal
// trigger={ open={openEditModal && selectedAdminId === admin.id}
// <div onClick={() => setOpen(true)}> onOpenChange={(isOpen) => {
// <Image alt="" src={icons.trash} className="absolute right-2 top-[-50%] transform translate-middle-y hover:text-blue-500" /> if (!isOpen) {
// </div> setSelectedAdminId(null);
// } setOpenEditModal(false);
// title={ }
// <p className="font-bold text-3xl">Supprimer une organisation</p> }}
// } trigger={
// content={ <div onClick={() =>{
// <div> setSelectedAdminId(admin.id);
// <p>Voulez-vous vraiment supprimer cette organisation ?</p> 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"> <div className="grid grid-cols-2 gap-3 mt-3">
// <button <button
// className="bg-blue-100 text-blue-600 py-2 px-4 text-lg rounded-full text-center hover:bg-blue-200" className="bg-blue-100 text-blue-600 py-2 px-4 text-lg rounded-full text-center hover:bg-blue-200"
// onClick={() => { setOpen(false) }} onClick={() => { setOpenDeleteModal(false) }}
// > >
// Annuler Annuler
// </button> </button>
// <button <button
// className="bg-red-500 text-white py-2 px-4 text-lg rounded-full text-center hover:bg-red-600" className="bg-red-500 text-white py-2 px-4 text-lg rounded-full text-center hover:bg-red-600"
// onClick={() => { onClick={() => {
// mutate(id) mutate(admin.id)
// setOpen(false) setOpenDeleteModal(false)
// }} }}
// > >
// Supprimer Supprimer
// </button> </button>
// </div> </div>
// </div> </div>
// } }
// /> />
// </div> </div>
// ) )
// } }
// } }
] ]
return ( return (
@ -144,30 +353,82 @@ export default function Admins (){
columns={columns} columns={columns}
data={users || []} data={users || []}
pageSize={5} pageSize={5}
header={(table) => ( header={(table) => {
<div className="grid grid-cols-1 md:grid-cols-2 md:flex-row w-full"> const ids = table.getRowModel().rows.filter((row) => row.getIsSelected()).map(row => row.original.id )
<div className="flex"> if(bulkDeleteMutation.isSuccess) {
<input checked={ table.toggleAllPageRowsSelected(false)
table.getIsAllPageRowsSelected() || }
(table.getIsSomePageRowsSelected() && undefined) return (
} <>
onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)} <div className="grid grid-cols-1 md:grid-cols-2 md:flex-row w-full">
type="checkbox" name="" id="" <div className="flex items-center space-x-3">
/> <input checked={table.getIsAllPageRowsSelected() ||
</div> (table.getIsSomePageRowsSelected() && undefined)}
onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)}
type="checkbox" name="" id="" />
<div className="flex justify-between md:justify-end space-x-3"> <DropdownMenu.Root open={open} onOpenChange={setOpen}>
<Modal <DropdownMenu.Trigger asChild>
trigger={ <p className="cursor-pointer rounded-full bg-gray-300 text-gray-500 p-2">
<button className="p-3 bg-blue-600 text-white rounded-full"> Sélectionner une action
Ajouter un admin </p>
</button> </DropdownMenu.Trigger>
}
/> <DropdownMenu.Portal>
<FloatingLabelInput name="search" placeholder="Effectuer une recherche" type="text" onChange={(value) => table.setGlobalFilter(value)} /> <DropdownMenu.Content className="min-w-[150px] shadow-sm bg-white rounded-md p-1" sideOffset={5}>
</div> <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">
</div> 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 <Modal
open={open} open={open}
onOpenChange={(isOpen)=>{
if(!isOpen) {
setOpen(isOpen)
}
}}
trigger={ trigger={
<div onClick={() => setOpen(true)}> <div onClick={() => setOpen(true)}>
<Image alt="" src={icons.trash} className="absolute right-2 top-[-50%] transform translate-middle-y hover:text-blue-500" /> <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 ( return (
<div className="relative w-full"> <div className="relative w-full">
<select <select
className="input-form focus:ring-2 focus:ring-blue-500" className="input-form focus:ring-2 focus:ring-blue-500 outline-none"
required
name={name} name={name}
defaultValue={defaultValue} defaultValue={defaultValue}
> >
@ -52,9 +51,8 @@ export default function FloatingLabelInput({
name={name} name={name}
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder={placeholder} 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} defaultValue={defaultValue}
required
/> />
{showPasswordToggle && ( {showPasswordToggle && (
@ -89,8 +87,7 @@ export default function FloatingLabelInput({
<input <input
type={type} type={type}
placeholder={placeholder} placeholder={placeholder}
className="floating-label input-form focus:ring-2 focus:ring-blue-500" className="input-form focus:ring-2 focus:ring-blue-500 outline-none"
required
name={name} name={name}
defaultValue={defaultValue} defaultValue={defaultValue}
onChange={handleChange} onChange={handleChange}

View File

@ -5,33 +5,38 @@ export function Modal({
trigger, trigger,
title, title,
content, content,
open open,
onOpenChange
}: { }: {
trigger: ReactNode; trigger: ReactNode;
title: string | ReactNode; title?: string | ReactNode;
content: ReactNode; content: ReactNode;
open?: boolean; open?: boolean;
onOpenChange?: (open: boolean) => void;
}) { }) {
const [toggle, setToggle] = useState(open);
return ( return (
<Dialog.Root open={toggle}> <Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
{trigger} {trigger}
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/25 z-40" /> <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"> <Dialog.Title className="text-xl font-bold text-center my-4">
{title} {title}
</Dialog.Title> </Dialog.Title>
{content}
<div className=" justify-center">
{content}
</div>
{/* <div className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 cursor-pointer" <div className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 cursor-pointer"
onClick={() => {setToggle(false)}} onClick={() => {onOpenChange?.(false)}}
> >
Fermer X
</div> */} </div>
</Dialog.Content> </Dialog.Content>
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>

View File

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

View File

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