diff --git a/package.json b/package.json index 860e495..9d131ca 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77fa7fc..3fab4d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index e89ffda..dc7659f 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -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) diff --git a/src/app/admin/admins/page.tsx b/src/app/admin/admins/page.tsx index c47a42a..234ae73 100644 --- a/src/app/admin/admins/page.tsx +++ b/src/app/admin/admins/page.tsx @@ -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(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[] = [ { header: "Administrateurs", @@ -90,52 +224,127 @@ export default function Admins (){ // ) // } // }, - // { - // id: "delete", - // cell: ({ cell }) => { - // const id = String(cell.row.original.id) - // return ( - //
{ mutate(id) }} - // > - // setOpen(true)}> - // - //
- // } - // title={ - //

Supprimer une organisation

- // } - // content={ - //
- //

Voulez-vous vraiment supprimer cette organisation ?

+ { + id: "delete", + cell: ({ cell }) => { + const admin = cell.row.original + return ( +
{ mutate(id) }} + > + + { + if (!isOpen) { + setSelectedAdminId(null); + setOpenEditModal(false); + } + }} + trigger={ +
{ + setSelectedAdminId(admin.id); + setOpenEditModal(true) + }}> + +
+ } + title={ +

Modifier un admin

+ } + content={ + <> +
setOpenModal(false)} type="submit" className="btn-auth">{isLoading ? "Chargement..." : "Modifier"}} + /> + } + + /> + + { + if (!isOpen) { + setSelectedAdminId(null); + setOpenDeleteModal(false); + } + }} + trigger={ +
{ + setSelectedAdminId(admin.id); + setOpenDeleteModal(true) + }}> + +
+ } + title={ +

Supprimer un admin

+ } + content={ +
+

Voulez-vous vraiment supprimer cet admin ?

- //
- // - // - //
- //
- // } - // /> - //
- // ) - // } - // } +
+ + +
+
+ } + /> + + ) + } + } ] return ( @@ -144,30 +353,82 @@ export default function Admins (){ columns={columns} data={users || []} pageSize={5} - header={(table) => ( -
-
- table.toggleAllPageRowsSelected(e.target.checked)} - type="checkbox" name="" id="" - /> -
+ header={(table) => { + const ids = table.getRowModel().rows.filter((row) => row.getIsSelected()).map(row => row.original.id ) + if(bulkDeleteMutation.isSuccess) { + table.toggleAllPageRowsSelected(false) + } + return ( + <> +
+
+ table.toggleAllPageRowsSelected(e.target.checked)} + type="checkbox" name="" id="" /> -
- - Ajouter un admin - - } - /> - table.setGlobalFilter(value)} /> -
-
- )} + + +

+ Sélectionner une action +

+
+ + + + 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 + + + +
+
+ +
+ setOpenModal(true)} className="cursor-pointer p-3 bg-blue-600 text-white rounded-full"> + Ajouter un admin +
} + content={ + <> + {isLoading ? "Chargement..." : "Créer le compte"}} + /> + + } + + /> + + table.setGlobalFilter(value)} /> +
+ + + ) + }} /> ) diff --git a/src/app/admin/home/page.tsx b/src/app/admin/home/page.tsx index 8dc9680..552aff0 100644 --- a/src/app/admin/home/page.tsx +++ b/src/app/admin/home/page.tsx @@ -135,6 +135,11 @@ export default function HomePage () { > { + if(!isOpen) { + setOpen(isOpen) + } + }} trigger={
setOpen(true)}> diff --git a/src/components/floatingLabelInput.tsx b/src/components/floatingLabelInput.tsx index 69df68d..be0b126 100644 --- a/src/components/floatingLabelInput.tsx +++ b/src/components/floatingLabelInput.tsx @@ -32,8 +32,7 @@ export default function FloatingLabelInput({ return (
void; }) { - const [toggle, setToggle] = useState(open); - return ( - + {trigger} - + e.preventDefault()} + > {title} - {content} + +
+ {content} +
- {/*
{setToggle(false)}} +
{onOpenChange?.(false)}} > - Fermer -
*/} + X +
diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index 7c4f01a..70a6973 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -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({ onColumnFiltersChange: setColumnFilters, }) + const headerCheckboxRef = useRef(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({ {table.getHeaderGroups().map((headerGroup) => ( - - + table.toggleAllPageRowsSelected(e.target.checked)} type="checkbox" name="" id="" /> {headerGroup.headers.map((header) => { return( - + {flexRender( header.column.columnDef.header, header.getContext() diff --git a/src/schema/loginSchema.ts b/src/schema/index.ts similarity index 50% rename from src/schema/loginSchema.ts rename to src/schema/index.ts index e32d3aa..b28bb6a 100644 --- a/src/schema/loginSchema.ts +++ b/src/schema/index.ts @@ -3,4 +3,11 @@ import { z } from "zod"; 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"), }); \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index be3c440..7993690 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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 } @@ -51,4 +51,12 @@ export interface Owner { first_name: string email: string last_name: string +} + +export interface Admin { + id: string + email: string + first_name: string + last_name: string + profile: string } \ No newline at end of file