feat: add stats on admin homme page
This commit is contained in:
parent
322f4d8637
commit
080ab6dd09
@ -2,8 +2,48 @@
|
||||
|
||||
import Form from "#/components/form/form"
|
||||
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() {
|
||||
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(
|
||||
<div>
|
||||
<Form
|
||||
@ -24,7 +64,7 @@ export default function LoginPage() {
|
||||
showPasswordToggle: true
|
||||
}
|
||||
]}
|
||||
submit={undefined}
|
||||
submit={mutation.mutate}
|
||||
schema={loginSchema}
|
||||
child={<button type="submit" className="btn-auth">Connexion</button>}
|
||||
/>
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import Statistics from "#/components/stats"
|
||||
import Table from "#/components/table/table"
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { string } from "zod"
|
||||
|
||||
|
||||
export default function HomePage () {
|
||||
|
||||
const session = useSession()
|
||||
|
||||
console.log("Session = ", session)
|
||||
|
||||
type Payment = {
|
||||
id: string
|
||||
amount: number
|
||||
@ -74,25 +82,32 @@ export default function HomePage () {
|
||||
|
||||
|
||||
const columns: ColumnDef<Payment>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<input checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && undefined)
|
||||
} onChange={(value) => table.toggleAllPageRowsSelected(!!value)} type="checkbox" name="" id="" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input checked={row.getIsSelected()} onChange={(value) => row.toggleSelected(!!value)} type="checkbox" name="" id="" />
|
||||
),
|
||||
},
|
||||
// {
|
||||
// id: "select",
|
||||
// header: ({ table }) => (
|
||||
// <input checked={
|
||||
// table.getIsAllPageRowsSelected() ||
|
||||
// (table.getIsSomePageRowsSelected() && undefined)
|
||||
// } onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)} type="checkbox" name="" id="" />
|
||||
// ),
|
||||
// cell: ({ row }) => (
|
||||
// <input checked={row.getIsSelected()} onChange={(e) => row.toggleSelected(e.target.checked)} type="checkbox" name="" id="" />
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
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",
|
||||
header: ({ column }) => {
|
||||
header: ({ }) => {
|
||||
return (
|
||||
<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(
|
||||
<>
|
||||
<div className="space-y-10">
|
||||
<Statistics />
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data}
|
||||
pageSize={5}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -13,7 +13,7 @@ const handler = NextAuth({
|
||||
async authorize(credentials) {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'private-docs-api.intside.co/users/login/',
|
||||
'https://private-docs-api.intside.co/users/login/',
|
||||
{
|
||||
email: credentials?.email,
|
||||
password: credentials?.password,
|
||||
|
||||
@ -3,6 +3,8 @@ import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import "../assets/css/ruben-ui.css"
|
||||
import { AuthProvider } from "#/components/provider/authProvider";
|
||||
import { QueryClientProvide } from "#/components/provider/queryClient";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@ -16,6 +18,7 @@ export default function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -23,8 +26,12 @@ export default function RootLayout({
|
||||
<link rel="favicon.svg" href="/favicon.svg" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<AuthProvider>
|
||||
<QueryClientProvide>
|
||||
<NextTopLoader color="#246BFD" shadow="0" />
|
||||
{children}
|
||||
</QueryClientProvide>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -23,7 +23,7 @@ import messagesIcon from "./message.svg"
|
||||
import homeIcon from "./NavItem.svg"
|
||||
import companiesIcon from "./buildings.svg"
|
||||
import arrowLeft from "./arrowLeft.svg"
|
||||
import arrowRight from "./arrowLeft.svg"
|
||||
import arrowRight from "./arrowRight.svg"
|
||||
import filesIcon from "./ph_files.svg"
|
||||
import pdfIcon from "./prime_file-pdf.svg"
|
||||
import wordIcon from "./prime_file-word.svg"
|
||||
|
||||
@ -37,6 +37,7 @@ export default function Form({
|
||||
setErrors(newErrors)
|
||||
} else {
|
||||
setErrors({})
|
||||
submit(result.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
src/components/provider/authProvider.tsx
Normal file
7
src/components/provider/authProvider.tsx
Normal 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>;
|
||||
}
|
||||
14
src/components/provider/queryClient.tsx
Normal file
14
src/components/provider/queryClient.tsx
Normal 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
70
src/components/stats.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -11,6 +11,8 @@ import {
|
||||
} from "@tanstack/react-table"
|
||||
import { useState } from "react";
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import Image from "next/image";
|
||||
import { icons } from "#/assets/icons";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
@ -43,6 +45,7 @@ export default function Table<TData, TValue>({
|
||||
pageSize: pageSize,
|
||||
}
|
||||
},
|
||||
enableRowSelection: true,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
@ -66,9 +69,18 @@ export default function Table<TData, TValue>({
|
||||
<thead className="h-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id} className="rounded-lg">
|
||||
<th className="bg-blue-300 p-3 text-start first:rounded-tl-lg">
|
||||
<input checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && undefined)
|
||||
}
|
||||
onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)}
|
||||
type="checkbox" name="" id=""
|
||||
/>
|
||||
</th>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return(
|
||||
<th key={header.id} className="bg-blue-300 p-3 text-start">
|
||||
<th key={header.id} className="bg-blue-300 p-3 text-start last:rounded-tr-lg">
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
@ -83,6 +95,13 @@ export default function Table<TData, TValue>({
|
||||
{table.getRowModel().rows.length ? (
|
||||
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()})}>
|
||||
<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) => (
|
||||
<td key={cell.id} className="p-3 text-start">
|
||||
{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">
|
||||
<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()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Précédent
|
||||
<Image alt="" src={icons.arrowLeft} className="hover:text-blue-400"/>
|
||||
</button>
|
||||
|
||||
<div className="flex space-x-1">
|
||||
@ -116,10 +135,10 @@ export default function Table<TData, TValue>({
|
||||
<button
|
||||
key={pageNumber}
|
||||
className={clsx(
|
||||
"px-3 py-1 rounded",
|
||||
"px-3 py-1 rounded w-9 h-9",
|
||||
pageNumber === currentPage
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-200 hover:bg-gray-300"
|
||||
? "bg-[#E9F0FF] text-blue-400"
|
||||
: "bg-gray-100 hover:bg-gray-300"
|
||||
)}
|
||||
onClick={() => table.setPageIndex(pageNumber - 1)}
|
||||
>
|
||||
@ -129,11 +148,11 @@ export default function Table<TData, TValue>({
|
||||
</div>
|
||||
|
||||
<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()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Suivant
|
||||
<Image alt="" src={icons.arrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -15,8 +15,23 @@ export interface FloatingLabelInputProps {
|
||||
export interface FormProps {
|
||||
title?: string,
|
||||
fields: FloatingLabelInputProps[],
|
||||
submit: FormEventHandler<HTMLFormElement> | undefined,
|
||||
submit: (param: any) => unknown,
|
||||
className: string,
|
||||
child: ReactNode,
|
||||
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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user