From c0f022dc10c201223957876da851cf6834866bf3 Mon Sep 17 00:00:00 2001 From: "Orace.A" Date: Thu, 27 Mar 2025 00:04:02 +0100 Subject: [PATCH 01/12] feat: upadate data-table component by adding dynamic header component --- package.json | 1 + pnpm-lock.yaml | 38 ++++++ src/app/admin/admins/page.tsx | 166 ++++++++++++++++++++++++++ src/app/admin/home/page.tsx | 16 ++- src/components/floatingLabelInput.tsx | 13 +- src/components/table/table.tsx | 24 +++- src/types/index.ts | 4 +- 7 files changed, 251 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 55d17f4..860e495 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/themes": "^3.2.1", "@tanstack/react-query": "^5.69.0", "@tanstack/react-table": "^8.21.2", "axios": "^1.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8724c4..77fa7fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/themes': + specifier: ^3.2.1 + version: 3.2.1(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tanstack/react-query': specifier: ^5.69.0 version: 5.69.0(react@19.0.0) @@ -1012,6 +1015,9 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@radix-ui/colors@3.0.0': + resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} + '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -1658,6 +1664,19 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@radix-ui/themes@3.2.1': + resolution: {integrity: sha512-WJL2YKAGItkunwm3O4cLTFKCGJTfAfF6Hmq7f5bCo1ggqC9qJQ/wfg/25AAN72aoEM1yqXZQ+pslsw48AFR0Xg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: 16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: 16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -2128,6 +2147,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -4685,6 +4707,8 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@radix-ui/colors@3.0.0': {} + '@radix-ui/number@1.1.0': {} '@radix-ui/primitive@1.1.1': {} @@ -5380,6 +5404,18 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@radix-ui/themes@3.2.1(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/colors': 3.0.0 + classnames: 2.5.1 + radix-ui: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll-bar: 2.3.8(@types/react@19.0.12)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + '@types/react-dom': 19.0.4(@types/react@19.0.12) + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.11.0': {} @@ -5893,6 +5929,8 @@ snapshots: dependencies: readdirp: 4.1.2 + classnames@2.5.1: {} + client-only@0.0.1: {} clsx@2.1.1: {} diff --git a/src/app/admin/admins/page.tsx b/src/app/admin/admins/page.tsx index 3c50529..c47a42a 100644 --- a/src/app/admin/admins/page.tsx +++ b/src/app/admin/admins/page.tsx @@ -1,8 +1,174 @@ +"use client" +import FloatingLabelInput from "#/components/floatingLabelInput" +import { Modal } from "#/components/modal" +import Table from "#/components/table/table" +import { useQuery } from "@tanstack/react-query" +import { ColumnDef } from "@tanstack/react-table" +import axios from "axios" +import { table } from "console" +import { useSession } from "next-auth/react" + +export interface Admin { + id: string + email: string + first_name: string + last_name: string + profile: string +} export default function Admins (){ + const {data: session, status} = useSession() + + const { data: users, refetch, isLoading} = useQuery({ + enabled: status === 'authenticated', + queryKey: ["users"], + queryFn: async () => { + try { + const response = await axios.get( + 'https://private-docs-api.intside.co/users', { + headers: { + 'Authorization': `Bearer ${session?.user.access_token}` + } + } + ) + + if(response.data) { + return response.data.data as Admin[] + } + } catch (error) { + console.error(error) + } + } + }) + + const columns: ColumnDef[] = [ + { + header: "Administrateurs", + cell: ({ row }) => { + const value = String(row.original.first_name) + " " + String(row.original.last_name) + const initials = String(row.original.first_name[0]) + String(row.original.last_name[0]) + return( +
+
+ {initials} +
+

{value}

+
+ ) + } + }, + { + accessorKey: "email", + header: "Adresse e-mail" + }, + // { + // accessorKey: "status", + // header: "Statut", + // cell: ({ cell }) => { + // const status = String(cell.getValue()) + // return ( + //

+ // { + // status === "active" ? "Actif" : + // status === "inactive" ? "Inactif" : + // status === "pending" ? "En attente" : + // status === "blocked" ? "Bloquée" : + // "" + // } + //

+ // ) + // } + // }, + // { + // 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 ?

+ + //
+ // + // + //
+ //
+ // } + // /> + // + // ) + // } + // } + ] + return ( <> + ( +
+
+ table.toggleAllPageRowsSelected(e.target.checked)} + type="checkbox" name="" id="" + /> +
+ +
+ + Ajouter un admin + + } + /> + table.setGlobalFilter(value)} /> +
+
+ )} + /> ) } \ No newline at end of file diff --git a/src/app/admin/home/page.tsx b/src/app/admin/home/page.tsx index 550c177..8dc9680 100644 --- a/src/app/admin/home/page.tsx +++ b/src/app/admin/home/page.tsx @@ -74,16 +74,22 @@ export default function HomePage () { accessorKey: "name", header: "Organisations", }, - // { - // accessorKey: "Utilisateurs", - // header: "Utilisateurs", - // }, + { + accessorKey: "total_users", + header: "Utilisateurs", + }, { header: "Administrateurs", cell: ({ row }) => { const value = String(row.original.owner.first_name) + " " + String(row.original.owner.last_name) + const initials = String(row.original.owner.first_name[0]) + String(row.original.owner.last_name[0]) return( -

{value}

+
+
+ {initials} +
+

{value}

+
) } }, diff --git a/src/components/floatingLabelInput.tsx b/src/components/floatingLabelInput.tsx index d11e439..5c5730b 100644 --- a/src/components/floatingLabelInput.tsx +++ b/src/components/floatingLabelInput.tsx @@ -13,10 +13,19 @@ export default function FloatingLabelInput({ button, showPasswordToggle = false, name, - defaultValue + defaultValue, + onChange }: FloatingLabelInputProps) { const [showPassword, setShowPassword] = useState(false); + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (onChange) { + onChange(value); + } + }; + + const renderInput = () => { switch(type) { case 'select': @@ -46,6 +55,7 @@ export default function FloatingLabelInput({ className="input-form focus:ring-2 focus:ring-blue-500 pr-10" defaultValue={defaultValue} required + /> {showPasswordToggle && (
diff --git a/src/types/index.ts b/src/types/index.ts index 1c591fc..50124c1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,7 +2,7 @@ import { FormEventHandler, ReactNode } from "react"; import { ZodSchema } from "zod"; export interface FloatingLabelInputProps { - label: string; + label?: string; placeholder?: string; type: 'text' | 'password' | 'select' | 'email' | 'number'; options?: string[]; @@ -10,6 +10,7 @@ export interface FloatingLabelInputProps { showPasswordToggle?: boolean; name: string; defaultValue?: string; + onChange?: (value: string) => void; } export interface FormProps { @@ -42,6 +43,7 @@ export interface Company { is_premium: boolean status: string owner: Owner + total_users: number } export interface Owner { From d56e33ece0edff053114e4c3472b6a0be2e1616f Mon Sep 17 00:00:00 2001 From: "Orace.A" Date: Thu, 27 Mar 2025 17:44:37 +0100 Subject: [PATCH 02/12] feat: add bulkDelete, tablefilter, rowEdit modal, add Admin modal --- package.json | 1 + pnpm-lock.yaml | 14 + src/app/(auth)/login/page.tsx | 7 +- src/app/admin/admins/page.tsx | 417 +++++++++++++++++++----- src/app/admin/home/page.tsx | 5 + src/components/floatingLabelInput.tsx | 9 +- src/components/modal.tsx | 27 +- src/components/table/table.tsx | 29 +- src/schema/{loginSchema.ts => index.ts} | 0 src/types/index.ts | 4 +- 10 files changed, 405 insertions(+), 108 deletions(-) rename src/schema/{loginSchema.ts => index.ts} (100%) 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 5c5730b..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) => ( - {headerGroup.headers.map((header) => { return( -
- + table.toggleAllPageRowsSelected(e.target.checked)} type="checkbox" name="" id="" /> + {flexRender( header.column.columnDef.header, header.getContext() diff --git a/src/schema/loginSchema.ts b/src/schema/index.ts similarity index 100% rename from src/schema/loginSchema.ts rename to src/schema/index.ts diff --git a/src/types/index.ts b/src/types/index.ts index 50124c1..99add43 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 } From f860fed456ffda668d54763e366177185cb1b196 Mon Sep 17 00:00:00 2001 From: "Orace.A" Date: Thu, 27 Mar 2025 18:21:14 +0100 Subject: [PATCH 03/12] feat: enhance search input --- src/app/admin/admins/page.tsx | 7 +++++-- src/app/globals.css | 11 +++++++++-- src/components/floatingLabelInput.tsx | 27 +++++++++++++++++++++++---- src/types/index.ts | 2 +- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/app/admin/admins/page.tsx b/src/app/admin/admins/page.tsx index 234ae73..2a54fa2 100644 --- a/src/app/admin/admins/page.tsx +++ b/src/app/admin/admins/page.tsx @@ -9,7 +9,6 @@ import axios from "axios" import Image from "next/image" import { useSession } from "next-auth/react" 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" @@ -423,7 +422,11 @@ export default function Admins (){ /> - table.setGlobalFilter(value)} /> + table.setGlobalFilter(value)} + button={ + + } + /> diff --git a/src/app/globals.css b/src/app/globals.css index 27f84be..6848057 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -57,9 +57,16 @@ body { z-index: 1; } -.btn-floating { +.btn-floating-right { position: absolute; - right: 12px; + right: 8px; + top: 50%; + transform: translateY(-50%); +} + +.btn-floating-left { + position: absolute; + left: 8px; top: 50%; transform: translateY(-50%); } diff --git a/src/components/floatingLabelInput.tsx b/src/components/floatingLabelInput.tsx index be0b126..7cc4593 100644 --- a/src/components/floatingLabelInput.tsx +++ b/src/components/floatingLabelInput.tsx @@ -40,7 +40,7 @@ export default function FloatingLabelInput({ ))} - {button &&
{button}
} + {button &&
{button}
} ); @@ -59,7 +59,7 @@ export default function FloatingLabelInput({ } - /> - } - - /> - - { - if (!isOpen) { - setSelectedAdminId(null); - setOpenDeleteModal(false); - } - }} - trigger={ -
{ - setSelectedAdminId(admin.id); - setOpenDeleteModal(true) - }}> - -
- } - title={ -

Supprimer un admin

- } - content={ -
-

Voulez-vous vraiment supprimer cet admin ?

- -
- - -
-
- } - /> - - ) - } - } - ] - - return ( - <> - { - 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="" /> - - - -

- 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)} - button={ - - } + setOpenEditModal(true); + }} + > +
- - - ) - }} - /> - - ) -} \ No newline at end of file + } + title="Modifier un admin" + content={ + + Modifier + + } + /> + } + /> + + {/* Modal de suppression */} + { + if (!isOpen) { + setSelectedAdminId(null); + setOpenDeleteModal(false); + } + }} + trigger={ +
{ + setSelectedAdminId(admin.id); + setOpenDeleteModal(true); + }} + > + +
+ } + title="Supprimer un admin" + content={ +
+

Voulez-vous vraiment supprimer cet admin ?

+
+ + +
+
+ } + /> + + ); + }, + }, + ]; + + return ( +
{ + const selectedIds = table + .getRowModel() + .rows.filter((row) => row.getIsSelected()) + .map((row) => row.original.id); + + return ( +
+
+ + table.toggleAllPageRowsSelected(e.target.checked) + } + /> + + + +

+ Sélectionner une action +

+
+ + + + bulkDeleteMutation.mutate(selectedIds)} + className="p-2 text-[14px] cursor-pointer hover:bg-blue-100 hover:text-blue-500 rounded-md" + > + Supprimer + + + +
+
+ +
+ {/* Modal d'ajout */} + { + if (!isOpen) { + setOpen(false); + } + }} + trigger={ +
setOpenModal(true)} + className="cursor-pointer p-3 bg-blue-600 text-white rounded-full" + > + Ajouter un admin +
+ } + content={ + + Créer le compte + + } + /> + } + /> + + table.setGlobalFilter(value)} + button={ + + } + /> +
+
+ ); + }} + /> + ); +} diff --git a/src/app/admin/home/page.tsx b/src/app/admin/home/page.tsx index 552aff0..bc69bc2 100644 --- a/src/app/admin/home/page.tsx +++ b/src/app/admin/home/page.tsx @@ -55,6 +55,7 @@ export default function HomePage () { if(response.status === 200 || response.status === 201) { console.log('Suppresion réussie !') + setOpen(false) } } catch (error) { console.error(error) diff --git a/src/components/floatingLabelInput.tsx b/src/components/floatingLabelInput.tsx index 7cc4593..0ce83cb 100644 --- a/src/components/floatingLabelInput.tsx +++ b/src/components/floatingLabelInput.tsx @@ -14,7 +14,7 @@ export default function FloatingLabelInput({ showPasswordToggle = false, name, defaultValue, - onChange + onChange, }: FloatingLabelInputProps) { const [showPassword, setShowPassword] = useState(false); diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index 4dd8e82..6a44dfe 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -5,71 +5,71 @@ import { FormProps } from "#/types" import { FormEvent, useState } from "react" export default function Form({ - fields, - submit, - className, - child, - title, - schema -}: FormProps) { - const [errors, setErrors] = useState>({}); + fields, + submit, + className, + child, + title, + schema +} : FormProps) { + const [errors, setErrors] = useState>({}); - const handleSubmit = (e: FormEvent) => { - e.preventDefault() + const handleSubmit = (e: FormEvent) => { + e.preventDefault() - const formData = new FormData(e.currentTarget) - const data = Object.fromEntries(formData) + const formData = new FormData(e.currentTarget) + const data = Object.fromEntries(formData) - console.log("FORM DATA = ", data) - const result = schema.safeParse(data); - console.log("ZOD = ", result.error?.format()) + console.log("FORM DATA = ", data) + const result = schema.safeParse(data); + console.log("ZOD = ", result.error?.format()) - if (!result.success) { - const formatedErrors = result.error.format() as Record; + if(!result.success) { + const formatedErrors = result.error.format() as Record; - const newErrors: Record = {}; - Object.keys(formatedErrors).forEach((field) => { - if (field !== "_errors" && formatedErrors[field]._errors?.length) { - newErrors[field] = formatedErrors[field]._errors[0]; + const newErrors: Record = {}; + Object.keys(formatedErrors).forEach((field) => { + if (field !== "_errors" && formatedErrors[field]._errors?.length) { + newErrors[field] = formatedErrors[field]._errors[0]; + } + }); + + setErrors(newErrors) + } else { + setErrors({}) + submit(result.data) } - }); - - setErrors(newErrors) - } else { - setErrors({}) - submit(result.data) } - } - return ( - -
-

{title}

-
- -
- - { - fields.map((item, index) => ( -
- - - {errors[item.name]} + return ( + +
+

{title}

- )) - } - {child} -
- - ) +
+ + { + fields.map((item, index) => ( +
+ + + {errors[item.name]} +
+ )) + } + {child} +
+ + + ) } \ No newline at end of file From 0f8267a9848fa5ce16c4933aadeeab7a2221f0dc Mon Sep 17 00:00:00 2001 From: "Orace.A" Date: Thu, 27 Mar 2025 21:24:50 +0100 Subject: [PATCH 05/12] feat: add organizations data-table page --- src/app/admin/admins/page.tsx | 2 +- src/app/admin/organizations/page.tsx | 2 -- src/components/floatingLabelInput.tsx | 2 +- src/schema/index.ts | 10 ++++++++-- src/types/index.ts | 8 ++++++-- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/app/admin/admins/page.tsx b/src/app/admin/admins/page.tsx index 5bf52e1..05df663 100644 --- a/src/app/admin/admins/page.tsx +++ b/src/app/admin/admins/page.tsx @@ -348,7 +348,7 @@ export default function Admins() { open={openModal} onOpenChange={(isOpen) => { if (!isOpen) { - setOpen(false); + setOpenModal(false); } }} trigger={ diff --git a/src/app/admin/organizations/page.tsx b/src/app/admin/organizations/page.tsx index 6a23f0d..a5fc92e 100644 --- a/src/app/admin/organizations/page.tsx +++ b/src/app/admin/organizations/page.tsx @@ -1,10 +1,8 @@ -import Link from "next/link"; export default function Organizations (){ return ( <> - Organization Profile ) } \ No newline at end of file diff --git a/src/components/floatingLabelInput.tsx b/src/components/floatingLabelInput.tsx index 0ce83cb..b9f87fb 100644 --- a/src/components/floatingLabelInput.tsx +++ b/src/components/floatingLabelInput.tsx @@ -37,7 +37,7 @@ export default function FloatingLabelInput({ defaultValue={defaultValue} > {options?.map((option, index) => ( - + ))} {button &&
{button}
} diff --git a/src/schema/index.ts b/src/schema/index.ts index 4d4c298..4570ae1 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -10,5 +10,11 @@ export const adminSchema = z.object({ last_name: z.string(), first_name: z.string(), email: z.string().min(1, "L'email est requis").email("Email invalide"), - organization: z.string().optional(), -}); \ No newline at end of file +}); + +export const companySchema = z.object({ + name: z.string().min(1, "Le nom est requis"), + description: z.string(), + status: z.string(), + owner: z.string() +}) diff --git a/src/types/index.ts b/src/types/index.ts index 4733e04..bc1ac0b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,11 +1,15 @@ import { FormEventHandler, ReactNode } from "react"; import { ZodSchema } from "zod"; +export interface Option { + label: string + value: string +} export interface FloatingLabelInputProps { label?: string; placeholder?: string; - type: 'text' | 'password' | 'select' | 'email' | 'number' | 'hidden' | 'search'; - options?: string[]; + type: 'text' | 'password' | 'select' | 'email' | 'number' | 'hidden' | 'search' | 'textarea'; + options?: Option[]; button?: React.ReactNode; showPasswordToggle?: boolean; name: string; From 0420b37cc9a2a61bde55bd2e42b9184280ba6dd4 Mon Sep 17 00:00:00 2001 From: "Orace.A" Date: Thu, 27 Mar 2025 22:13:44 +0100 Subject: [PATCH 06/12] feat: add link to organization details in data-table --- src/app/admin/organizations/page.tsx | 303 +++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) diff --git a/src/app/admin/organizations/page.tsx b/src/app/admin/organizations/page.tsx index a5fc92e..8341444 100644 --- a/src/app/admin/organizations/page.tsx +++ b/src/app/admin/organizations/page.tsx @@ -1,4 +1,307 @@ +import { useState } from "react"; +import Image from "next/image"; +import { useSession } from "next-auth/react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { ColumnDef } from "@tanstack/react-table"; +import axios from "axios"; +import { DropdownMenu } from "radix-ui"; +import FloatingLabelInput from "#/components/floatingLabelInput"; +import { Modal } from "#/components/modal"; +import Table from "#/components/table/table"; +import Form from "#/components/form/form"; +import { icons } from "#/assets/icons"; +import { adminSchema, companySchema } from "#/schema"; +import { Admin, Company } from "#/types"; +import Link from "next/link"; + +export default function Organizations() { + 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: companies, + refetch, + isLoading, + } = useQuery({ + enabled: status === "authenticated", + queryKey: ["companies"], + queryFn: async () => { + try { + const response = await axios.get( + "https://private-docs-api.intside.co/companies", + { + headers: { + Authorization: `Bearer ${session?.user.access_token}`, + }, + } + ); + + if (response.data) { + return response.data.data as Company[]; + } + } catch (error) { + console.error(error); + } + }, + }); + + const { + data: users, + } = useQuery({ + enabled: status === "authenticated", + queryKey: ["organizations"], + queryFn: async () => { + try { + const response = await axios.get( + "https://private-docs-api.intside.co/users", + { + headers: { Authorization: `Bearer ${session?.user.access_token}` }, + } + ); + return response.data.data.map((user: Admin) => ({ + id: user.id, + name: `${user.first_name} ${user.last_name}`, + })); + } catch (error) { + console.error(error); + return []; + } + }, + }); + + const createMutation = useMutation({ + mutationFn: async (data: { + name: string; + description: string; + status: string; + is_premium: string; + owner: string; + }) => { + try { + const response = await axios.post( + "https://private-docs-api.intside.co/companies/", + data, + { headers: { Authorization: `Bearer ${session?.user.access_token}` } } + ); + + if (response.status === 200 || response.status === 201) { + console.log("ajout réussie !"); + setOpenModal(false); + } + } catch (error) { + console.error("Erreur lors de la création", error); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["organizations"] }); + refetch(); + }, + }); + + const updateMutation = useMutation({ + mutationFn: async (data: { + id: string; + last_name: string; + first_name: string; + email: string; + }) => { + try { + const response = await axios.put( + `https://private-docs-api.intside.co/companies/${data.id}/`, + data, + { headers: { Authorization: `Bearer ${session?.user.access_token}` } } + ); + + if (response.status === 200 || response.status === 201) { + console.log("modification réussie !"); + setOpenEditModal(false); + } + } catch (error) { + console.error("Erreur lors de la mise à jour", error); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["organizations"] }); + refetch(); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + try { + const response = await axios.delete( + `https://private-docs-api.intside.co/companies/${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("Erreur lors de la suppression", error); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["organizations"] }); + refetch(); + }, + }); + + const bulkDeleteMutation = useMutation({ + mutationFn: async (ids: string[]) => { + try { + const deletePromises = ids.map((id) => + axios.delete(`https://private-docs-api.intside.co/companies/${id}/`, { + headers: { Authorization: `Bearer ${session?.user.access_token}` }, + }) + ); + await Promise.all(deletePromises); + } catch (error) { + console.error("Erreur lors de la suppression groupée", error); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["organizations"] }); + refetch(); + }, + }); + + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: "Organisations", + cell: ({ row }) => { + const company = row.original; + return ( + +

{company.name}

+ + ); + }, + }, + { + accessorKey: "total_users", + header: "Utilisateurs", + }, + { + header: "Administrateurs", + cell: ({ row }) => { + const value = String(row.original.owner.first_name) + " " + String(row.original.owner.last_name) + const initials = String(row.original.owner.first_name[0]) + String(row.original.owner.last_name[0]) + return( +
+
+ {initials} +
+

{value}

+
+ ) + } + }, + { + accessorKey: "owner.email", + header: "Adresse e-mail" + }, + { + accessorKey: "status", + header: "Statut", + cell: ({ cell }) => { + const status = String(cell.getValue()) + return ( +

+ { + status === "active" ? "Actif" : + status === "inactive" ? "Inactif" : + status === "pending" ? "En attente" : + status === "blocked" ? "Bloquée" : + "" + } +

+ ) + } + }, + { + id: "delete", + cell: ({ row }) => { + const company = row.original; + return ( +
{ mutate(id) }} + > + + {/* Modal de suppression */} + { + if (!isOpen) { + setSelectedAdminId(null); + setOpenDeleteModal(false); + } + }} + trigger={ +
{ + setSelectedAdminId(company.id); + setOpenDeleteModal(true); + }} + > + +
+ } + title="Supprimer une organisation" + content={ +
+

Voulez-vous vraiment supprimer cette organisation ?

+
+ + +
+
+ } + /> +
+ ) + } + } + ] export default function Organizations (){ return ( From 14483200eac8df17fa320fec1b27840a26e0b6af Mon Sep 17 00:00:00 2001 From: "Orace.A" Date: Fri, 28 Mar 2025 12:32:42 +0100 Subject: [PATCH 07/12] feat: enhance login redirection and add loading state to tables --- src/app/(auth)/login/page.tsx | 13 +- src/app/admin/admins/page.tsx | 1 + src/app/admin/home/page.tsx | 1 + src/app/admin/layout.tsx | 17 ++- src/app/admin/organizations/page.tsx | 179 +++++++++++++++++++++------ src/components/admin/adminHeader.tsx | 4 +- src/components/form/form.tsx | 9 +- src/components/table/table.tsx | 25 +++- src/lib/function.ts | 11 ++ src/types/index.ts | 3 +- 10 files changed, 209 insertions(+), 54 deletions(-) create mode 100644 src/lib/function.ts diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index dc7659f..d7e2105 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -4,10 +4,11 @@ import Form from "#/components/form/form" import { loginSchema } from "#/schema" import { useMutation } from "@tanstack/react-query" import { signIn } from "next-auth/react" -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; export default function LoginPage() { const router = useRouter() + const params = useSearchParams().get("redirect_to"); const mutation = useMutation({ mutationKey: ['login'], @@ -26,7 +27,11 @@ export default function LoginPage() { console.error(errorMessage) throw new Error(result.error) } else { - router.push('/admin/home') + if (params) { + router.push(params); + } else { + router.push('/admin/home') + } } return result } catch (error: any) { @@ -47,7 +52,7 @@ export default function LoginPage() {
Connexion} + child={} />
) diff --git a/src/app/admin/admins/page.tsx b/src/app/admin/admins/page.tsx index 05df663..88219a5 100644 --- a/src/app/admin/admins/page.tsx +++ b/src/app/admin/admins/page.tsx @@ -298,6 +298,7 @@ export default function Admins() { columns={columns} data={users || []} pageSize={5} + isDataLoading={isLoading} header={(table) => { const selectedIds = table .getRowModel() diff --git a/src/app/admin/home/page.tsx b/src/app/admin/home/page.tsx index bc69bc2..879891a 100644 --- a/src/app/admin/home/page.tsx +++ b/src/app/admin/home/page.tsx @@ -187,6 +187,7 @@ export default function HomePage () {
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 15e2217..e4e34ec 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,9 +1,24 @@ -import { ReactNode } from "react"; +"use client" + +import { ReactNode, useEffect } from "react"; import "../../assets/css/admin.css" import Sidebar from "../../components/admin/sidebar"; import Header from "../../components/admin/adminHeader"; +import { signOutFunc } from "#/lib/function"; +import { usePathname } from "next/navigation"; +import { useSession } from "next-auth/react"; export default function Dashboard({ children }: { children: ReactNode }) { + const { status, data } = useSession(); + const path = usePathname(); + + useEffect(() => { + if (status !== "loading") { + if (status === "unauthenticated" || (data && !data.user.access_token)) { + signOutFunc(); + } + } + }, [data, status, path]); return ( diff --git a/src/app/admin/organizations/page.tsx b/src/app/admin/organizations/page.tsx index 8341444..1a400c1 100644 --- a/src/app/admin/organizations/page.tsx +++ b/src/app/admin/organizations/page.tsx @@ -1,3 +1,4 @@ +"use client"; import { useState } from "react"; import Image from "next/image"; @@ -11,16 +12,14 @@ import { Modal } from "#/components/modal"; import Table from "#/components/table/table"; import Form from "#/components/form/form"; import { icons } from "#/assets/icons"; -import { adminSchema, companySchema } from "#/schema"; +import { companySchema } from "#/schema"; import { Admin, Company } from "#/types"; import Link from "next/link"; export default function Organizations() { 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(); @@ -105,34 +104,6 @@ export default function Organizations() { }, }); - const updateMutation = useMutation({ - mutationFn: async (data: { - id: string; - last_name: string; - first_name: string; - email: string; - }) => { - try { - const response = await axios.put( - `https://private-docs-api.intside.co/companies/${data.id}/`, - data, - { headers: { Authorization: `Bearer ${session?.user.access_token}` } } - ); - - if (response.status === 200 || response.status === 201) { - console.log("modification réussie !"); - setOpenEditModal(false); - } - } catch (error) { - console.error("Erreur lors de la mise à jour", error); - } - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["organizations"] }); - refetch(); - }, - }); - const deleteMutation = useMutation({ mutationFn: async (id: string) => { try { @@ -268,14 +239,14 @@ export default function Organizations() { } title="Supprimer une organisation" content={
-

Voulez-vous vraiment supprimer cette organisation ?

+

Voulez-vous vraiment supprimer cette organisation ?

{ + const selectedIds = table + .getRowModel() + .rows.filter((row) => row.getIsSelected()) + .map((row) => row.original.id); + + return ( +
+
+ + table.toggleAllPageRowsSelected(e.target.checked) + } + /> + + + +

+ Sélectionner une action +

+
+ + + + bulkDeleteMutation.mutate(selectedIds)} + className="p-2 text-[14px] cursor-pointer hover:bg-blue-100 hover:text-blue-500 rounded-md" + > + Supprimer + + + +
+
+ +
+ {/* Modal d'ajout */} + { + if (!isOpen) { + setOpenModal(false); + } + }} + trigger={ +
setOpenModal(true)} + className="cursor-pointer p-3 bg-blue-600 text-white rounded-full" + > + Ajouter une organisation +
+ } + content={ + ({ + label: user.name, + value: user.id, + })) || [], + }, + ]} + submit={createMutation.mutate} // Le type est maintenant compatible + schema={companySchema} + child={ + + } + /> + } + /> + + table.setGlobalFilter(value)} + button={ + + } + /> +
+
+ ); + }} + /> + ); +} diff --git a/src/components/admin/adminHeader.tsx b/src/components/admin/adminHeader.tsx index 993ed9f..2e31708 100644 --- a/src/components/admin/adminHeader.tsx +++ b/src/components/admin/adminHeader.tsx @@ -15,7 +15,7 @@ export default function AdminHeader() { return ( <> {table.getHeaderGroups().map((headerGroup) => ( + {header + ? + + : + } {headerGroup.headers.map((header) => { return( )) ) + : isDataLoading ? + ( + + + + ) : ( - diff --git a/src/lib/function.ts b/src/lib/function.ts new file mode 100644 index 0000000..44c6d61 --- /dev/null +++ b/src/lib/function.ts @@ -0,0 +1,11 @@ +import { signOut } from "next-auth/react"; + +export const signOutFunc = () => { + signOut({ + callbackUrl: `/login?redirect_to=${ + window.location.pathname === "/logout" + ? "/home" + : window.location.pathname + }`, + }); + }; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index bc1ac0b..ec29d34 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,7 +23,8 @@ export interface FormProps { submit: (param: any) => unknown, className?: string, child: ReactNode, - schema: ZodSchema + schema: ZodSchema, + formClassName?: string } export interface StatsType { From ec14a91d6f9460111ceaab813c1473792634dd72 Mon Sep 17 00:00:00 2001 From: "Orace.A" Date: Fri, 28 Mar 2025 12:50:20 +0100 Subject: [PATCH 08/12] feat: add loading state to buttons and center align confirmation messages --- src/app/(auth)/login/page.tsx | 2 +- src/app/admin/admins/page.tsx | 12 +++++++----- src/app/admin/home/page.tsx | 2 +- src/app/globals.css | 31 +++++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index d7e2105..0256b1a 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -70,7 +70,7 @@ export default function LoginPage() { ]} submit={mutation.mutate} schema={loginSchema} - child={} + child={} /> ) diff --git a/src/app/admin/admins/page.tsx b/src/app/admin/admins/page.tsx index 88219a5..bb3c666 100644 --- a/src/app/admin/admins/page.tsx +++ b/src/app/admin/admins/page.tsx @@ -230,9 +230,10 @@ export default function Admins() { child={ } /> @@ -265,7 +266,7 @@ export default function Admins() { title="Supprimer un admin" content={
-

Voulez-vous vraiment supprimer cet admin ?

+

Voulez-vous vraiment supprimer cet admin ?

} /> diff --git a/src/app/admin/home/page.tsx b/src/app/admin/home/page.tsx index 879891a..760bb20 100644 --- a/src/app/admin/home/page.tsx +++ b/src/app/admin/home/page.tsx @@ -151,7 +151,7 @@ export default function HomePage () { } content={
-

Voulez-vous vraiment supprimer cette organisation ?

+

Voulez-vous vraiment supprimer cette organisation ?

} @@ -258,7 +258,7 @@ export default function Admins() {
} diff --git a/src/app/admin/home/page.tsx b/src/app/admin/home/page.tsx index a6d5cdd..d87b1eb 100644 --- a/src/app/admin/home/page.tsx +++ b/src/app/admin/home/page.tsx @@ -17,6 +17,7 @@ export default function HomePage () { const {data: session, status} = useSession() const queryClient = useQueryClient() const [open, setOpen] = useState(false); + const [selectedId, setSelectedId] = useState(null); console.log("Session = ", session) @@ -128,55 +129,67 @@ export default function HomePage () { }, { id: "delete", - cell: ({ cell }) => { - const id = String(cell.row.original.id) - return ( -
{ mutate(id) }} - > - { - if(!isOpen) { - setOpen(isOpen) - } - }} - trigger={ -
setOpen(true)}> - -
+ cell: ({ row }) => { + const id = String(row.original.id); + return ( +
+ { + if (!isOpen) { + setSelectedId(null); } - title={ -

Supprimer une organisation

- } - content={ -
-

Voulez-vous vraiment supprimer cette organisation ?

+ }} + trigger={ +
{ + setSelectedId(id); + }} + > + +
+ } + title={ +

+ Supprimer cette organisation +

+ } + content={ +
+

+ Voulez-vous vraiment supprimer cette organisation ? +

-
- - -
+
+ +
- } - /> -
- ) - } - } +
+ } + /> +
+ ); + }, + }, ] return( diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index 7ffc8cd..e392269 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -99,78 +99,80 @@ export default function Table({ } return( -
+
{render()}
-
+ + ({ type="checkbox" name="" id="" /> @@ -141,9 +150,17 @@ export default function Table({
+ Chargement... +
+ Aucun résultats
- - {table.getHeaderGroups().map((headerGroup) => ( - - {header - ? - - : - - } - {headerGroup.headers.map((header) => { - return( - - ) - })} - - ))} - - - {table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row) => ( - - + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + ) + : isDataLoading ? + ( + + + + ) + : ( + + + + )} + +
- - - table.toggleAllPageRowsSelected(e.target.checked)} - type="checkbox" name="" id="" - /> - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} -
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {header + ? + + : + - ))} + + } + {headerGroup.headers.map((header) => { + return( + + ) + })} - )) - ) - : isDataLoading ? - ( - - - - ) - : ( - - - - )} - -
+ + row.toggleSelected(e.target.checked)} + ref={headerCheckboxRef} + checked={!!table.getIsAllPageRowsSelected()} + onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)} type="checkbox" name="" id="" /> - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
- Chargement... -
- Aucun résultats -
+ ))} + +
+ row.toggleSelected(e.target.checked)} + type="checkbox" name="" id="" + /> + + {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ Chargement... +
+ Aucun résultats +
+
From e56b346c8b2de3e0f9e622276b5c6bbddad909fb Mon Sep 17 00:00:00 2001 From: "Orace.A" Date: Fri, 28 Mar 2025 15:47:10 +0100 Subject: [PATCH 11/12] chore: update schema --- src/schema/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/schema/index.ts b/src/schema/index.ts index 4570ae1..f0fac4d 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -10,6 +10,7 @@ export const adminSchema = z.object({ last_name: z.string(), first_name: z.string(), email: z.string().min(1, "L'email est requis").email("Email invalide"), + organization: z.string().optional(), }); export const companySchema = z.object({ From 015094e9c598e71772749e5b3ba676dadf9c35ed Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 28 Mar 2025 19:05:16 +0100 Subject: [PATCH 12/12] style: closest to pixel-perfect --- src/app/admin/admins/page.tsx | 17 +++++---- src/app/admin/home/page.tsx | 8 ++--- src/app/admin/organizations/[id]/page.tsx | 2 +- src/app/admin/organizations/page.tsx | 41 ++++++++++++--------- src/app/globals.css | 44 +++++++++++++++++++++++ src/assets/icons/chevron-down.svg | 3 ++ src/assets/icons/index.ts | 5 ++- src/components/floatingLabelInput.tsx | 8 ++--- src/components/modal.tsx | 12 ++++--- src/components/table/table.tsx | 6 ++-- 10 files changed, 105 insertions(+), 41 deletions(-) create mode 100644 src/assets/icons/chevron-down.svg diff --git a/src/app/admin/admins/page.tsx b/src/app/admin/admins/page.tsx index fb2e7f8..2389fc8 100644 --- a/src/app/admin/admins/page.tsx +++ b/src/app/admin/admins/page.tsx @@ -193,7 +193,7 @@ export default function Admins() { width={24} height={24} src={icons.editIcon} - className="cursor-pointer responsive-icon" + className="cursor-pointer responsive-icon mr-1" />
} @@ -227,13 +227,15 @@ export default function Admins() { submit={updateMutation.mutate} schema={adminSchema} child={ - + } /> } @@ -321,8 +323,9 @@ export default function Admins() { -

+

Sélectionner une action + arrow down

@@ -355,7 +358,7 @@ export default function Admins() { trigger={
setOpenModal(true)} - className="cursor-pointer p-3 bg-blue-600 text-white rounded-full" + className="cta" > Ajouter un admin
@@ -385,13 +388,15 @@ export default function Admins() { submit={createMutation.mutate} schema={adminSchema} child={ +
+
} /> } diff --git a/src/app/admin/home/page.tsx b/src/app/admin/home/page.tsx index d87b1eb..d9dacdb 100644 --- a/src/app/admin/home/page.tsx +++ b/src/app/admin/home/page.tsx @@ -161,12 +161,12 @@ export default function HomePage () { content={

- Voulez-vous vraiment supprimer cette organisation ? + Voulez-vous vraiment supprimer l'organisation {row.original.name} ?

-
+

-
+
Documents diff --git a/src/app/admin/organizations/page.tsx b/src/app/admin/organizations/page.tsx index 1a400c1..4f273d3 100644 --- a/src/app/admin/organizations/page.tsx +++ b/src/app/admin/organizations/page.tsx @@ -16,6 +16,7 @@ import { companySchema } from "#/schema"; import { Admin, Company } from "#/types"; import Link from "next/link"; + export default function Organizations() { const { data: session, status } = useSession(); const [openModal, setOpenModal] = useState(false); @@ -192,10 +193,10 @@ export default function Organizations() {

-

Voulez-vous vraiment supprimer cette organisation ?

-
- +
} /> } diff --git a/src/app/globals.css b/src/app/globals.css index 018286c..7f3dbe2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -102,6 +102,8 @@ body { .cta{ padding: 10px 24px; + width: max-content; + height: max-content; color: white; background-color: var(--primary); font-size: 14px; @@ -109,6 +111,12 @@ body { border: 1px solid var(--primary); border-radius: 100px; cursor: pointer; + text-wrap: nowrap; +} + +.cta.modal-cta{ + width: 240px; + height: 40px; } .cta.cancel{ @@ -138,6 +146,42 @@ hr{ color: var(--gray); } +input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + + width: 24px; + height: 24px; + border: 1px solid var(--secondary) !important; + border-radius: 6px !important; + background-color: transparent; + cursor: pointer; + position: relative; +} + + +input[type="checkbox"]:checked { + background-color: var(--primary); + border-color: var(--primary); +} + + +input[type="checkbox"]:checked::before { + content: "✔"; + font-size: 18px; + color: white; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.modal-input{ + width: 490px; + margin-left: auto; + margin-right: auto; +} /* Scroll Bar */ diff --git a/src/assets/icons/chevron-down.svg b/src/assets/icons/chevron-down.svg new file mode 100644 index 0000000..532f579 --- /dev/null +++ b/src/assets/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 6d9b354..fed2f25 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -42,6 +42,7 @@ import moonIcon from "./moon.svg" import trash from "./trash.svg" import mailIcon from "./sms.svg" import personalCard from "./personalcard.svg" +import arrowDown from "#/assets/icons/chevron-down.svg" export const icons = { @@ -88,7 +89,9 @@ export const icons = { moonIcon, trash, mailIcon, - personalCard + personalCard, + arrowDown + } diff --git a/src/components/floatingLabelInput.tsx b/src/components/floatingLabelInput.tsx index b9f87fb..8e3b1e8 100644 --- a/src/components/floatingLabelInput.tsx +++ b/src/components/floatingLabelInput.tsx @@ -32,7 +32,7 @@ export default function FloatingLabelInput({ return (
- {button &&
{button}
} + {button &&
{button}
}
); @@ -106,7 +106,7 @@ export default function FloatingLabelInput({ - e.preventDefault()} > - + {title} -
+
{content}
-
{onOpenChange?.(false)}} > - X + fermer
diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index e392269..948ca8c 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -177,7 +177,7 @@ export default function Table({