Compare commits

...

18 Commits

Author SHA1 Message Date
Victor
05b0a4514b changement test items 2024-05-13 21:23:40 +02:00
560226b6a8 update README 2024-05-13 19:54:18 +02:00
4e6a03df17 room creation fix + README update 2024-05-13 19:53:43 +02:00
22ec7f8fdd register + deletions + image request changes 2024-05-13 19:42:49 +02:00
baf31f7959 update rooms 2024-05-13 18:12:59 +02:00
1e3dcf340d commit final 2024-05-13 01:42:15 +02:00
Victor
6506d6817b fix date reset 2024-05-13 00:59:33 +02:00
Victor
4cf9069bf2 premier commit final 2024-05-13 00:33:13 +02:00
68cd3397d4 Merge branch 'main' of https://grond.iut-fbleau.fr/descampsv/2024-DEV-BUT3 2024-05-13 00:13:57 +02:00
4985dd9ec2 room list and create room 2024-05-13 00:13:52 +02:00
Victor
2868efcda4 fix jsp pein de truc en vrais 2024-05-13 00:13:34 +02:00
Victor
18b7052259 ajout test 2024-05-12 23:25:05 +02:00
35b6d419e1 fix 2024-05-12 21:51:26 +02:00
2444f620f5 fix 2024-05-12 21:44:42 +02:00
aa59eaa3fc wip 2024-05-12 21:32:30 +02:00
af562b7b10 merge 2024-05-12 18:51:25 +02:00
2b65a6fef2 Merge branch 'main' of https://grond.iut-fbleau.fr/descampsv/2024-DEV-BUT3 2024-05-12 18:50:48 +02:00
94b6cdb9ba tests Julien 2024-05-12 18:48:39 +02:00
40 changed files with 2635 additions and 1347 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,14 @@
"", "",
"\\src", "\\src",
"\\src\\api", "\\src\\api",
"\\src\\hooks", "\\src\\assets",
"\\src\\pages", "\\src\\components",
"\\tests-examples", "\\src\\components\\form",
"\\__tests__", "\\src\\components\\item",
"\\__tests__\\api", "\\src\\components\\nav",
"\\__tests__\\pages", "\\src\\components\\parts",
"\\__tests__\\pages\\authenticated" "\\src\\components\\rooms"
], ],
"SelectedNode": "\\src\\pages\\rooms-stats.jsx", "SelectedNode": "\\README.md",
"PreviewInSolutionExplorer": false "PreviewInSolutionExplorer": false
} }

Binary file not shown.

View File

@@ -11,6 +11,25 @@ Victor DESCAMPS
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. 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. 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 ## Run Locally

View File

@@ -1,10 +1,11 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { searchAndResizeImage } from "../../src/api/image-request" 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 // 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", () => {
describe("Image request API", () => {
it();
//it("return a string", async () => { //it("return a string", async () => {
// let query = 'cat'; // let query = 'cat';
@@ -24,4 +25,4 @@ import { searchAndResizeImage } from "../../src/api/image-request"
// const imageUrl = await searchAndResizeImage(query); // const imageUrl = await searchAndResizeImage(query);
// expect(imageUrl).toEqual(''); // expect(imageUrl).toEqual('');
//}); //});
//}); });

View 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 });
});*/
});

1644
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-cookie": "^7.0.2", "react-cookie": "^7.0.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.21.3" "react-router-dom": "^6.23.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.42.1", "@playwright/test": "^1.42.1",

View File

@@ -6,7 +6,7 @@ export const isLoggedIn = async () => {
return response.data; return response.data;
} catch (error) { } 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 }); const response = await axios.post("/authenticate", { username, password });
return response.data; return response.data;
} catch (error) { } catch (error) {
return error.response.data; return error.response;
} }
}; };
@@ -25,6 +25,6 @@ export const logout = async () => {
return response.data; return response.data;
} catch (error) { } catch (error) {
return error.response.data; return error.response;
} }
}; };

View File

@@ -24,3 +24,45 @@ export const getItems = async () => {
return 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);
}
}

View File

@@ -5,7 +5,8 @@ import axios from "axios";
export const getRoom = async (_id) => { export const getRoom = async (_id) => {
try { try {
const response = await axios.get("/room", {_id}); let id = _id.toString()
const response = await axios.get(`/room/${id}`);
console.log(response.data) console.log(response.data)
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -16,7 +17,7 @@ export const getRoom = async (_id) => {
export const getRooms = async () => { export const getRooms = async () => {
try { try {
const response = await axios.get("/room", {}); const response = await axios.get("/room");
console.log(response.data) console.log(response.data)
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -24,3 +25,110 @@ export const getRooms = async () => {
return 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
};
}

View 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;
}

View File

@@ -78,8 +78,7 @@
} }
.item-list { .item-list {
width: 100%; width: 50vw;
max-width:50vw;
} }
.pagination { .pagination {
@@ -87,12 +86,10 @@
} }
/* Style pour les boutons de pagination */
.pagination button { .pagination button {
margin: 0 5px; margin: 0 5px;
} }
/* Style pour les ItemBox */
.item-container .item-list .ant-col { .item-container .item-list .ant-col {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -9,7 +9,7 @@
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
margin-bottom: 20px; margin-bottom: 20px;
width: 250px; /* Largeur ajustable en fonction de votre mise en page */ width: 250px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
@@ -57,3 +57,34 @@
.product-details button:hover { .product-details button:hover {
background-color: #0056b3; 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;
}

View File

@@ -1,4 +1,3 @@
/* CSS pour la fen<65>tre modale */
.modal { .modal {
display: flex; display: flex;
justify-content: center; justify-content: center;

View 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;
}

View 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%;
}

View 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);
}

View File

@@ -1,7 +1,8 @@
import React from "react"; import React, { useContext } from "react";
import { AuthenticationProvider } from "../contexts"; import { AuthenticationProvider } from "../contexts";
export const Authenticated = ({ children }) => { export const Authenticated = ({ children }) => {
return <AuthenticationProvider>{children}</AuthenticationProvider>; return <AuthenticationProvider>{children}</AuthenticationProvider>;
}; };

View 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;

View File

@@ -1,11 +1,15 @@
import React, { useState, useEffect } from "react"; 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 axios from "axios";
import { createItem } from '../../api/item'
const { TextArea } = Input; const { TextArea } = Input;
const { Option } = Select; const { Option } = Select;
const dateFormat = "YYYY-MM-DD";
export const FormCreateItem = () => { export const FormCreateItem = ({ onClose }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [rooms, setRooms] = useState([]); const [rooms, setRooms] = useState([]);
@@ -25,14 +29,9 @@ export const FormCreateItem = () => {
}, []); }, []);
const onFinish = async (values) => { const onFinish = async (values) => {
try { let response = await createItem(values);
const response = await axios.post( if (response?.status >= 200 && response?.status < 300) {
`${import.meta.env.VITE_API_URL}/item`, window.location.reload();
values,
);
console.log(response.data);
} catch (error) {
console.error(error);
} }
}; };
@@ -58,7 +57,7 @@ export const FormCreateItem = () => {
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
<Form.Item label="Purchase Date" name="purchaseDate"> <Form.Item label="Purchase Date" name="purchaseDate">
<Input /> <DatePicker format={dateFormat} />
</Form.Item> </Form.Item>
<Form.Item label="Description" name="description"> <Form.Item label="Description" name="description">
<TextArea rows={4} /> <TextArea rows={4} />

View File

@@ -1,19 +1,16 @@
import React from "react"; import React from "react";
import { Form, Input, Button } from "antd"; import { Form, Input, Button } from "antd";
import { createRoom } from '../../api/room'
import axios from "axios"; import axios from "axios";
export const FormCreateRoom = () => { export const FormCreateRoom = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const onFinish = async (values) => { const onFinish = async (values) => {
try { let response = await createRoom(values);
const response = await axios.post( if (response?.status >= 200 && response?.status < 300) {
`${import.meta.env.VITE_API_URL}/room`, window.location.reload();
values,
);
console.log(response.data);
} catch (error) {
console.error(error);
} }
}; };

View File

@@ -2,26 +2,13 @@ import React, { useState, useEffect } from "react";
import { Form, Input, InputNumber, Button, Select, DatePicker } from "antd"; import { Form, Input, InputNumber, Button, Select, DatePicker } from "antd";
import axios from "axios"; import axios from "axios";
import { getRooms } from "../../api/room"; import { getRooms } from "../../api/room";
import { getItem } from "../../api/item"; import { getItem, updateItem } from "../../api/item";
import moment from 'moment'; import moment from "moment";
const { Option } = Select; const { Option } = Select;
function formatItem(itemObj) {
function formatItem(itemObj) {//_id, brand, model, room, price, purchaseDate, description, categories, createdAt, updatedAt, __v, link) { const {
let _id = itemObj._id
let brand = itemObj.brand
let model = itemObj.model
let room = itemObj.room
let price = itemObj.price
let purchaseDate = new Date(itemObj.purchaseDate)
let description = itemObj.description
let categories = itemObj.categories
let createdAt = itemObj.createdAt
let updatedAt = itemObj.updatedAt
let __v = itemObj.__v
let link = itemObj.link
let item = {
_id, _id,
brand, brand,
model, model,
@@ -33,15 +20,31 @@ function formatItem(itemObj) {//_id, brand, model, room, price, purchaseDate, de
createdAt, createdAt,
updatedAt, updatedAt,
__v, __v,
link link,
} } = itemObj;
return item; 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 }) => { export const FormUpdateItem = ({ itemId, onClose }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [rooms, setRooms] = useState([]); const [rooms, setRooms] = useState([]);
const [item, setItem] = useState(null) const [item, setItem] = useState(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -49,7 +52,7 @@ export const FormUpdateItem = ({ itemId }) => {
setRooms(roomsResponse); setRooms(roomsResponse);
const itemResponse = await getItem(itemId); const itemResponse = await getItem(itemId);
console.log(itemResponse) console.log(itemResponse);
setItem(formatItem(itemResponse)); setItem(formatItem(itemResponse));
}; };
@@ -66,23 +69,18 @@ export const FormUpdateItem = ({ itemId }) => {
}, [item, form]); }, [item, form]);
const onFinish = async (values) => { const onFinish = async (values) => {
try { let response = await updateItem(item._id, values);
const response = await axios.put( if (response?.status >= 200 && response?.status < 300) {
`${import.meta.env.VITE_API_URL}/item/${item._id}`, window.location.reload();
values,
);
console.log(response.data);
} catch (error) {
console.error(error);
} }
}; };
const dateFormat = 'YYYY-MM-DD'; const dateFormat = "YYYY-MM-DD";
return ( return (
<Form <Form
form={form} form={form}
onFinish={onFinish} onFinish={onFinish}
initialValues={item} // Initialise le formulaire avec les valeurs de l'<EFBFBD>l<EFBFBD>ment initialValues={item}
> >
<h1>Update Item</h1> <h1>Update Item</h1>
<Form.Item label="Brand" name="brand"> <Form.Item label="Brand" name="brand">
@@ -94,8 +92,7 @@ export const FormUpdateItem = ({ itemId }) => {
<Form.Item label="Room" name="room"> <Form.Item label="Room" name="room">
<Select placeholder="Select a room"> <Select placeholder="Select a room">
{console.log(rooms)} {console.log(rooms)}
{ {rooms.map((room) => (
rooms.map((room) => (
<Option key={room._id} value={room._id}> <Option key={room._id} value={room._id}>
{room.name} {room.name}
</Option> </Option>
@@ -106,9 +103,7 @@ export const FormUpdateItem = ({ itemId }) => {
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
<Form.Item label="Purchase Date" name="purchaseDate"> <Form.Item label="Purchase Date" name="purchaseDate">
<DatePicker <DatePicker dateFormat={dateFormat} />
dateFormat={dateFormat}
/>
</Form.Item> </Form.Item>
<Form.Item label="Description" name="description"> <Form.Item label="Description" name="description">
<Input.TextArea rows={4} /> <Input.TextArea rows={4} />

View 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;

View File

@@ -1,104 +1,61 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from "react";
import { searchAndResizeImage } from '../../api/image-request' import "../../assets/styles/modal.css";
import '../../assets/styles/modal.css' import "../../assets/styles/itembox.css";
import '../../assets/styles/itembox.css' import FormUpdateItem from "../form/formUpdateItem";
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
const Image = ({ src, alt, request, _id }) => {
const [cacheUrl, setCacheUrl] = useState(null);
useEffect(() => {
const fetchData = async () => {
let cachedUrl = localStorage.getItem(_id);
if (!cachedUrl) {
try {
cachedUrl = await searchAndResizeImage(request);
localStorage.setItem(_id, cachedUrl);
console.log("Mise en cache de l'image avec l'ID : " + _id);
} catch (error) {
console.error("Erreur lors de la récupération de l'image : ", error);
}
}
setCacheUrl(cachedUrl);
};
fetchData();
}, [request, _id]);
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 {
return <img src={"https://media.discordapp.net/attachments/1164176196930637956/1167746303820840990/IMG_20231028_104620.jpg?ex=663ddefe&is=663c8d7e&hm=0985ce123fd1751f65388f7fefde5db6ce817e514e30f1d3c81eb28b15e78453&=&"} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />;
}
};
// Composant Description
const Description = ({ title, children }) => {
return (
<div className="description">
<h2 className="text-ellipsis">{title}</h2>
{children}
</div>
);
};
// Composant Caractéristique
const Characteristic = ({ label, value }) => {
return (
<div className="characteristic">
<strong>{label}:</strong> {value}
</div>
);
};
// Composant Détails du Produit
export const ItemBox = ({ model, brand, purchaseDate, price, _id }) => { export const ItemBox = ({ model, brand, purchaseDate, price, _id }) => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
// Fonction pour ouvrir la fenêtre modale
const openModal = () => { const openModal = () => {
setIsModalOpen(true); setIsModalOpen(true);
}; };
// Fonction pour fermer la fenêtre modale
const closeModal = () => { const closeModal = () => {
setIsModalOpen(false); setIsModalOpen(false);
}; };
const onDelete = async () => {
let response = await deleteItem(_id);
if (response?.status >= 200 && response?.status < 300) {
window.location.reload();
}
}
let productname = brand + " " + model; let productname = brand + " " + model;
let formatedPrice = price + "€"; let formatedPrice = price + "€";
let formatedDate = new Date(purchaseDate).toLocaleDateString('fr-FR'); let formatedDate = new Date(purchaseDate).toLocaleDateString("fr-FR");
let request = brand + " " + model let request = brand + " " + model;
return ( return (
<div className="product-details"> <div className="product-details">
<Description title={productname}> <Description title={productname}>
<Image request={request} _id={_id} alt="Product" /> <Image request={request} alt="Product" />
<Characteristic label="Model" value={model} /> <Characteristic label="Model" value={model} />
<Characteristic label="Brand" value={brand} /> <Characteristic label="Brand" value={brand} />
<Characteristic label="Purchase Date" value={formatedDate} /> <Characteristic label="Purchase Date" value={formatedDate} />
<Characteristic label="Price" value={formatedPrice} /> <Characteristic label="Price" value={formatedPrice} />
{/* Bouton d'édition pour ouvrir la fenêtre modale */} {/* Boutons d'édition et de suppression */}
<button onClick={openModal}>Edit</button> <div className="button-group">
<button className="edit-button" onClick={openModal}>Edit</button>
<button className="delete-button" onClick={onDelete}>Delete</button>
</div>
</Description> </Description>
{/* Fenêtre modale */} {/* Fenêtre modale */}
{isModalOpen && ( {isModalOpen && (
<div className="modal"> <div className="modal">
<div className="modal-content"> <div className="modal-content">
<span className="close" onClick={closeModal}>&times;</span> <span className="close" onClick={closeModal}>
<FormUpdateItem itemId={_id}>{console.log("item ID :" + _id)}</FormUpdateItem> &times;
</span>
<FormUpdateItem itemId={_id}>
{console.log("item ID :" + _id)}
</FormUpdateItem>
</div> </div>
</div> </div>
)} )}

View File

@@ -1,18 +1,16 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import axios from 'axios'; // Assurez-vous que le chemin d'importation soit correct import axios from "axios";
import { ItemBox } from './ItemBox'; import { ItemBox } from "./ItemBox";
import { Space, DatePicker, Row, Col, Select, Input, InputNumber } from 'antd'; import { DatePicker, Row, Col, Select, Input, InputNumber } from "antd";
import '../../assets/styles/item-page.css' import "../../assets/styles/item-page.css";
import FormCreateItem from "../form/formCreateItem";
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
const { Option } = Select; const { Option } = Select;
const itemsPerPage = 8; // Nombre total d'items par page const itemsPerPage = 8;
const itemsPerRow = 4; // Nombre d'items par rangée const itemsPerRow = 4;
// Fonction pour diviser le tableau d'items en rangées
const chunkArray = (arr, size) => { const chunkArray = (arr, size) => {
const chunkedArr = []; const chunkedArr = [];
for (let i = 0; i < arr.length; i += size) { for (let i = 0; i < arr.length; i += size) {
@@ -21,18 +19,17 @@ const chunkArray = (arr, size) => {
return chunkedArr; return chunkedArr;
}; };
// Composant d'affichage de la page
export const ItemPage = () => { export const ItemPage = () => {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]); // State pour les items filtrés const [filteredItems, setFilteredItems] = useState([]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [filterName, setFilterName] = useState(''); const [filterName, setFilterName] = useState("");
const [filterPriceMin, setFilterPriceMin] = useState(''); const [filterPriceMin, setFilterPriceMin] = useState("");
const [filterPriceMax, setFilterPriceMax] = useState(''); const [filterPriceMax, setFilterPriceMax] = useState("");
const [filterDateRange, setFilterDateRange] = useState([]); const [filterDateRange, setFilterDateRange] = useState([]);
const [selectedRoom, setSelectedRoom] = useState('all'); // State pour la chambre sélectionnée const [selectedRoom, setSelectedRoom] = useState("all");
const [rooms, setRooms] = useState([]);
const [rooms, setRooms] = useState([]); // State pour stocker les chambres const [isCreateItemModalOpen, setIsCreateItemModalOpen] = useState(false);
useEffect(() => { useEffect(() => {
const fetchItems = async () => { const fetchItems = async () => {
@@ -40,20 +37,8 @@ export const ItemPage = () => {
const response = await axios.get( const response = await axios.get(
`${import.meta.env.VITE_API_URL}/item`, `${import.meta.env.VITE_API_URL}/item`,
); );
//await response.data.forEach(async (item) => {
// let cacheUrl = localStorage.getItem(item._id);
// if (cacheUrl == null || cacheUrl == "") {
// let request = item.brand + " " + item.model
// cacheUrl = await searchAndResizeImage(request)
// await localStorage.setItem(item._id, cacheUrl)
// console.log("mise en cache "+ item.imageUrl)
// }
// item.imageUrl = cacheUrl;
//})
setItems(response.data); setItems(response.data);
} } catch (error) {
catch (error) {
console.error(error); console.error(error);
} }
}; };
@@ -74,77 +59,134 @@ export const ItemPage = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
// Filtrer les éléments chaque fois que les filtres ou la chambre sélectionnée changent const filtered = items.filter((item) => {
const filtered = items.filter(item => { const matchesRoom = selectedRoom === "all" || item.room === selectedRoom;
// Filtrer par chambre si une chambre est sélectionnée const matchesName =
const matchesRoom = selectedRoom === 'all' || item.room === selectedRoom; item.brand.toLowerCase().includes(filterName.toLowerCase()) ||
// Filtrer par nom (brand ou model)
const matchesName = item.brand.toLowerCase().includes(filterName.toLowerCase()) ||
item.model.toLowerCase().includes(filterName.toLowerCase()); item.model.toLowerCase().includes(filterName.toLowerCase());
// Filtrer par prix
const matchesPrice = const matchesPrice =
(filterPriceMin === '' || item.price >= parseFloat(filterPriceMin)) && (filterPriceMin === "" || item.price >= parseFloat(filterPriceMin)) &&
(filterPriceMax === '' || item.price <= parseFloat(filterPriceMax)); (filterPriceMax === "" || item.price <= parseFloat(filterPriceMax));
// Filtrer par date const matchesDate =
const matchesDate = filterDateRange.length === 0 || ( !filterDateRange ||
filterDateRange.length === 0 ||
(filterDateRange[0] &&
filterDateRange[1] &&
new Date(item.purchaseDate) >= filterDateRange[0] && new Date(item.purchaseDate) >= filterDateRange[0] &&
new Date(item.purchaseDate) <= filterDateRange[1] new Date(item.purchaseDate) <= filterDateRange[1]);
);
return matchesRoom && matchesName && matchesPrice && matchesDate; return matchesRoom && matchesName && matchesPrice && matchesDate;
}); });
setFilteredItems(filtered); setFilteredItems(filtered);
}, [items, selectedRoom, filterName, filterPriceMin, filterPriceMax, filterDateRange]); }, [
items,
selectedRoom,
filterName,
filterPriceMin,
filterPriceMax,
filterDateRange,
]);
const handleNextPage = () => { const handleNextPage = () => {
setCurrentPage(prevPage => prevPage + 1); setCurrentPage((prevPage) => prevPage + 1);
}; };
const handlePrevPage = () => { const handlePrevPage = () => {
setCurrentPage(prevPage => prevPage - 1); setCurrentPage((prevPage) => prevPage - 1);
}; };
const handleRoomChange = (value) => { const handleRoomChange = (value) => {
setSelectedRoom(value); setSelectedRoom(value);
}; };
const handleCloseModal = () => {
setIsCreateItemModalOpen(false);
};
const chunkedItems = chunkArray(filteredItems, itemsPerRow); const chunkedItems = chunkArray(filteredItems, itemsPerRow);
const startIndex = (currentPage - 1) * (itemsPerPage / itemsPerRow); const startIndex = (currentPage - 1) * (itemsPerPage / itemsPerRow);
const endIndex = startIndex + (itemsPerPage / itemsPerRow); const endIndex = startIndex + itemsPerPage / itemsPerRow;
const rowsToDisplay = chunkedItems.slice(startIndex, endIndex); const rowsToDisplay = chunkedItems.slice(startIndex, endIndex);
return ( return (
<div className="item-container"> <div className="item-container">
<div className="filters"> <div className="filters">
<div className="filter-group"> <div className="filter-group">
<label htmlFor="roomSelect">Chambre :</label> <label htmlFor="roomSelect">Room:</label>
<Select id="roomSelect" defaultValue="all" style={{ width: 120 }} onChange={handleRoomChange}> <Select
<Option value="all">Toutes</Option> id="roomSelect"
{rooms.map(room => ( defaultValue="all"
<Option key={room._id} value={room._id}>{room.name}</Option> style={{ width: 120 }}
onChange={handleRoomChange}
>
<Option value="all">All</Option>
{rooms.map((room) => (
<Option key={room._id} value={room._id}>
{room.name}
</Option>
))} ))}
</Select> </Select>
</div> </div>
<div className="filter-group"> <div className="filter-group">
<label htmlFor="nameInput">Recherche par nom :</label> <label htmlFor="nameInput">Search by name:</label>
<Input id="nameInput" placeholder="Recherche par nom" value={filterName} onChange={e => setFilterName(e.target.value)} /> <Input
id="nameInput"
placeholder="Search by name"
value={filterName}
onChange={(e) => setFilterName(e.target.value)}
/>
</div> </div>
<div className="filter-group"> <div className="filter-group">
<label>Prix :</label> <label>Price:</label>
<InputNumber placeholder="Prix min" value={filterPriceMin} onChange={value => setFilterPriceMin(value)} /> <InputNumber
placeholder="Min price"
value={filterPriceMin}
onChange={(value) => setFilterPriceMin(value)}
/>
<span> - </span> <span> - </span>
<InputNumber placeholder="Prix max" value={filterPriceMax} onChange={value => setFilterPriceMax(value)} /> <InputNumber
placeholder="Max price"
value={filterPriceMax}
onChange={(value) => setFilterPriceMax(value)}
/>
</div> </div>
<div className="filter-group"> <div className="filter-group">
<label>Date d'achat :</label> <label>Purchase Date:</label>
<RangePicker format="YYYY-MM-DD" onChange={dates => setFilterDateRange(dates)} value={filterDateRange} /> <RangePicker
format="YYYY-MM-DD"
onChange={(dates) => setFilterDateRange(dates)}
value={filterDateRange}
/>
</div> </div>
</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}>
&times;
</span>
<FormCreateItem onClose={handleCloseModal} />
</div>
</div>
)}
<div className="item-list"> <div className="item-list">
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}> <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
{rowsToDisplay.map((row, rowIndex) => ( {rowsToDisplay.map((row, rowIndex) => (
<React.Fragment key={rowIndex}> <React.Fragment key={rowIndex}>
{row.map(item => ( {row.map((item) => (
<Col className="gutter-row" span={24 / itemsPerRow} key={item._id}> <Col
className="gutter-row"
span={24 / itemsPerRow}
key={item._id}
>
<ItemBox {...item} /> <ItemBox {...item} />
</Col> </Col>
))} ))}
@@ -152,14 +194,19 @@ export const ItemPage = () => {
))} ))}
</Row> </Row>
</div> </div>
<div className="pagination"> <div className="pagination">
<button onClick={handlePrevPage} disabled={currentPage === 1}>Précédente</button> <button onClick={handlePrevPage} disabled={currentPage === 1}>
Previous
</button>
<span>Page {currentPage}</span> <span>Page {currentPage}</span>
<button onClick={handleNextPage} disabled={endIndex >= chunkedItems.length}>Suivante</button> <button
onClick={handleNextPage}
disabled={endIndex >= chunkedItems.length}
>
Next
</button>
</div> </div>
</div> </div>
); );
}; };

View File

@@ -1,9 +1,9 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Menu } from "antd"; import { Menu } from "antd";
import { HomeOutlined } from "@ant-design/icons" import { HomeOutlined } from "@ant-design/icons";
import { Home } from "../../pages"; import { Home } from "../../pages";
import { getRooms } from "../../api/room"; import { getRooms } from "../../api/room";
import { isLoggedIn } from "../../api/authentication" import { isLoggedIn } from "../../api/authentication";
import { Items } from "../../pages/items"; import { Items } from "../../pages/items";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -15,71 +15,59 @@ function getItem(key, label) {
// label, // label,
// link, // link,
//}; //};
var item = <Menu.Item key={key}> var item = (
<Menu.Item key={key}>
<Link to="/test">{label}</Link> <Link to="/test">{label}</Link>
</Menu.Item> </Menu.Item>
return item );
return item;
} }
const { SubMenu } = Menu; const { SubMenu } = Menu;
// Component
const Navbar = () => { const Navbar = () => {
//Hook calls
const [rooms, setRooms] = useState([]); const [rooms, setRooms] = useState([]);
useEffect(() => { useEffect(() => {
console.log("NAVBAR EFFECT") console.log("NAVBAR EFFECT");
isLoggedIn().then((user) => { isLoggedIn().then((user) => {
if (user !== "Unauthorized") { if (user !== "Unauthorized") {
getRooms().then((result) => { getRooms().then((result) => {
setRooms(result); setRooms(result);
}) });
} }
}) });
}, []); }, []);
const roomItems = rooms.map((room, index) => (
console.log(index),
getItem(`sub${index}`, room.name)
));
const items = [ const items = [
<Menu.Item key="0"> <Menu.Item key="0">
<Link to="/">Menu Principal</Link> <Link to="/logout">Login / Logout</Link>
</Menu.Item>, </Menu.Item>,
<Menu.Item key="1"> <Menu.Item key="1">
<Link to="/home">Home</Link> <Link to="/home">Menu Principal</Link>
</Menu.Item>, </Menu.Item>,
<Menu.Item key="2"> <Menu.Item key="2">
<Link to="/items">Tous les articles</Link> <Link to="/items">Tous les articles</Link>
</Menu.Item>, </Menu.Item>,
<SubMenu key="3" title="Chambres"> <Menu.Item key="3">
{roomItems} <Link to="/rooms">Voir les chambres</Link>
</SubMenu> </Menu.Item>,
]; ];
// Rendu du composant Navbar
try { try {
return ( return (
<Menu <Menu
defaultSelectedKeys={["1"]} defaultSelectedKeys={["0"]}
defaultOpenKeys={["sub1"]} defaultOpenKeys={["sub1"]}
theme="dark" theme="dark"
mode="inline" mode="inline"
> >
{items} {items}
</Menu> </Menu>
); );
} catch (error) { } catch (error) {
console.log(error.stack) console.log(error.stack);
} }
}; };
export default Navbar; export default Navbar;

View 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>
);
};

View 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>
);
};

View 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' />;
}
}

View 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>
);
};

View 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>
);
};

View 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;

View 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>
);
};

View File

@@ -18,6 +18,10 @@ export const Login = () => {
} }
}; };
const onRegister = () => {
window.location.href = '/register'
}
return ( return (
<div> <div>
<h1>Login page</h1> <h1>Login page</h1>
@@ -36,6 +40,7 @@ export const Login = () => {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
<button type="submit">submit</button> <button type="submit">submit</button>
<button onClick={onRegister}>Register</button>
</form> </form>
</div> </div>
); );

View File

@@ -22,6 +22,7 @@ export const Register = () => {
return ( return (
<div> <div>
<h1>Register page</h1> <h1>Register page</h1>
<p>Password must include a capital letter, a digit and a symbol</p>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<input <input

View File

@@ -1,25 +0,0 @@
import React, { useEffect } from 'react';
import { useAuth } from "../hooks";
import { ItemPage } from "../components/item/ItemPage";
import { usePageTitle } from '../hooks/page-title-context';
export const RoomsStats = () => {
const { user } = useAuth();
const { setPageTitle } = usePageTitle();
// Mettre à jour le titre de la page dans le contexte
useEffect(() => {
setPageTitle("Toutes les rooms :");
}, [setPageTitle]);
useEffect(() => {
setPageTitle("Toutes les rooms :");
}, [setPageTitle]);
return (
<div className="gridContainer">
<div className="StatsContainer"></div>
</div>
);
};

52
src/pages/rooms.jsx Normal file
View File

@@ -0,0 +1,52 @@
import React, { useEffect, useState } from "react";
import "../assets/styles/room-page.css";
import { useAuth } from "../hooks";
import { getRoomStats } from "../api/room";
import { usePageTitle } from "../hooks/page-title-context";
import RoomStats from "../components/rooms/room-stats";
import { RoomList } from "../components/rooms/room-list";
import { RoomDetail } from "../components/rooms/room-detail";
export const Rooms = () => {
const { user } = useAuth();
const { setPageTitle } = usePageTitle();
const [stats, setStats] = useState({});
const [selectedRoom, setSelectedRoom] = useState();
useEffect(() => {
setPageTitle("Toutes les rooms :");
}, [setPageTitle]);
useEffect(() => {
const fetchData = async () => {
const roomsStatsResponse = await getRoomStats();
setStats(roomsStatsResponse);
};
fetchData();
}, []);
const handleRoomClick = (roomId) => {
console.log("Clicked room ID:", roomId);
setSelectedRoom(roomId);
};
return (
<div className="manContainer">
<div className="topContainer">
<div className="statsContainer">
<RoomStats statsParam={stats} />
</div>
<div className="listContainer">
<RoomList statsParam={stats} onRoomClick={handleRoomClick}></RoomList>
</div>
</div>
<div className="detailContainer">
<RoomDetail
selectedRoomParam={selectedRoom}
></RoomDetail>
</div>
</div>
);
};

View File

@@ -3,6 +3,7 @@ import { Route, Routes } from "react-router-dom";
import { Home, Login, Register } from "./pages"; import { Home, Login, Register } from "./pages";
import { Items } from "./pages/items"; import { Items } from "./pages/items";
import { Rooms } from "./pages/rooms";
export const Router = () => ( export const Router = () => (
<Routes> <Routes>
@@ -11,5 +12,6 @@ export const Router = () => (
<Route path="login" element={<Login />} /> <Route path="login" element={<Login />} />
<Route path="register" element={<Register />} /> <Route path="register" element={<Register />} />
<Route path="items" element={<Items />} /> <Route path="items" element={<Items />} />
<Route path="rooms" element={<Rooms />} />
</Routes> </Routes>
); );