feat: add stats on admin homme page

This commit is contained in:
Orace.A 2025-03-26 15:42:58 +01:00
parent 98d68a535b
commit 9b4d8cf02f
11 changed files with 229 additions and 31 deletions

View File

@ -2,8 +2,48 @@
import Form from "#/components/form/form" import Form from "#/components/form/form"
import { loginSchema } from "#/schema/loginSchema" import { loginSchema } from "#/schema/loginSchema"
import { useMutation } from "@tanstack/react-query"
import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation";
export default function LoginPage() { export default function LoginPage() {
const router = useRouter()
const mutation = useMutation({
mutationKey: ['login'],
mutationFn: async (data: { email: string; password: string }) => {
try {
const result = await signIn("credentials", {
email: data.email,
password: data.password,
redirect: false,
})
if (result?.error) {
const errorMessage = result.error.includes("CredentialsSignin")
? "Email ou mot de passe incorrect"
: result.error;
console.error(errorMessage)
throw new Error(result.error)
}
return result
} catch (error: any) {
if (error.message.includes("Network Error")) {
console.error("Problème de connexion au serveur");
}
console.error("Autre = ", error);
}
},
onSuccess: () => {
router.push('/admin/home')
},
onError: (error: Error) => {
console.error(error.message)
},
})
return( return(
<div> <div>
<Form <Form
@ -24,7 +64,7 @@ export default function LoginPage() {
showPasswordToggle: true showPasswordToggle: true
} }
]} ]}
submit={undefined} submit={mutation.mutate}
schema={loginSchema} schema={loginSchema}
child={<button type="submit" className="btn-auth">Connexion</button>} child={<button type="submit" className="btn-auth">Connexion</button>}
/> />

View File

@ -1,10 +1,18 @@
"use client" "use client"
import Statistics from "#/components/stats"
import Table from "#/components/table/table" import Table from "#/components/table/table"
import { ColumnDef } from "@tanstack/react-table" import { ColumnDef } from "@tanstack/react-table"
import { useSession } from "next-auth/react"
import { string } from "zod"
export default function HomePage () { export default function HomePage () {
const session = useSession()
console.log("Session = ", session)
type Payment = { type Payment = {
id: string id: string
amount: number amount: number
@ -74,25 +82,32 @@ export default function HomePage () {
const columns: ColumnDef<Payment>[] = [ const columns: ColumnDef<Payment>[] = [
{ // {
id: "select", // id: "select",
header: ({ table }) => ( // header: ({ table }) => (
<input checked={ // <input checked={
table.getIsAllPageRowsSelected() || // table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && undefined) // (table.getIsSomePageRowsSelected() && undefined)
} onChange={(value) => table.toggleAllPageRowsSelected(!!value)} type="checkbox" name="" id="" /> // } onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)} type="checkbox" name="" id="" />
), // ),
cell: ({ row }) => ( // cell: ({ row }) => (
<input checked={row.getIsSelected()} onChange={(value) => row.toggleSelected(!!value)} type="checkbox" name="" id="" /> // <input checked={row.getIsSelected()} onChange={(e) => row.toggleSelected(e.target.checked)} type="checkbox" name="" id="" />
), // ),
}, // },
{ {
accessorKey: "status", accessorKey: "status",
header: "Status", header: "Statut du paiement",
// cell: ({row}) => {
// const value = String(row.getValue("status"))
// return(
// <div className={`${value === "success" ? "text-green-500" : "text-red-500"}`}>
// {value}
// </div>)
// }
}, },
{ {
accessorKey: "email", accessorKey: "email",
header: ({ column }) => { header: ({ }) => {
return ( return (
<p>Email</p> <p>Email</p>
) )
@ -114,16 +129,26 @@ export default function HomePage () {
}, },
{
accessorKey: "id",
cell: ({ cell }) => {
const value = String(cell.getValue())
return (
<p>{value}</p>
)
}
}
] ]
return( return(
<> <div className="space-y-10">
<Statistics />
<Table <Table
columns={columns} columns={columns}
data={data} data={data}
pageSize={5} pageSize={5}
/> />
</> </div>
) )
} }

View File

@ -13,7 +13,7 @@ const handler = NextAuth({
async authorize(credentials) { async authorize(credentials) {
try { try {
const response = await axios.post( const response = await axios.post(
'private-docs-api.intside.co/users/login/', 'https://private-docs-api.intside.co/users/login/',
{ {
email: credentials?.email, email: credentials?.email,
password: credentials?.password, password: credentials?.password,

View File

@ -3,6 +3,8 @@ import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import NextTopLoader from "nextjs-toploader"; import NextTopLoader from "nextjs-toploader";
import "../assets/css/ruben-ui.css" import "../assets/css/ruben-ui.css"
import { AuthProvider } from "#/components/provider/authProvider";
import { QueryClientProvide } from "#/components/provider/queryClient";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@ -16,6 +18,7 @@ export default function RootLayout({
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
@ -23,8 +26,12 @@ export default function RootLayout({
<link rel="favicon.svg" href="/favicon.svg" /> <link rel="favicon.svg" href="/favicon.svg" />
</head> </head>
<body className={inter.className}> <body className={inter.className}>
<NextTopLoader color="#246BFD" shadow="0" /> <AuthProvider>
{children} <QueryClientProvide>
<NextTopLoader color="#246BFD" shadow="0" />
{children}
</QueryClientProvide>
</AuthProvider>
</body> </body>
</html> </html>
); );

View File

@ -23,7 +23,7 @@ import messagesIcon from "./message.svg"
import homeIcon from "./NavItem.svg" import homeIcon from "./NavItem.svg"
import companiesIcon from "./buildings.svg" import companiesIcon from "./buildings.svg"
import arrowLeft from "./arrowLeft.svg" import arrowLeft from "./arrowLeft.svg"
import arrowRight from "./arrowLeft.svg" import arrowRight from "./arrowRight.svg"
import filesIcon from "./ph_files.svg" import filesIcon from "./ph_files.svg"
import pdfIcon from "./prime_file-pdf.svg" import pdfIcon from "./prime_file-pdf.svg"
import wordIcon from "./prime_file-word.svg" import wordIcon from "./prime_file-word.svg"

View File

@ -37,6 +37,7 @@ export default function Form({
setErrors(newErrors) setErrors(newErrors)
} else { } else {
setErrors({}) setErrors({})
submit(result.data)
} }
} }

View File

@ -0,0 +1,7 @@
'use client'
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
export function AuthProvider({ children }: Readonly<{ children: ReactNode }>) {
return <SessionProvider>{children}</SessionProvider>;
}

View File

@ -0,0 +1,14 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode } from "react";
export function QueryClientProvide({
children,
}: Readonly<{ children: ReactNode }>) {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

70
src/components/stats.tsx Normal file
View File

@ -0,0 +1,70 @@
import Image from "next/image"
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { useSession } from "next-auth/react";
import { icons } from "#/assets/icons";
import { Stats, StatsType, } from "#/types";
export default function Statistics() {
const { data: session, status } = useSession();
const { data: stats, isLoading} = useQuery({
enabled: status === 'authenticated',
queryKey: ["stats", session?.user.access_token],
queryFn: async () => {
try {
const response = await axios.get(
'https://private-docs-api.intside.co/statistics', {
headers: {
'Authorization': `Bearer ${session?.user.access_token}`
}
}
)
if(response.data) {
return response.data as Stats
}
} catch (error: any) {
console.error(error)
}
}
})
const statsData: StatsType[] = [
{ id: 1, title: 'Organisations', value: stats?.companies, icon: icons.companiesIcon, color: 'blue' },
{ id: 2, title: 'Utilisateurs', value: stats?.users, icon: icons.userIcon, color: 'blue' },
{ id: 3, title: 'Documents', value: stats?.documents, icon: icons.docummentTextIcon, color: 'blue' },
{ id: 4, title: 'Stockage', value: stats?.documents_size, icon: icons.archivesIcon, color: 'blue' }
];
return(
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{statsData.map(({ id, title, value, icon }) => (
<div key={id} className="w-full">
<div className="flex items-center rounded-xl border-2 border-blue-500 p-4 space-x-3">
<div
className="flex items-center justify-center rounded-lg bg-[#E9F0FF] bg-opacity-25 p-2"
style={{ width: '54px', height: '54px' }}
>
<Image
alt={title}
src={icon}
width={32}
height={32}
className="text-blue-500"
/>
</div>
<div className="ml-3">
<p className="text-sm text-gray-500 mb-0">{title}</p>
<p className="font-bold text-2xl mb-0">{ status === "loading" && isLoading ? "Chargement..." : value}</p>
</div>
</div>
</div>
))}
</div>
)
}

View File

@ -11,6 +11,8 @@ import {
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { useState } from "react"; import { useState } from "react";
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx"
import Image from "next/image";
import { icons } from "#/assets/icons";
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[] columns: ColumnDef<TData, TValue>[]
@ -43,6 +45,7 @@ export default function Table<TData, TValue>({
pageSize: pageSize, pageSize: pageSize,
} }
}, },
enableRowSelection: true,
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
@ -66,9 +69,18 @@ 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">
<input checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && undefined)
}
onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)}
type="checkbox" name="" id=""
/>
</th>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return( return(
<th key={header.id} className="bg-blue-300 p-3 text-start"> <th key={header.id} className="bg-blue-300 p-3 text-start last:rounded-tr-lg">
{flexRender( {flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext()
@ -83,6 +95,13 @@ export default function Table<TData, TValue>({
{table.getRowModel().rows.length ? ( {table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<tr key={row.id} className={clsx('hover:bg-gray-300 border-t border-gray-200', { 'bg-gray-300': row.getIsSelected()})}> <tr key={row.id} className={clsx('hover:bg-gray-300 border-t border-gray-200', { 'bg-gray-300': row.getIsSelected()})}>
<td className="p-3 text-start">
<input
checked={row.getIsSelected()}
onChange={(e) => row.toggleSelected(e.target.checked)}
type="checkbox" name="" id=""
/>
</td>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td key={cell.id} className="p-3 text-start"> <td key={cell.id} className="p-3 text-start">
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
@ -104,11 +123,11 @@ export default function Table<TData, TValue>({
<div className="flex items-center justify-end space-x-2 py-4"> <div className="flex items-center justify-end space-x-2 py-4">
<button <button
className="border bg-gray-200 shadow-xs hover:bg-gray-300 hover:text-black px-3 py-1 rounded" className="bg-gray-100 shadow-xs hover:bg-gray-300 px-3 py-1 rounded w-9 h-9"
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
Précédent <Image alt="" src={icons.arrowLeft} className="hover:text-blue-400"/>
</button> </button>
<div className="flex space-x-1"> <div className="flex space-x-1">
@ -116,10 +135,10 @@ export default function Table<TData, TValue>({
<button <button
key={pageNumber} key={pageNumber}
className={clsx( className={clsx(
"px-3 py-1 rounded", "px-3 py-1 rounded w-9 h-9",
pageNumber === currentPage pageNumber === currentPage
? "bg-blue-500 text-white" ? "bg-[#E9F0FF] text-blue-400"
: "bg-gray-200 hover:bg-gray-300" : "bg-gray-100 hover:bg-gray-300"
)} )}
onClick={() => table.setPageIndex(pageNumber - 1)} onClick={() => table.setPageIndex(pageNumber - 1)}
> >
@ -129,11 +148,11 @@ export default function Table<TData, TValue>({
</div> </div>
<button <button
className="border bg-gray-200 shadow-xs hover:bg-gray-300 hover:text-black px-3 py-1 rounded" className="w-9 h-9 bg-gray-100 shadow-xs hover:bg-gray-300 hover:text-black px-3 py-1 rounded"
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
Suivant <Image alt="" src={icons.arrowRight} />
</button> </button>
</div> </div>
</div> </div>

View File

@ -15,8 +15,23 @@ export interface FloatingLabelInputProps {
export interface FormProps { export interface FormProps {
title?: string, title?: string,
fields: FloatingLabelInputProps[], fields: FloatingLabelInputProps[],
submit: FormEventHandler<HTMLFormElement> | undefined, submit: (param: any) => unknown,
className: string, className: string,
child: ReactNode, child: ReactNode,
schema: ZodSchema schema: ZodSchema
}
export interface StatsType {
id: number;
title: string;
value: number | undefined;
icon: string;
color: string;
}
export interface Stats {
companies: number
documents: number
users: number
documents_size: number
} }