Compare commits
32 Commits
bcac0d2a4b
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
05b0a4514b | ||
560226b6a8 | |||
4e6a03df17 | |||
22ec7f8fdd | |||
baf31f7959 | |||
1e3dcf340d | |||
|
6506d6817b | ||
|
4cf9069bf2 | ||
68cd3397d4 | |||
4985dd9ec2 | |||
|
2868efcda4 | ||
|
18b7052259 | ||
35b6d419e1 | |||
2444f620f5 | |||
aa59eaa3fc | |||
af562b7b10 | |||
2b65a6fef2 | |||
94b6cdb9ba | |||
|
1e05ef6abf | ||
8b01b19365 | |||
85cba8b1dc | |||
aa448921ac | |||
3f0db3f976 | |||
6602bfd408 | |||
83291c246b | |||
135ab48886 | |||
|
66cc9c6801 | ||
d33e9b29ba | |||
|
e321927283 | ||
|
cf0226ea29 | ||
7f9f95ecc2 | |||
|
e1897ca11a |
1021
.vs/2024-DEV-BUT3/config/applicationhost.config
Normal file
1021
.vs/2024-DEV-BUT3/config/applicationhost.config
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.vs/2024-DEV-BUT3/v17/.wsuo
Normal file
BIN
.vs/2024-DEV-BUT3/v17/.wsuo
Normal file
Binary file not shown.
606
.vs/2024-DEV-BUT3/v17/DocumentLayout.json
Normal file
606
.vs/2024-DEV-BUT3/v17/DocumentLayout.json
Normal file
File diff suppressed because it is too large
Load Diff
3
.vs/ProjectSettings.json
Normal file
3
.vs/ProjectSettings.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"CurrentProjectSetting": null
|
||||
}
|
16
.vs/VSWorkspaceState.json
Normal file
16
.vs/VSWorkspaceState.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"ExpandedNodes": [
|
||||
"",
|
||||
"\\src",
|
||||
"\\src\\api",
|
||||
"\\src\\assets",
|
||||
"\\src\\components",
|
||||
"\\src\\components\\form",
|
||||
"\\src\\components\\item",
|
||||
"\\src\\components\\nav",
|
||||
"\\src\\components\\parts",
|
||||
"\\src\\components\\rooms"
|
||||
],
|
||||
"SelectedNode": "\\README.md",
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
BIN
.vs/slnx.sqlite
Normal file
BIN
.vs/slnx.sqlite
Normal file
Binary file not shown.
32
README.md
32
README.md
@@ -1,6 +1,36 @@
|
||||
|
||||
# Projet BUT 3
|
||||
|
||||
made by :
|
||||
Simon CATANESE
|
||||
Julien CHARBONNEL
|
||||
Victor DESCAMPS
|
||||
|
||||
## Tests warning :
|
||||
|
||||
the tests sheet 'api/image-request.test.jsx' was commented out to avoid over-sending requests to the google API during development. Remember to uncomment the file if you want to run all the tests once, but remember to recomment the file if you want to run a series of tests.
|
||||
An over-sending of requests could get us blocked for the day by google.
|
||||
|
||||
## Project summary (FR):
|
||||
|
||||
Nous n'avons pas r<>ussi <20> mettre en <20>uvre toutes les fonctionnalit<69>s de l'interface utilisateur <20> temps. Le d<>veloppement n'a pas non plus <20>t<EFBFBD> rigoureusement test-driven.
|
||||
|
||||
r<EFBFBD>sum<EFBFBD> des fonctionnalit<69>s :
|
||||
|
||||
L'utilisateur peut cr<63>er un compte sur la page de login (navbar)
|
||||
En se connectant, l'utilisateur peut charger la liste des articles et les filtrer par pi<70>ce, date, nom, etc. Il est possible de cr<63>er, supprimer et de mettre <20> jour des articles.
|
||||
Une page permet de consulter les statistiques des pi<70>ces et la liste des pi<70>ces. Il est possible d'y cr<63>er de nouvelles, de les modifier ou de les supprimer (supprimant <20>galement les articles li<6C>s).
|
||||
Toute op<6F>ration d'<27>criture r<>ussie provoque un rechargement de page.
|
||||
|
||||
Les icones AntDesign provoquent syst<73>matiquement une exception dans la console. Nous n'avons pas <20>t<EFBFBD> en mesure de corriger ou <20>viter cette erreur. Les icones s'affichent bien, donc <20>a ne posera pas de probl<62>me <20> part la pollution de la console.
|
||||
|
||||
Nous avons voulu personnaliser un peu notre projet pour le diff<66>rencier des autres, nous avons donc mis en place un syst<73>me simple de recherche automatique d'images li<6C>es aux objets enregistr<74>s, et une requ<71>te <20> l'API Inspirobot <20> chaque rechargement de la page sur la page d'accueil pour un accueil chaleureux.
|
||||
|
||||
Nous sommes conscients que ce projet ne correspond pas exactement aux attentes d'un d<>veloppement full test-driven. Mais nous nous satisfaisons tout de m<>me de nous <20>tre familiaris<69>s avec js et React, avec les tests en g<>n<EFBFBD>ral, et d'avoir pris du plaisir <20> d<>velopper ce petit projet.
|
||||
|
||||
Nous vous souhaitons une bonne correction et vous remercions pour vos cours.
|
||||
|
||||
|
||||
## Run Locally
|
||||
|
||||
Clone the project
|
||||
@@ -30,3 +60,5 @@ Start the front-end
|
||||
## API Reference
|
||||
|
||||
API Documentation : [Postman]([Postman documentation url])
|
||||
|
||||
|
||||
|
28
__tests__/api/image-request.test.jsx
Normal file
28
__tests__/api/image-request.test.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { searchAndResizeImage } from "../../src/api/image-request"
|
||||
|
||||
// Ces tests sont comment<6E>s pour ne pas provoquer de requ<71>tes <20> r<>p<EFBFBD>titions qui r<>sulteraient en un blocage de google.
|
||||
//D<>commentez les test^s pour les <20>xecuter
|
||||
|
||||
describe("Image request API", () => {
|
||||
it();
|
||||
|
||||
//it("return a string", async () => {
|
||||
// let query = 'cat';
|
||||
// let imageUrl = await searchAndResizeImage(query);
|
||||
// let isString = typeof imageUrl == 'string'
|
||||
// expect(isString).toBe(true);
|
||||
//});
|
||||
|
||||
//it("returns a valid image URL when results are found", async () => {
|
||||
// let query = 'cat';
|
||||
// let imageUrl = await searchAndResizeImage(query);
|
||||
// expect(imageUrl).toMatch(/^https?:\/\/.*\.(?:png|jpg|jpeg|gif|webp)$/i);
|
||||
//});
|
||||
|
||||
//it("returns an empty string when no image is found", async () => {
|
||||
// const query = '[][][][][][][][][][][][][][]'; //cette requ<71>te ne renvoie aucune image
|
||||
// const imageUrl = await searchAndResizeImage(query);
|
||||
// expect(imageUrl).toEqual('');
|
||||
//});
|
||||
});
|
18
__tests__/api/inspirobot.test.jsx
Normal file
18
__tests__/api/inspirobot.test.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { findInspiration } from "../../src/api/inspirobot"
|
||||
|
||||
describe("Inspirobot API", () => {
|
||||
|
||||
it("return a string", async () => {
|
||||
let imageUrl = await findInspiration();
|
||||
let isString = typeof imageUrl == 'string'
|
||||
expect(isString).toBe(true);
|
||||
});
|
||||
|
||||
it("returns a valid image URL or an empty string", async () => {
|
||||
let query = 'cat';
|
||||
let imageUrl = await findInspiration(query);
|
||||
let okResult = imageUrl.endsWith('.jpg') || imageUrl == ""
|
||||
expect(okResult).toBe(true)
|
||||
});
|
||||
});
|
78
__tests__/pages/authenticated/Items.test.jsx
Normal file
78
__tests__/pages/authenticated/Items.test.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
|
||||
import { render, fireEvent, screen, cleanup } from "@testing-library/react";
|
||||
import { ItemPage } from "../../../src/components/item/ItemPage.jsx"; // Assurez-vous d'importer correctement votre composant
|
||||
import { AuthenticationContext } from "../../../src/contexts/index.js";
|
||||
|
||||
describe("Item Page with Authentication", () => {
|
||||
let rooms;
|
||||
beforeEach(() => {
|
||||
// Simuler la connexion de l'utilisateur
|
||||
const user = { username: "Juvisi!0TEST", password: "Juvisi!0TEST" };
|
||||
const loginFunction = vi.fn().mockResolvedValue(user); // Mock de la fonction de connexion
|
||||
rooms = [{ _id: "66425c9d8869396c3e853633", name: "test" }];
|
||||
render(
|
||||
<AuthenticationContext.Provider value={{ login: loginFunction }}>
|
||||
<ItemPage />
|
||||
</AuthenticationContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(cleanup); // Nettoyer après chaque test pour éviter les fuites de mémoire
|
||||
|
||||
it("renders without errors", () => {
|
||||
expect(screen.getByTestId("item-container")).toBeTruthy();
|
||||
});
|
||||
|
||||
/*it('allows filtering items', async () => {
|
||||
// Simuler une saisie dans le champ de recherche par nom
|
||||
fireEvent.change(screen.getByLabelText('Search by name:'), { target: { value: 'Item' } });
|
||||
// Attendre que les éléments filtrés soient rendus dans la liste
|
||||
await screen.findByText('Filtered Item');
|
||||
});*/
|
||||
|
||||
/*it('allows creating an item', async () => {
|
||||
// Simuler un clic sur le bouton "Create Item"
|
||||
fireEvent.click(screen.getByTestId('create-item-button'));
|
||||
// Attendre que le modal de création d'élément s'ouvre
|
||||
await screen.findByTestId('create-item-button');
|
||||
// Simuler une saisie dans les champs du formulaire de création d'élément
|
||||
fireEvent.change(screen.getByTestId("create-item-Brand-Input"), { target: { value: 'Brand X' } });
|
||||
fireEvent.change(screen.getByTestId("create-item-Model-Input"), { target: { value: 'Model Y' } });
|
||||
// Simuler un clic pour ouvrir le menu déroulant
|
||||
fireEvent.mouseDown(screen.getByTestId("create-item-Room-Select"));
|
||||
|
||||
// Attendre que les options du menu déroulant soient rendues
|
||||
await screen.findByText('test');
|
||||
|
||||
// Simuler un clic pour choisir l'option "test"
|
||||
fireEvent.click(screen.getByText('test'));
|
||||
|
||||
|
||||
// Simuler un clic sur le bouton "Submit" du formulaire
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
// Attendre que l'élément soit ajouté à la liste
|
||||
await screen.findByText('Brand X');
|
||||
await screen.findByText('Model Y');
|
||||
});*/
|
||||
|
||||
/*it('allows updating an item', async () => {
|
||||
// Simuler un clic sur le bouton "Edit" de l'élément à mettre à jour
|
||||
fireEvent.click(screen.getByText('Edit'));
|
||||
// Attendre que le modal de mise à jour de l'élément s'ouvre
|
||||
await screen.findByText('Update Item');
|
||||
// Simuler une saisie dans les champs du formulaire de mise à jour d'élément
|
||||
fireEvent.change(screen.getByLabelText('Brand'), { target: { value: 'Updated Brand' } });
|
||||
fireEvent.change(screen.getByLabelText('Model'), { target: { value: 'Updated Model' } });
|
||||
// Simuler un clic sur le bouton "Submit" du formulaire de mise à jour
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
// Attendre que l'élément mis à jour soit affiché dans la liste
|
||||
await screen.findByText('Updated Brand Updated Model');
|
||||
});*/
|
||||
|
||||
/*it('allows deleting an item', async () => {
|
||||
// Simuler un clic sur le bouton "Delete" de l'élément à supprimer
|
||||
fireEvent.click(screen.getByText('Delete'));
|
||||
// Attendre que l'élément soit supprimé de la liste
|
||||
await screen.findByText('Updated Brand Updated Model', { timeout: 5000, shouldNotExist: true });
|
||||
});*/
|
||||
});
|
36
__tests__/pages/authenticated/Register.test.jsx
Normal file
36
__tests__/pages/authenticated/Register.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("inscription avec nom d'utilisateur déjà existant", async ({ page }) => {
|
||||
await page.goto("http://localhost:3001/register");
|
||||
await page.fill('input[placeholder="username"]', "utilisateur_existant");
|
||||
await page.fill('input[placeholder="password"]', "mot_de_passe_test");
|
||||
await page.fill('input[placeholder="confirmation"]', "mot_de_passe_test");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState("networkidle");
|
||||
const errorMessage = await page.textContent(".text-red-500");
|
||||
expect(errorMessage).toContain("Ce nom d'utilisateur existe déjà");
|
||||
});
|
||||
|
||||
test("inscription avec mots de passe non correspondants", async ({ page }) => {
|
||||
await page.goto("http://localhost:3001/register");
|
||||
await page.fill('input[placeholder="username"]', "utilisateur_test");
|
||||
await page.fill('input[placeholder="password"]', "mot_de_passe_test");
|
||||
await page.fill(
|
||||
'input[placeholder="confirmation"]',
|
||||
"mot_de_passe_incorrect",
|
||||
);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState("networkidle");
|
||||
const errorMessage = await page.textContent(".text-red-500");
|
||||
expect(errorMessage).toContain("Les mots de passe ne correspondent pas");
|
||||
});
|
||||
|
||||
test("inscription sans nom d'utilisateur", async ({ page }) => {
|
||||
await page.goto("http://localhost:3001/register");
|
||||
await page.fill('input[placeholder="password"]', "mot_de_passe_test");
|
||||
await page.fill('input[placeholder="confirmation"]', "mot_de_passe_test");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState("networkidle");
|
||||
const errorMessage = await page.textContent(".text-red-500");
|
||||
expect(errorMessage).toContain("Veuillez fournir un nom d'utilisateur");
|
||||
});
|
2258
package-lock.json
generated
2258
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -15,21 +15,23 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^5.15.4",
|
||||
"@ant-design/icons": "^5.3.7",
|
||||
"axios": "^1.6.7",
|
||||
"bootstrap": "^5.3.3",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^18.2.0",
|
||||
"react-cookie": "^7.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.3"
|
||||
"react-router-dom": "^6.23.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@types/node": "^20.11.9",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"antd": "^5.17.0",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
|
@@ -1,13 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { Authenticated } from "./components";
|
||||
|
||||
import { Router } from "./router";
|
||||
|
||||
const App = () => (
|
||||
<Authenticated>
|
||||
<Router />
|
||||
</Authenticated>
|
||||
);
|
||||
|
||||
export default App;
|
15
src/App.jsx
15
src/App.jsx
@@ -3,11 +3,24 @@ import React from "react";
|
||||
import { Authenticated } from "./components";
|
||||
|
||||
import { Router } from "./router";
|
||||
import Navbar from "./components/nav/Navbar";
|
||||
import AppLayout from "./components/app-layout";
|
||||
import { PageTitleProvider } from "./hooks/page-title-context";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const App = () => (
|
||||
|
||||
<Authenticated>
|
||||
<Router />
|
||||
<PageTitleProvider>
|
||||
<AppLayout>
|
||||
</AppLayout>
|
||||
</PageTitleProvider>
|
||||
|
||||
</Authenticated>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
||||
|
@@ -6,7 +6,7 @@ export const isLoggedIn = async () => {
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return error.response.data;
|
||||
return error.responseq;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const login = async (username, password) => {
|
||||
const response = await axios.post("/authenticate", { username, password });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return error.response.data;
|
||||
return error.response;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,6 +25,6 @@ export const logout = async () => {
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return error.response.data;
|
||||
return error.response;
|
||||
}
|
||||
};
|
||||
|
28
src/api/image-request.js
Normal file
28
src/api/image-request.js
Normal file
@@ -0,0 +1,28 @@
|
||||
//const API_KEY = 'AIzaSyA_w7wmOAibpiDF5H3mXuL9AhcN-KVkFg4';
|
||||
const API_KEY = 'AIzaSyCWPG-WbraYVEk6wVEZX8dJQAABTibvGNM';
|
||||
const CSE_ID = '611cd62b5f315445c';
|
||||
|
||||
export async function searchAndResizeImage(query) {
|
||||
try {
|
||||
const response = await fetch(`https://www.googleapis.com/customsearch/v1?key=${API_KEY}&cx=${CSE_ID}&q=${query}&searchType=image&num=1&imgSize=medium`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.items && data.items.length > 0) {
|
||||
const imageUrl = data.items[0].link;
|
||||
console.log('Image URL:', imageUrl);
|
||||
return imageUrl;
|
||||
}
|
||||
else {
|
||||
console.error('No image found.');
|
||||
return "";
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching image:', error);
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,2 +1,6 @@
|
||||
export * from './authentication'
|
||||
export * from './user'
|
||||
export * from './image-request'
|
||||
export * from './room'
|
||||
export * from './item'
|
||||
export * from './inspirobot'
|
||||
|
15
src/api/inspirobot.js
Normal file
15
src/api/inspirobot.js
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
export const findInspiration = async () => {
|
||||
|
||||
try {
|
||||
const url = 'https://corsproxy.io/?https://inspirobot.me/api?generate=true'
|
||||
const response = await fetch(url);
|
||||
|
||||
const pictureUrl = await response.text();
|
||||
console.log(pictureUrl);
|
||||
return pictureUrl
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return "";
|
||||
}
|
68
src/api/item.js
Normal file
68
src/api/item.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
|
||||
export const getItem = async (id) => {
|
||||
try {
|
||||
console.log("GET ITEM ID : ", id)
|
||||
const response = await axios.get("/item/"+id);
|
||||
console.log("GET ITEM : ", response.data)
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log("ERROR", error.response.data)
|
||||
return error.response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const getItems = async () => {
|
||||
try {
|
||||
const response = await axios.get("/item", {});
|
||||
console.log(response.data)
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log("ERROR", error.response.data)
|
||||
return error.response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const createItem = async (values) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${import.meta.env.VITE_API_URL}/item`,
|
||||
values,
|
||||
);
|
||||
console.log(response.data);
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const updateItem = async (_id, values) => {
|
||||
try {
|
||||
const response = await axios.put(
|
||||
`${import.meta.env.VITE_API_URL}/item/${_id}`,
|
||||
values,
|
||||
);
|
||||
console.log(response.data);
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteItem = async (_id) => {
|
||||
try {
|
||||
const response = await axios.delete(
|
||||
`${import.meta.env.VITE_API_URL}/item/${_id}`
|
||||
);
|
||||
console.log(response.data);
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
134
src/api/room.js
Normal file
134
src/api/room.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
|
||||
|
||||
export const getRoom = async (_id) => {
|
||||
try {
|
||||
let id = _id.toString()
|
||||
const response = await axios.get(`/room/${id}`);
|
||||
console.log(response.data)
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log("ERROR", error.response.data)
|
||||
return error.response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRooms = async () => {
|
||||
try {
|
||||
const response = await axios.get("/room");
|
||||
console.log(response.data)
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log("ERROR", error.response.data)
|
||||
return error.response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const createRoom = async (values) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${import.meta.env.VITE_API_URL}/room`,
|
||||
values,
|
||||
);
|
||||
console.log(response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const updateRoom = async(_id, values) => {
|
||||
try {
|
||||
const response = await axios.put(
|
||||
`${import.meta.env.VITE_API_URL}/room/${_id}`,
|
||||
values,
|
||||
);
|
||||
console.log(response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteRoom = async (_id) => {
|
||||
try {
|
||||
const itemsResponse = await axios.get(
|
||||
`${import.meta.env.VITE_API_URL}/item`,
|
||||
);
|
||||
const items = itemsResponse.data;
|
||||
items?.forEach(async (item) => {
|
||||
await axios.delete(
|
||||
`${import.meta.env.VITE_API_URL}/item/${item._id}`,
|
||||
);
|
||||
})
|
||||
|
||||
const response = await axios.delete(
|
||||
`${import.meta.env.VITE_API_URL}/room/${_id}`
|
||||
);
|
||||
console.log(response.data);
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const getRoomStats = async () => {
|
||||
try {
|
||||
const response = await axios.get("/room/stats");
|
||||
console.log(response.data)
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log("ERROR", error.response.data)
|
||||
return error.response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatRoomStats = (roomStats) => {
|
||||
const global = formatRoomStatsGlobal(roomStats.global)
|
||||
const rooms = Object.values(roomStats.rooms).map(formatRoom);
|
||||
const years = roomStats.years;
|
||||
return {
|
||||
global,
|
||||
rooms,
|
||||
years
|
||||
}
|
||||
}
|
||||
|
||||
function formatRoomStatsGlobal(global){
|
||||
const { rooms_count, items_count, total_price, average_price} = global;
|
||||
|
||||
const most_item_room = {
|
||||
name: global.most_item_room.name,
|
||||
count: global.most_item_room.count
|
||||
};
|
||||
|
||||
const most_expensive_room = {
|
||||
name: global.most_expensive_room.name,
|
||||
count: global.most_expensive_room.count
|
||||
};
|
||||
|
||||
return {
|
||||
rooms_count,
|
||||
items_count,
|
||||
total_price,
|
||||
average_price,
|
||||
most_item_room,
|
||||
most_expensive_room
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function formatRoom(room) {
|
||||
const { _id, name, items_count, room_price } = room;
|
||||
|
||||
return {
|
||||
_id,
|
||||
name,
|
||||
items_count,
|
||||
room_price
|
||||
};
|
||||
}
|
40
src/assets/styles/diagram.css
Normal file
40
src/assets/styles/diagram.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.diagram-container {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.row-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.row {
|
||||
background-color: #007bff;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
transition: width 0.5s ease;
|
||||
|
||||
}
|
||||
|
||||
.value {
|
||||
position: absolute;
|
||||
bottom: 50%;
|
||||
right: 0;
|
||||
transform: translate(50%, 50%);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 55px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
}
|
100
src/assets/styles/item-page.css
Normal file
100
src/assets/styles/item-page.css
Normal file
@@ -0,0 +1,100 @@
|
||||
.item-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
margin-right: 15px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-group .ant-picker,
|
||||
.filter-group .ant-input-number,
|
||||
.filter-group .ant-input,
|
||||
.filter-group .ant-select {
|
||||
margin-right: 15px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.filter-group .ant-select {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.filter-group .ant-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-group .ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-group .ant-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.filter-group .ant-picker,
|
||||
.filter-group .ant-input-number,
|
||||
.filter-group .ant-input,
|
||||
.filter-group .ant-select {
|
||||
margin-right: 0;
|
||||
margin-bottom: 5px;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
width: 50vw;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
|
||||
.pagination button {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.item-container .item-list .ant-col {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item-container .item-list .ant-col .gutter-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
90
src/assets/styles/itembox.css
Normal file
90
src/assets/styles/itembox.css
Normal file
@@ -0,0 +1,90 @@
|
||||
.text-ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
width: 250px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-details img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
object-fit: fill;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.product-details h2 {
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.characteristic {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.product-details button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.product-details button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.edit-button,
|
||||
.delete-button {
|
||||
flex: 1;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.edit-button:hover,
|
||||
.delete-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
35
src/assets/styles/modal.css
Normal file
35
src/assets/styles/modal.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.modal {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 20% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaaaaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
38
src/assets/styles/room-list.css
Normal file
38
src/assets/styles/room-list.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.list-container {
|
||||
width: 100%;
|
||||
max-height:500px;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.room-details {
|
||||
width: 100%;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.room-details:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.room-details .title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.room-details .characteristic {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.room-details .label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.room-details .value {
|
||||
color: #666;
|
||||
}
|
18
src/assets/styles/room-page.css
Normal file
18
src/assets/styles/room-page.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.manContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.topContainer {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.statsContainer, .listContainer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.detailContainer {
|
||||
height: 50%;
|
||||
}
|
47
src/assets/styles/room-stats.css
Normal file
47
src/assets/styles/room-stats.css
Normal file
@@ -0,0 +1,47 @@
|
||||
.room-stats-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.text-stats {
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.stat p {
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.stat ul li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.diagram {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
47
src/components/app-layout.jsx
Normal file
47
src/components/app-layout.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Layout, theme, Button } from 'antd';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
||||
import { Router } from '../router';
|
||||
import { usePageTitle } from '../hooks/page-title-context';
|
||||
import Navbar from './nav/Navbar';
|
||||
const { Header, Content, Footer, Sider } = Layout;
|
||||
|
||||
|
||||
|
||||
const AppLayout = ({ footer }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { pageTitle } = usePageTitle();
|
||||
const {
|
||||
token: { colorBgContainer, borderRadiusLG },
|
||||
} = theme.useToken();
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh', margin: '-8px -8px 0' }}>
|
||||
<Sider breakpoint="lg">
|
||||
<div className="demo-logo-vertical" />
|
||||
<Navbar />
|
||||
</Sider>
|
||||
<Layout style={{ flexGrow: 1 }}>
|
||||
<Header style={{ padding: 0, background: colorBgContainer }}>
|
||||
<Button type="text" icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} onClick={() => setCollapsed(!collapsed)} style={{ fontSize: '16px', width: 64, height: 64 }} />
|
||||
{pageTitle}
|
||||
</Header>
|
||||
|
||||
<Content style={{ margin: '24px 16px 0', overflowY: 'auto', overflowX: 'hidden' }}>
|
||||
<div style={{ padding: 24, background: colorBgContainer, borderRadius: borderRadiusLG, minHeight: '100%' }}>
|
||||
<Router />
|
||||
</div>
|
||||
</Content>
|
||||
<Footer style={{ textAlign: 'center' }}>
|
||||
{footer}
|
||||
<br />
|
||||
Ant Design <EFBFBD>{new Date().getFullYear()} Created by Ant UED
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
export default AppLayout;
|
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { AuthenticationProvider } from "../contexts";
|
||||
|
||||
export const Authenticated = ({ children }) => {
|
||||
|
||||
return <AuthenticationProvider>{children}</AuthenticationProvider>;
|
||||
};
|
37
src/components/diagram.jsx
Normal file
37
src/components/diagram.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Col, Row } from "antd";
|
||||
import "../assets/styles/diagram.css";
|
||||
|
||||
const Diagram = ({ data }) => {
|
||||
const [diagramData, setDiagramData] = useState(data);
|
||||
|
||||
useEffect(() => {
|
||||
setDiagramData(data);
|
||||
}, [data]);
|
||||
|
||||
const maxValue = Math.max(...diagramData.map((item) => item.value));
|
||||
|
||||
return (
|
||||
<div className="diagram-container">
|
||||
<h2>Prix total par an</h2>
|
||||
{diagramData.map(({ name, value }) => {
|
||||
// Appliquer l'<27>chelle logarithmique sur la valeur
|
||||
const scaledValue = Math.log(value + 1);
|
||||
const scaledMaxValue = Math.log(maxValue + 1);
|
||||
return (
|
||||
<div key={name} className="row-container">
|
||||
<div className="label">{name}</div>
|
||||
<div
|
||||
className="row"
|
||||
style={{ width: `${(scaledValue / scaledMaxValue) * 100}%` }}
|
||||
>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Diagram;
|
@@ -1,11 +1,15 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Input, InputNumber, Button, Select } from "antd";
|
||||
import { Form, Input, InputNumber, Button, Select, DatePicker } from "antd";
|
||||
import axios from "axios";
|
||||
import { createItem } from '../../api/item'
|
||||
|
||||
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
const dateFormat = "YYYY-MM-DD";
|
||||
|
||||
export const FormCreateItem = () => {
|
||||
export const FormCreateItem = ({ onClose }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
@@ -25,14 +29,9 @@ export const FormCreateItem = () => {
|
||||
}, []);
|
||||
|
||||
const onFinish = async (values) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${import.meta.env.VITE_API_URL}/item`,
|
||||
values,
|
||||
);
|
||||
console.log(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
let response = await createItem(values);
|
||||
if (response?.status >= 200 && response?.status < 300) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,7 +57,7 @@ export const FormCreateItem = () => {
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item label="Purchase Date" name="purchaseDate">
|
||||
<Input />
|
||||
<DatePicker format={dateFormat} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<TextArea rows={4} />
|
||||
|
@@ -1,19 +1,16 @@
|
||||
import React from "react";
|
||||
import { Form, Input, Button } from "antd";
|
||||
import { createRoom } from '../../api/room'
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
export const FormCreateRoom = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = async (values) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${import.meta.env.VITE_API_URL}/room`,
|
||||
values,
|
||||
);
|
||||
console.log(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
let response = await createRoom(values);
|
||||
if (response?.status >= 200 && response?.status < 300) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
|
120
src/components/form/formUpdateItem.jsx
Normal file
120
src/components/form/formUpdateItem.jsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Input, InputNumber, Button, Select, DatePicker } from "antd";
|
||||
import axios from "axios";
|
||||
import { getRooms } from "../../api/room";
|
||||
import { getItem, updateItem } from "../../api/item";
|
||||
import moment from "moment";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function formatItem(itemObj) {
|
||||
const {
|
||||
_id,
|
||||
brand,
|
||||
model,
|
||||
room,
|
||||
price,
|
||||
purchaseDate,
|
||||
description,
|
||||
categories,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
__v,
|
||||
link,
|
||||
} = itemObj;
|
||||
const formattedPurchaseDate =
|
||||
typeof purchaseDate === "string" ? new Date(purchaseDate) : purchaseDate;
|
||||
|
||||
return {
|
||||
_id,
|
||||
brand,
|
||||
model,
|
||||
room,
|
||||
price,
|
||||
purchaseDate: formattedPurchaseDate,
|
||||
description,
|
||||
categories,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
__v,
|
||||
link,
|
||||
};
|
||||
}
|
||||
|
||||
export const FormUpdateItem = ({ itemId, onClose }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [rooms, setRooms] = useState([]);
|
||||
const [item, setItem] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const roomsResponse = await getRooms();
|
||||
setRooms(roomsResponse);
|
||||
|
||||
const itemResponse = await getItem(itemId);
|
||||
console.log(itemResponse);
|
||||
setItem(formatItem(itemResponse));
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [itemId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
form.setFieldsValue({
|
||||
...item,
|
||||
purchaseDate: item.purchaseDate ? moment(item.purchaseDate) : null,
|
||||
});
|
||||
}
|
||||
}, [item, form]);
|
||||
|
||||
const onFinish = async (values) => {
|
||||
let response = await updateItem(item._id, values);
|
||||
if (response?.status >= 200 && response?.status < 300) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const dateFormat = "YYYY-MM-DD";
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
initialValues={item}
|
||||
>
|
||||
<h1>Update Item</h1>
|
||||
<Form.Item label="Brand" name="brand">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Model" name="model">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Room" name="room">
|
||||
<Select placeholder="Select a room">
|
||||
{console.log(rooms)}
|
||||
{rooms.map((room) => (
|
||||
<Option key={room._id} value={room._id}>
|
||||
{room.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="Price" name="price">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item label="Purchase Date" name="purchaseDate">
|
||||
<DatePicker dateFormat={dateFormat} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormUpdateItem;
|
62
src/components/form/formUpdateRoom.jsx
Normal file
62
src/components/form/formUpdateRoom.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {getRoom, updateRoom, deleteRoom } from '../../api/room'
|
||||
import { Form, Input, Button } from "antd";
|
||||
import axios from "axios";
|
||||
|
||||
export const FormUpdateRoom = ({_id}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [room, setRoom] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
console.log(_id)
|
||||
const roomsResponse = await getRoom(_id);
|
||||
setRoom(roomsResponse);
|
||||
console.log(roomsResponse);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (room) {
|
||||
form.setFieldsValue({
|
||||
...room
|
||||
});
|
||||
}
|
||||
}, [room, form]);
|
||||
|
||||
const onFinish = async (values) => {
|
||||
let response = await updateRoom(_id, values);
|
||||
if (response?.status >= 200 && response?.status < 300) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
let response = await deleteRoom(_id);
|
||||
if (response?.status >= 200 && response?.status < 300) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={onFinish}>
|
||||
<button onClick={onDelete}> Supprimer </button>
|
||||
<Form.Item
|
||||
label="Room Name"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "Please input the room name!" }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormUpdateRoom;
|
@@ -1,41 +1,64 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import "../../assets/styles/modal.css";
|
||||
import "../../assets/styles/itembox.css";
|
||||
import FormUpdateItem from "../form/formUpdateItem";
|
||||
import { Image } from "../parts/image";
|
||||
import { Description } from "../parts/description";
|
||||
import { Characteristic } from "../parts/characteristic";
|
||||
import { deleteItem } from '../../api/item'
|
||||
|
||||
// Composant Image atomique
|
||||
const Image = ({ src, alt }) => {
|
||||
return <img src={src} alt={alt} style={{ display: 'block', margin: 'auto' }} />;
|
||||
|
||||
export const ItemBox = ({ model, brand, purchaseDate, price, _id }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const openModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Composant Description atomique
|
||||
const Description = ({ title, children }) => {
|
||||
return (
|
||||
<div className="description">
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
// Composant Caractéristique atomique
|
||||
const Characteristic = ({ label, value }) => {
|
||||
return (
|
||||
<div className="characteristic">
|
||||
<strong>{label}:</strong> {value}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const onDelete = async () => {
|
||||
let response = await deleteItem(_id);
|
||||
if (response?.status >= 200 && response?.status < 300) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
let productname = brand + " " + model;
|
||||
let formatedPrice = price + "€";
|
||||
let formatedDate = new Date(purchaseDate).toLocaleDateString("fr-FR");
|
||||
|
||||
let request = brand + " " + model;
|
||||
|
||||
// Composant Détails du Produit (combinaison atomique)
|
||||
export const ItemBox = ({ imageUrl, model, brand, purchaseDate, price, onEdit }) => {
|
||||
return (
|
||||
<div className="product-details">
|
||||
<Image src={imageUrl} alt="Product" />
|
||||
<Description title="Product Information">
|
||||
<Description title={productname}>
|
||||
<Image request={request} alt="Product" />
|
||||
<Characteristic label="Model" value={model} />
|
||||
<Characteristic label="Brand" value={brand} />
|
||||
<Characteristic label="Purchase Date" value={purchaseDate} />
|
||||
<Characteristic label="Price" value={price} />
|
||||
<Characteristic label="Purchase Date" value={formatedDate} />
|
||||
<Characteristic label="Price" value={formatedPrice} />
|
||||
{/* Boutons d'édition et de suppression */}
|
||||
<div className="button-group">
|
||||
<button className="edit-button" onClick={openModal}>Edit</button>
|
||||
<button className="delete-button" onClick={onDelete}>Delete</button>
|
||||
</div>
|
||||
</Description>
|
||||
<button onClick={onEdit}>Edit</button>
|
||||
{/* Fenêtre modale */}
|
||||
{isModalOpen && (
|
||||
<div className="modal">
|
||||
<div className="modal-content">
|
||||
<span className="close" onClick={closeModal}>
|
||||
×
|
||||
</span>
|
||||
<FormUpdateItem itemId={_id}>
|
||||
{console.log("item ID :" + _id)}
|
||||
</FormUpdateItem>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Col, Row } from 'antd';
|
||||
import { ItemBox } from './ItemBox';
|
||||
|
||||
const GridCell = () => (
|
||||
<ItemBox
|
||||
imageUrl="https://www.shutterstock.com/image-vector/default-ui-image-placeholder-wireframes-600nw-1037719192.jpg"
|
||||
brandName="GlubGlub GabGalab"
|
||||
price={666}
|
||||
modelName="Satanus"
|
||||
purchaseDate="-6 avant Marcel PAGNOL"
|
||||
/>
|
||||
);
|
||||
|
||||
export const ItemGrid = () => (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}><GridCell /></Col>
|
||||
<Col span={6}><GridCell /></Col>
|
||||
<Col span={6}><GridCell /></Col>
|
||||
<Col span={6}><GridCell /></Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ItemGrid;
|
@@ -1,11 +1,16 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from 'axios'; // Assurez-vous que le chemin d'importation soit correct
|
||||
import { ItemBox } from './ItemBox';
|
||||
import axios from "axios";
|
||||
import { ItemBox } from "./ItemBox";
|
||||
import { DatePicker, Row, Col, Select, Input, InputNumber } from "antd";
|
||||
import "../../assets/styles/item-page.css";
|
||||
import FormCreateItem from "../form/formCreateItem";
|
||||
|
||||
const itemsPerPage = 4; // Nombre total d'items par page
|
||||
const itemsPerRow = 2; // Nombre d'items par rangée
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
|
||||
const itemsPerPage = 8;
|
||||
const itemsPerRow = 4;
|
||||
|
||||
// Fonction pour diviser le tableau d'items en rangées
|
||||
const chunkArray = (arr, size) => {
|
||||
const chunkedArr = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
@@ -14,10 +19,17 @@ const chunkArray = (arr, size) => {
|
||||
return chunkedArr;
|
||||
};
|
||||
|
||||
// Composant d'affichage de la page
|
||||
export const ItemPage = () => {
|
||||
const [items, setItems] = useState([]);
|
||||
const [filteredItems, setFilteredItems] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filterName, setFilterName] = useState("");
|
||||
const [filterPriceMin, setFilterPriceMin] = useState("");
|
||||
const [filterPriceMax, setFilterPriceMax] = useState("");
|
||||
const [filterDateRange, setFilterDateRange] = useState([]);
|
||||
const [selectedRoom, setSelectedRoom] = useState("all");
|
||||
const [rooms, setRooms] = useState([]);
|
||||
const [isCreateItemModalOpen, setIsCreateItemModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchItems = async () => {
|
||||
@@ -31,38 +43,169 @@ export const ItemPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRooms = async () => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${import.meta.env.VITE_API_URL}/room`,
|
||||
);
|
||||
setRooms(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchItems();
|
||||
fetchRooms();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const filtered = items.filter((item) => {
|
||||
const matchesRoom = selectedRoom === "all" || item.room === selectedRoom;
|
||||
const matchesName =
|
||||
item.brand.toLowerCase().includes(filterName.toLowerCase()) ||
|
||||
item.model.toLowerCase().includes(filterName.toLowerCase());
|
||||
const matchesPrice =
|
||||
(filterPriceMin === "" || item.price >= parseFloat(filterPriceMin)) &&
|
||||
(filterPriceMax === "" || item.price <= parseFloat(filterPriceMax));
|
||||
const matchesDate =
|
||||
!filterDateRange ||
|
||||
filterDateRange.length === 0 ||
|
||||
(filterDateRange[0] &&
|
||||
filterDateRange[1] &&
|
||||
new Date(item.purchaseDate) >= filterDateRange[0] &&
|
||||
new Date(item.purchaseDate) <= filterDateRange[1]);
|
||||
|
||||
return matchesRoom && matchesName && matchesPrice && matchesDate;
|
||||
});
|
||||
setFilteredItems(filtered);
|
||||
}, [
|
||||
items,
|
||||
selectedRoom,
|
||||
filterName,
|
||||
filterPriceMin,
|
||||
filterPriceMax,
|
||||
filterDateRange,
|
||||
]);
|
||||
|
||||
const handleNextPage = () => {
|
||||
setCurrentPage(prevPage => prevPage + 1);
|
||||
setCurrentPage((prevPage) => prevPage + 1);
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
setCurrentPage(prevPage => prevPage - 1);
|
||||
setCurrentPage((prevPage) => prevPage - 1);
|
||||
};
|
||||
|
||||
// Divise les items en rangées
|
||||
const chunkedItems = chunkArray(items, itemsPerRow);
|
||||
// Calcule l'index de la première et de la dernière rangée à afficher sur la page courante
|
||||
const handleRoomChange = (value) => {
|
||||
setSelectedRoom(value);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsCreateItemModalOpen(false);
|
||||
};
|
||||
|
||||
const chunkedItems = chunkArray(filteredItems, itemsPerRow);
|
||||
const startIndex = (currentPage - 1) * (itemsPerPage / itemsPerRow);
|
||||
const endIndex = startIndex + (itemsPerPage / itemsPerRow);
|
||||
// Sélectionne les rangées à afficher sur la page courante
|
||||
const endIndex = startIndex + itemsPerPage / itemsPerRow;
|
||||
const rowsToDisplay = chunkedItems.slice(startIndex, endIndex);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{rowsToDisplay.map((row, index) => (
|
||||
<div key={index} className="item-row">
|
||||
{row.map(item => (
|
||||
<ItemBox key={item._id} {...item} />
|
||||
<div className="item-container">
|
||||
<div className="filters">
|
||||
<div className="filter-group">
|
||||
<label htmlFor="roomSelect">Room:</label>
|
||||
<Select
|
||||
id="roomSelect"
|
||||
defaultValue="all"
|
||||
style={{ width: 120 }}
|
||||
onChange={handleRoomChange}
|
||||
>
|
||||
<Option value="all">All</Option>
|
||||
{rooms.map((room) => (
|
||||
<Option key={room._id} value={room._id}>
|
||||
{room.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label htmlFor="nameInput">Search by name:</label>
|
||||
<Input
|
||||
id="nameInput"
|
||||
placeholder="Search by name"
|
||||
value={filterName}
|
||||
onChange={(e) => setFilterName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Price:</label>
|
||||
<InputNumber
|
||||
placeholder="Min price"
|
||||
value={filterPriceMin}
|
||||
onChange={(value) => setFilterPriceMin(value)}
|
||||
/>
|
||||
<span> - </span>
|
||||
<InputNumber
|
||||
placeholder="Max price"
|
||||
value={filterPriceMax}
|
||||
onChange={(value) => setFilterPriceMax(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Purchase Date:</label>
|
||||
<RangePicker
|
||||
format="YYYY-MM-DD"
|
||||
onChange={(dates) => setFilterDateRange(dates)}
|
||||
value={filterDateRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Button to open create item modal */}
|
||||
<button onClick={() => setIsCreateItemModalOpen(true)}>
|
||||
Create Item
|
||||
</button>
|
||||
|
||||
{/* Create item form modal */}
|
||||
{isCreateItemModalOpen && (
|
||||
<div className="modal">
|
||||
<div className="modal-content">
|
||||
<span className="close" onClick={handleCloseModal}>
|
||||
×
|
||||
</span>
|
||||
<FormCreateItem onClose={handleCloseModal} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="item-list">
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
|
||||
{rowsToDisplay.map((row, rowIndex) => (
|
||||
<React.Fragment key={rowIndex}>
|
||||
{row.map((item) => (
|
||||
<Col
|
||||
className="gutter-row"
|
||||
span={24 / itemsPerRow}
|
||||
key={item._id}
|
||||
>
|
||||
<ItemBox {...item} />
|
||||
</Col>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div className="pagination">
|
||||
<button onClick={handlePrevPage} disabled={currentPage === 1}>Previous</button>
|
||||
<button onClick={handlePrevPage} disabled={currentPage === 1}>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {currentPage}</span>
|
||||
<button onClick={handleNextPage} disabled={endIndex >= chunkedItems.length}>Next</button>
|
||||
<button
|
||||
onClick={handleNextPage}
|
||||
disabled={endIndex >= chunkedItems.length}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
51
src/components/item/RoomItemsList.jsx
Normal file
51
src/components/item/RoomItemsList.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
export const RoomItemsList = () => {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRoomsAndItems = async () => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${import.meta.env.VITE_API_URL}/room`,
|
||||
);
|
||||
const roomsWithItems = await Promise.all(
|
||||
response.data.map(async (room) => {
|
||||
const itemsResponse = await axios.get(
|
||||
`${import.meta.env.VITE_API_URL}/item?room=${room._id}`,
|
||||
);
|
||||
const itemsForThisRoom = itemsResponse.data.filter(
|
||||
(item) => item.room === room._id,
|
||||
);
|
||||
return { ...room, items: itemsForThisRoom };
|
||||
}),
|
||||
);
|
||||
setRooms(roomsWithItems);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
fetchRoomsAndItems();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{rooms.map((room) => (
|
||||
<div key={room._id}>
|
||||
<h2>{room.name}</h2>
|
||||
{Array.isArray(room.items) &&
|
||||
room.items.map((item) => {
|
||||
return (
|
||||
<div key={item._id}>
|
||||
<p>{item.brand}</p>
|
||||
<p>{item.model}</p>
|
||||
<p>{item.price}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
73
src/components/nav/Navbar.jsx
Normal file
73
src/components/nav/Navbar.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Menu } from "antd";
|
||||
import { HomeOutlined } from "@ant-design/icons";
|
||||
import { Home } from "../../pages";
|
||||
import { getRooms } from "../../api/room";
|
||||
import { isLoggedIn } from "../../api/authentication";
|
||||
import { Items } from "../../pages/items";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
function getItem(key, label) {
|
||||
//return {
|
||||
// key: String(key),
|
||||
// icon,
|
||||
// children,
|
||||
// label,
|
||||
// link,
|
||||
//};
|
||||
var item = (
|
||||
<Menu.Item key={key}>
|
||||
<Link to="/test">{label}</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
return item;
|
||||
}
|
||||
|
||||
const { SubMenu } = Menu;
|
||||
|
||||
const Navbar = () => {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log("NAVBAR EFFECT");
|
||||
isLoggedIn().then((user) => {
|
||||
if (user !== "Unauthorized") {
|
||||
getRooms().then((result) => {
|
||||
setRooms(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const items = [
|
||||
<Menu.Item key="0">
|
||||
<Link to="/logout">Login / Logout</Link>
|
||||
</Menu.Item>,
|
||||
<Menu.Item key="1">
|
||||
<Link to="/home">Menu Principal</Link>
|
||||
</Menu.Item>,
|
||||
<Menu.Item key="2">
|
||||
<Link to="/items">Tous les articles</Link>
|
||||
</Menu.Item>,
|
||||
<Menu.Item key="3">
|
||||
<Link to="/rooms">Voir les chambres</Link>
|
||||
</Menu.Item>,
|
||||
];
|
||||
|
||||
try {
|
||||
return (
|
||||
<Menu
|
||||
defaultSelectedKeys={["0"]}
|
||||
defaultOpenKeys={["sub1"]}
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
>
|
||||
{items}
|
||||
</Menu>
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error.stack);
|
||||
}
|
||||
};
|
||||
export default Navbar;
|
9
src/components/parts/characteristic.jsx
Normal file
9
src/components/parts/characteristic.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export const Characteristic = ({ label, value }) => {
|
||||
return (
|
||||
<div className="characteristic">
|
||||
<strong>{label}:</strong> {value}
|
||||
</div>
|
||||
);
|
||||
};
|
10
src/components/parts/description.jsx
Normal file
10
src/components/parts/description.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export const Description = ({ title, children }) => {
|
||||
return (
|
||||
<div className="description">
|
||||
<h2 className="text-ellipsis">{title}</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
33
src/components/parts/image.jsx
Normal file
33
src/components/parts/image.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { searchAndResizeImage } from '../../api/image-request'
|
||||
|
||||
export const Image = ({ src, alt, request }) => {
|
||||
const [cacheUrl, setCacheUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
let cachedUrl = localStorage.getItem(request);
|
||||
if (!cachedUrl) {
|
||||
try {
|
||||
cachedUrl = await searchAndResizeImage(request);
|
||||
localStorage.setItem(request, cachedUrl);
|
||||
console.log("Mise en cache de l'image avec la requ<71>te : " + request);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la r<>cup<75>ration de l'image : ", error);
|
||||
}
|
||||
}
|
||||
setCacheUrl(cachedUrl);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [request]);
|
||||
|
||||
if (src) {
|
||||
return <img src={src} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />;
|
||||
} else if (cacheUrl) {
|
||||
return <img src={cacheUrl} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />;
|
||||
} else {//default image
|
||||
return <img src={"https://cdn.discordapp.com/attachments/1164176196930637956/1213857681362784266/dinorundiscord.gif?ex=6642258f&is=6640d40f&hm=cf0ca54df3e002a15049618a6654b22c5d0c7943dc420e84936635725aceb90f&"} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />;
|
||||
}
|
||||
}
|
39
src/components/rooms/room-detail.jsx
Normal file
39
src/components/rooms/room-detail.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormCreateRoom } from "../../components/form/formCreateRoom";
|
||||
import { FormUpdateRoom } from "../../components/form/formUpdateRoom";
|
||||
|
||||
export const RoomDetail = ({
|
||||
selectedRoomParam,
|
||||
onCreateFormSubmit
|
||||
}) => {
|
||||
const [isUpdateFormVisible, setIsUpdateFormVisible] = useState(false);
|
||||
const [selectedRoom, setSelectedRoom] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRoomParam) {
|
||||
setSelectedRoom(selectedRoomParam)
|
||||
setIsUpdateFormVisible(true);
|
||||
}
|
||||
}, [selectedRoomParam]);
|
||||
|
||||
const handleBackClick = () => {
|
||||
setIsUpdateFormVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="room-detail">
|
||||
{isUpdateFormVisible ? (
|
||||
<div>
|
||||
<h2>Modifier une chambre</h2>
|
||||
<button onClick={handleBackClick}>Annuler </button>
|
||||
<FormUpdateRoom _id={selectedRoom} />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h2>Ajouter une chambre</h2>
|
||||
<FormCreateRoom onSubmit={onCreateFormSubmit} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
34
src/components/rooms/room-list.jsx
Normal file
34
src/components/rooms/room-list.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import "../../assets/styles/room-list.css";
|
||||
import { RoomBox } from "../../components/rooms/roomBox";
|
||||
import { formatRoomStats } from "../../api/room";
|
||||
|
||||
|
||||
|
||||
export const RoomList = ({ statsParam, onRoomClick }) => {
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (statsParam.rooms) {
|
||||
console.log(statsParam.rooms);
|
||||
let formatedStats = formatRoomStats(statsParam);
|
||||
setRooms(formatedStats.rooms);
|
||||
}
|
||||
}, [statsParam]);
|
||||
|
||||
const handleRoomClick = (roomId) => {
|
||||
onRoomClick(roomId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Liste des chambres</h2>
|
||||
<div className="list-container">
|
||||
{rooms &&
|
||||
rooms.map((room) => (
|
||||
<RoomBox room={room} key={room._id} onRoomClick={handleRoomClick} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
69
src/components/rooms/room-stats.jsx
Normal file
69
src/components/rooms/room-stats.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { formatRoomStats } from '../../api/room'
|
||||
import Diagram from '../diagram';
|
||||
import '../../assets/styles/room-stats.css'
|
||||
|
||||
|
||||
|
||||
const RoomStats = ({statsParam}) => {
|
||||
const [stats, setStats] = useState();
|
||||
const [diagramValues, setDiagramValues] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
if (statsParam.global) {
|
||||
console.log(statsParam)
|
||||
let formatedStats = formatRoomStats(statsParam)
|
||||
setStats(formatedStats)
|
||||
}
|
||||
}, [statsParam]);
|
||||
|
||||
useEffect(() => {
|
||||
if (stats) {
|
||||
let table = Object.entries(stats.years)
|
||||
.filter(([year, value]) => value !== 0)
|
||||
.map(([year, value]) => ({ name: year, value: value }));
|
||||
setDiagramValues(table);
|
||||
}
|
||||
}, [stats])
|
||||
|
||||
return (
|
||||
<div className="room-stats-container">
|
||||
<div className="text-stats">
|
||||
<h2>Statistiques des chambres</h2>
|
||||
<div className="stats-info">
|
||||
<div className="stat">
|
||||
<p>Total des chambres :</p>
|
||||
<span>{stats?.global?.rooms_count}</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<p>Total des articles :</p>
|
||||
<span>{stats?.global?.items_count}</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<p>Prix total :</p>
|
||||
<span>{stats?.global?.total_price}</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<p>La chambre avec le plus d'articles :</p>
|
||||
<ul>
|
||||
<li><span>{stats?.global.most_item_room?.name}</span></li>
|
||||
<li>Nombre d'articles : <span>{stats?.global?.most_item_room?.count}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<p>La chambre la plus ch<EFBFBD>re :</p>
|
||||
<ul>
|
||||
<li><span>{stats?.global?.most_expensive_room?.name}</span></li>
|
||||
<li>Prix total : <span>{stats?.global?.most_expensive_room?.count}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="diagram">
|
||||
<Diagram data={diagramValues} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default RoomStats;
|
23
src/components/rooms/roomBox.jsx
Normal file
23
src/components/rooms/roomBox.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import "../../assets/styles/room-list.css";
|
||||
import { Description } from "../parts/description";
|
||||
import { Characteristic } from "../parts/characteristic";
|
||||
|
||||
export const RoomBox = ({ room, onRoomClick }) => {
|
||||
const handleBoxClick = () => {
|
||||
onRoomClick(room._id);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="room-details" onClick={handleBoxClick}>
|
||||
<Description title={room?.name}>
|
||||
<Characteristic
|
||||
label="Nombre d'articles"
|
||||
value={room?.items_count || "N/A"}
|
||||
/>
|
||||
<Characteristic label="Prix total" value={room?.room_price || "N/A"} />
|
||||
</Description>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,2 +1,3 @@
|
||||
export * from "./use-auth";
|
||||
export * from "./use-query";
|
||||
export * from "./page-title-context";
|
||||
|
23
src/hooks/page-title-context.jsx
Normal file
23
src/hooks/page-title-context.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const PageTitleContext = createContext();
|
||||
|
||||
// Composant de fournisseur de titre de page
|
||||
export const PageTitleProvider = ({ children }) => {
|
||||
const [pageTitle, setPageTitle] = useState("");
|
||||
|
||||
return (
|
||||
<PageTitleContext.Provider value={{ pageTitle, setPageTitle }}>
|
||||
{children}
|
||||
</PageTitleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook pour r<>cup<75>rer le titre de la page
|
||||
export const usePageTitle = () => {
|
||||
const context = useContext(PageTitleContext);
|
||||
if (!context) {
|
||||
throw new Error('usePageTitle must be used within a PageTitleProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
@@ -5,3 +5,7 @@ import { AuthenticationContext } from "../contexts";
|
||||
export function useAuth() {
|
||||
return React.useContext(AuthenticationContext);
|
||||
}
|
||||
|
||||
export function useGetAuth() {
|
||||
return React.useContext(AuthenticationContext).user;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user