From 38a3f105eb83878289f61071a7c4e58845e59a36 Mon Sep 17 00:00:00 2001 From: YohanKoch Date: Thu, 26 Feb 2026 13:28:32 +0100 Subject: [PATCH 01/66] feat(frontend): implement item management with list display and mock data --- frontend/src/features/home/index.jsx | 7 +- frontend/src/features/items/api/api.js | 34 +++++++++ frontend/src/features/items/items.jsx | 50 ++++++++++++ .../src/features/items/locales/en/items.json | 3 + .../src/features/items/locales/fr/items.json | 3 + frontend/src/features/items/mocks/items.json | 23 ++++++ frontend/src/features/items/ui/list.jsx | 76 +++++++++++++++++++ 7 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 frontend/src/features/items/api/api.js create mode 100644 frontend/src/features/items/items.jsx create mode 100644 frontend/src/features/items/locales/en/items.json create mode 100644 frontend/src/features/items/locales/fr/items.json create mode 100644 frontend/src/features/items/mocks/items.json create mode 100644 frontend/src/features/items/ui/list.jsx diff --git a/frontend/src/features/home/index.jsx b/frontend/src/features/home/index.jsx index a2c7756d..ad5686bf 100644 --- a/frontend/src/features/home/index.jsx +++ b/frontend/src/features/home/index.jsx @@ -1,17 +1,14 @@ import React from 'react' import { useTranslation } from 'react-i18next' import Title from '../../common/ui/title' +import Items from '../items/items' const Home = () => { const { t } = useTranslation("home", "common"); return ( <> {t("home_title")} -

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus dictum interdum felis eget varius. Integer ut imperdiet erat. Nulla vitae diam neque. Aliquam accumsan, lectus eu ornare lobortis, ligula sem convallis augue, sit amet tincidunt neque mauris vel metus. Sed ultricies nunc et ex egestas venenatis. Donec ac est eget sem convallis sollicitudin ut eu dui. Duis mattis, eros sit amet maximus interdum, nulla nunc dignissim neque, ut dignissim urna ligula at velit. Morbi magna enim, ullamcorper sit amet congue sit amet, auctor eget ligula. Nullam pellentesque tortor et nunc ornare, in imperdiet sapien tincidunt. Mauris ipsum augue, pellentesque nec imperdiet fermentum, ultricies at mauris. Aliquam sed diam non velit mattis laoreet. Curabitur euismod leo odio, ac dignissim odio consequat vel. Sed eget consequat dolor.

-

Nullam enim sapien, ullamcorper ut porttitor in, condimentum eget enim. In hendrerit ligula dui. Phasellus felis nisl, fermentum in cursus id, rhoncus sed tortor. Donec sed orci eget lectus lobortis blandit. Etiam a mollis neque, sit amet iaculis nisi. Proin auctor velit sed mi bibendum tempor. Suspendisse aliquet non lectus non ultrices.

-

Suspendisse eu ex ullamcorper, posuere ligula id, gravida erat. Duis vitae varius nulla. Nunc et fringilla ante, eu molestie ex. Ut hendrerit faucibus molestie. Fusce condimentum facilisis commodo. Nullam commodo feugiat nulla at aliquam. Vivamus tincidunt tempor bibendum.

-

Morbi mattis tristique magna, et pretium elit finibus vel. Mauris varius consectetur dui, in accumsan nisl vulputate quis. Ut imperdiet accumsan augue in posuere. Vivamus semper purus sit amet posuere porta. Integer condimentum tellus a velit accumsan malesuada. Maecenas cursus nisi sit amet viverra tincidunt. In nisi leo, lacinia blandit lacus ac, laoreet mollis ante. Aenean mattis fringilla blandit. Suspendisse quis mattis mauris, luctus ultricies leo. Praesent nec magna ante. Pellentesque vel est dapibus, porta mauris at, hendrerit mi. Sed purus dolor, interdum quis eleifend in, rhoncus id velit. Quisque mi turpis, lacinia eu vulputate quis, porta vitae urna. Morbi commodo dui at erat luctus pulvinar. Mauris convallis eget mauris a congue. Proin semper porta velit, et interdum felis consectetur nec.

-

Sed bibendum neque eget lorem lobortis eleifend nec ut lectus. Nam tincidunt lacus in volutpat placerat. Morbi at elit et nulla eleifend rutrum eu vitae sapien. Nullam mattis vulputate nibh vitae rhoncus. Vestibulum vel tempus diam, sit amet consectetur turpis. Nam eu elit enim. Maecenas scelerisque ornare nibh eu condimentum. Pellentesque dignissim nisi ligula, ac aliquet tortor facilisis sed. Vestibulum non turpis at lectus suscipit imperdiet eu et nunc. Sed vehicula mauris in dapibus malesuada. Suspendisse sed dapibus dolor.

+ ) } diff --git a/frontend/src/features/items/api/api.js b/frontend/src/features/items/api/api.js new file mode 100644 index 00000000..458bab9f --- /dev/null +++ b/frontend/src/features/items/api/api.js @@ -0,0 +1,34 @@ +import items from "../mocks/items.json"; +import api from "../../auth/ui/api/httpClient"; + +/** + * Gets all the items. + * + * @returns {Array} + * + */ +export const getItems = async () => +{ + try + { + // Uncomment below to use the real backend. + /* + const response = await api.get(`/items`); + + if (!response.ok) + { + const error = await response.text(); + console.error(`${response.status} ${response.statusText} : ${error}`); + return []; + } + + return await response.json(); + */ + return items; + } + catch(error) + { + console.error(`Error while fetching data: ${error.message}`); + return []; + } +}; \ No newline at end of file diff --git a/frontend/src/features/items/items.jsx b/frontend/src/features/items/items.jsx new file mode 100644 index 00000000..de6db1ef --- /dev/null +++ b/frontend/src/features/items/items.jsx @@ -0,0 +1,50 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Button } from '@orif-informatique/react-components-library'; +import { getItems } from './api/api'; +import List from './ui/list'; + +const Items = () => { + const { t } = useTranslation('items'); + const [items, setItems] = useState([]); + + useEffect(() => { + const fetchItems = async () => { + const data = await getItems(); + setItems(data); + }; + + fetchItems(); + }, []); + + // Example actions — adapt these to your needs + const actions = [ + { + label: t("edit", "Edit"), + onClick: (item) => console.log("Edit", item), + }, + { + label: t("delete", "Delete"), + onClick: (item) => console.log("Delete", item), + }, + ]; + + return ( +
+ +
+ ); +}; + +export default Items; diff --git a/frontend/src/features/items/locales/en/items.json b/frontend/src/features/items/locales/en/items.json new file mode 100644 index 00000000..0e0dcd23 --- /dev/null +++ b/frontend/src/features/items/locales/en/items.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/frontend/src/features/items/locales/fr/items.json b/frontend/src/features/items/locales/fr/items.json new file mode 100644 index 00000000..0e0dcd23 --- /dev/null +++ b/frontend/src/features/items/locales/fr/items.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/frontend/src/features/items/mocks/items.json b/frontend/src/features/items/mocks/items.json new file mode 100644 index 00000000..dca78f31 --- /dev/null +++ b/frontend/src/features/items/mocks/items.json @@ -0,0 +1,23 @@ +[ + { + "id": 1, + "name": "Item 1", + "description": "This is the first item.", + "date": "2024-06-01T12:00:00Z", + "deleted": false + }, + { + "id": 2, + "name": "Item 2", + "description": "This is the second item.", + "date": "2024-06-02T12:00:00Z", + "deleted": false + }, + { + "id": 3, + "name": "Item 3", + "description": "This is the third item.", + "date": "2024-06-03T12:00:00Z", + "deleted": false + } +] \ No newline at end of file diff --git a/frontend/src/features/items/ui/list.jsx b/frontend/src/features/items/ui/list.jsx new file mode 100644 index 00000000..77ddb599 --- /dev/null +++ b/frontend/src/features/items/ui/list.jsx @@ -0,0 +1,76 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@orif-informatique/react-components-library"; + +/** + * A generic list/table component that dynamically generates columns + * from the keys of the provided items. + * + * @param {Object} props + * @param {Array} props.items - The array of objects to display. + * @param {Array} [props.actions] - Optional action buttons per row. + * Each action is { label: string, onClick: (item) => void, variant?: string }. + * @param {Array} [props.columns] - Optional subset/order of columns to display. + * If omitted, all keys from the first item are used. + * @param {Object} [props.columnLabels] - Optional map of key -> display header label. + */ +const List = ({ items = [], actions = [], columns, columnLabels = {} }) => { + const { t } = useTranslation("items"); + + if (!items.length) { + return

{t("no_items", "No items to display.")}

; + } + + // Derive columns from the first item's keys if not explicitly provided + const cols = columns ?? Object.keys(items[0]); + + return ( +
+ + + + {cols.map((col) => ( + + ))} + {actions.length > 0 && ( + + )} + + + + {items.map((item, index) => ( + + {cols.map((col) => ( + + ))} + {actions.length > 0 && ( + + )} + + ))} + +
+ {columnLabels[col] ?? col} + + {t("actions", "Actions")} +
+ {String(item[col] ?? "")} + + {actions.map((action, actionIndex) => ( + + ))} +
+
+ ); +}; + +export default List; From 78a9d6409ae479fe73d04ceba06e416d05ee4224 Mon Sep 17 00:00:00 2001 From: YohanKoch Date: Fri, 27 Feb 2026 11:06:32 +0100 Subject: [PATCH 02/66] feat(frontend): enhance item management with detailed item attributes and dynamic action visibility --- frontend/src/features/items/items.jsx | 9 +++++--- frontend/src/features/items/mocks/items.json | 24 ++++++++++++-------- frontend/src/features/items/ui/list.jsx | 15 ++++++++---- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/frontend/src/features/items/items.jsx b/frontend/src/features/items/items.jsx index de6db1ef..59e53145 100644 --- a/frontend/src/features/items/items.jsx +++ b/frontend/src/features/items/items.jsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button } from '@orif-informatique/react-components-library'; import { getItems } from './api/api'; import List from './ui/list'; @@ -22,10 +21,12 @@ const Items = () => { const actions = [ { label: t("edit", "Edit"), + permission: "user:update", // Optional permission required to see this action onClick: (item) => console.log("Edit", item), }, { label: t("delete", "Delete"), + permission: "user:delete", // Optional permission required to see this action onClick: (item) => console.log("Delete", item), }, ]; @@ -34,12 +35,14 @@ const Items = () => {
diff --git a/frontend/src/features/items/mocks/items.json b/frontend/src/features/items/mocks/items.json index dca78f31..cd9e9e83 100644 --- a/frontend/src/features/items/mocks/items.json +++ b/frontend/src/features/items/mocks/items.json @@ -1,23 +1,29 @@ [ { "id": 1, - "name": "Item 1", - "description": "This is the first item.", - "date": "2024-06-01T12:00:00Z", + "name": "Leek", + "description": "A leek is a vegetable that is often used in cooking. It has a mild onion-like flavor and is commonly used in soups, stews, and other dishes. (It is also a popular item in the Vocaloid community, often associated with the character Hatsune Miku.)", + "author": "Hatsune Miku", + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", "deleted": false }, { "id": 2, - "name": "Item 2", - "description": "This is the second item.", - "date": "2024-06-02T12:00:00Z", + "name": "Baguette", + "description": "A baguette is a long, thin loaf of French bread that is known for its crisp crust and soft interior. It is a staple in French cuisine and is often enjoyed with butter, cheese, or as part of a sandwich. (It is also a popular item in the Vocaloid community, often associated with the character Kasane Teto", + "author": "Kasane Teto", + "createdAt": "2024-06-02T12:00:00Z", + "updatedAt": "2024-06-02T12:00:00Z", "deleted": false }, { "id": 3, - "name": "Item 3", - "description": "This is the third item.", - "date": "2024-06-03T12:00:00Z", + "name": "Handphone", + "description": "A handphone is a portable device that combines mobile telephone and computing functions into one unit. It is commonly used for communication, internet access, and various applications. (It is also a popular item in the Vocaloid community, often associated with the character Akita Neru.)", + "author": "Akita Neru", + "createdAt": "2024-06-03T12:00:00Z", + "updatedAt": "2024-06-03T12:00:00Z", "deleted": false } ] \ No newline at end of file diff --git a/frontend/src/features/items/ui/list.jsx b/frontend/src/features/items/ui/list.jsx index 77ddb599..98cc8d62 100644 --- a/frontend/src/features/items/ui/list.jsx +++ b/frontend/src/features/items/ui/list.jsx @@ -2,6 +2,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@orif-informatique/react-components-library"; +import useAuthStore from "../../auth/authStore"; /** * A generic list/table component that dynamically generates columns @@ -17,6 +18,7 @@ import { Button } from "@orif-informatique/react-components-library"; */ const List = ({ items = [], actions = [], columns, columnLabels = {} }) => { const { t } = useTranslation("items"); + const hasPermission = useAuthStore((state) => state.hasPermission); if (!items.length) { return

{t("no_items", "No items to display.")}

; @@ -25,6 +27,10 @@ const List = ({ items = [], actions = [], columns, columnLabels = {} }) => { // Derive columns from the first item's keys if not explicitly provided const cols = columns ?? Object.keys(items[0]); + // Only show the actions column if at least one action is visible to the user + const visibleActions = actions.filter((action) => !action.permission || hasPermission(action.permission)); + const hasVisibleActions = visibleActions.length > 0; + return (
@@ -38,7 +44,7 @@ const List = ({ items = [], actions = [], columns, columnLabels = {} }) => { {columnLabels[col] ?? col} ))} - {actions.length > 0 && ( + {hasVisibleActions && ( @@ -49,15 +55,16 @@ const List = ({ items = [], actions = [], columns, columnLabels = {} }) => { {items.map((item, index) => ( {cols.map((col) => ( - ))} - {actions.length > 0 && ( + {hasVisibleActions && ( )} @@ -70,7 +74,7 @@ const List = ({ items = [], actions = {}, columns, columnLabels = {}, showDelete colSpan={cols.length + (hasVisibleActions ? 1 : 0)} className="px-6 py-4 text-sm text-gray-500 italic text-center" > - {t("no_items", "No items to display.")} + {noItemsLabel ?? t("no_items", "No items to display.")} ) : ( From fade93d85db3bf42ec8bb2dc33af37256b295ce7 Mon Sep 17 00:00:00 2001 From: YohanKoch Date: Fri, 6 Mar 2026 13:16:21 +0100 Subject: [PATCH 07/66] feat(frontend): enhance item management with improved error handling, localization, and data fetching --- frontend/src/features/items/api/api.js | 57 ++++--------------- frontend/src/features/items/items.jsx | 24 ++++++-- .../src/features/items/locales/en/items.json | 3 +- .../src/features/items/locales/fr/items.json | 3 +- frontend/src/features/items/mocks/items.json | 2 +- frontend/src/features/items/ui/list.jsx | 8 +-- 6 files changed, 36 insertions(+), 61 deletions(-) diff --git a/frontend/src/features/items/api/api.js b/frontend/src/features/items/api/api.js index 89cdcab1..b04b7550 100644 --- a/frontend/src/features/items/api/api.js +++ b/frontend/src/features/items/api/api.js @@ -1,6 +1,9 @@ -import items from "../mocks/items.json"; +import itemsData from "../mocks/items.json"; import api from "../../auth/ui/api/apiClient"; +// Mutable copy of the mock data so mutations don't affect the original import +let items = [...itemsData]; + /** * Gets all the items. * @@ -13,16 +16,8 @@ export const getItems = async (includeDeleted = false) => { // Uncomment below to use the real backend. /* - const response = await api.get(`/items?includeDeleted=${includeDeleted}`); - - if (!response.ok) - { - const error = await response.text(); - console.error(`${response.status} ${response.statusText} : ${error}`); - return []; - } - - return await response.json(); + const response = await api.get(`/items`, { params: { includeDeleted } }); + return response.data; */ return includeDeleted ? [...items] : items.filter((item) => !item.isDeleted); } @@ -39,15 +34,7 @@ export const modifyItem = async (id, data) => // Uncomment below to use the real backend. /* const response = await api.put(`/items/${id}`, data); - - if (!response.ok) - { - const error = await response.text(); - console.error(`${response.status} ${response.statusText} : ${error}`); - return null; - } - - return await response.json(); + return response.data; */ const index = items.findIndex((item) => item.id === id); if (index !== -1) @@ -70,15 +57,7 @@ export const deleteItem = async (id) => // Uncomment below to use the real backend. /* const response = await api.delete(`/items/${id}`); - - if (!response.ok) - { - const error = await response.text(); - console.error(`${response.status} ${response.statusText} : ${error}`); - return null; - } - - return await response.json(); + return response.data; */ const index = items.findIndex((item) => item.id === id); if (index !== -1) @@ -101,15 +80,7 @@ export const restoreItem = async (id) => // Uncomment below to use the real backend. /* const response = await api.post(`/items/${id}/restore`); - - if (!response.ok) - { - const error = await response.text(); - console.error(`${response.status} ${response.statusText} : ${error}`); - return null; - } - - return await response.json(); + return response.data; */ const index = items.findIndex((item) => item.id === id); if (index !== -1) @@ -132,15 +103,7 @@ export const hardDeleteItem = async (id) => // Uncomment below to use the real backend. /* const response = await api.delete(`/items/${id}/hard`); - - if (!response.ok) - { - const error = await response.text(); - console.error(`${response.status} ${response.statusText} : ${error}`); - return null; - } - - return await response.json(); + return response.data; */ const index = items.findIndex((item) => item.id === id); if (index !== -1) diff --git a/frontend/src/features/items/items.jsx b/frontend/src/features/items/items.jsx index 2ebacd17..aab0570a 100644 --- a/frontend/src/features/items/items.jsx +++ b/frontend/src/features/items/items.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useMemo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getItems, modifyItem, deleteItem, restoreItem, hardDeleteItem } from './api/api'; @@ -7,28 +7,40 @@ import List from './ui/list'; const Items = () => { const { t } = useTranslation('items'); const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const [showDeleted, setShowDeleted] = useState(false); const fetchItems = useCallback(async () => { - const data = await getItems(showDeleted); - setItems(data); - }, [showDeleted]); + setIsLoading(true); + setError(null); + try { + const data = await getItems(showDeleted); + setItems(data); + } catch (err) { + setError(err.message || t("fetch_error", "Failed to load items.")); + } finally { + setIsLoading(false); + } + }, [showDeleted, t]); useEffect(() => { fetchItems(); }, [fetchItems]); - const actions = { + const actions = useMemo(() => ({ // TODO: Replace hardcoded edit with a proper edit form/modal edit: { permission: "user:update", onClick: (item) => modifyItem(item.id, { name: item.name + " (edited)" }).then(() => fetchItems()).catch((err) => console.error("Edit failed:", err)) }, delete: { permission: "user:delete", onClick: (item) => deleteItem(item.id).then(() => fetchItems()).catch((err) => console.error("Delete failed:", err)) }, restore: { permission: "user:write", onClick: (item) => restoreItem(item.id).then(() => fetchItems()).catch((err) => console.error("Restore failed:", err)) }, hardDelete: { permission: "user:delete", onClick: (item) => hardDeleteItem(item.id).then(() => fetchItems()).catch((err) => console.error("Hard delete failed:", err)) }, viewDeleted: { permission: "user:read" }, - }; + }), [fetchItems]); return (
+ {isLoading &&

{t("loading", "Loading...")}

} + {error &&

{error}

} { - const { t } = useTranslation("items"); const hasPermission = useAuthStore((state) => state.hasPermission); // Derive columns from the first item's keys if not explicitly provided @@ -41,7 +39,7 @@ const List = ({ items = [], actions = {}, columns, columnLabels = {}, showDelete
{onToggleShowDeleted && canViewDeleted && (
)} From f16a00093eed95579dc046321ae13dd5f2fd3c22 Mon Sep 17 00:00:00 2001 From: YohanKoch Date: Fri, 6 Mar 2026 14:12:25 +0100 Subject: [PATCH 09/66] feat(frontend): add visual indication for soft-deleted items in list component --- frontend/src/features/items/ui/list.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/items/ui/list.jsx b/frontend/src/features/items/ui/list.jsx index 13006402..f10cb322 100644 --- a/frontend/src/features/items/ui/list.jsx +++ b/frontend/src/features/items/ui/list.jsx @@ -79,7 +79,7 @@ const List = ({ items = [], actions = {}, columns, columnLabels = {}, showDelete items.map((item, index) => ( {cols.map((col) => ( - ))} From 72008ebbb1c2216e82ff1a5229d89a4c39e90b5c Mon Sep 17 00:00:00 2001 From: YohanKoch Date: Fri, 6 Mar 2026 15:18:45 +0100 Subject: [PATCH 10/66] feat(frontend): add confirmation dialog for permanent deletion of items --- frontend/src/features/items/items.jsx | 2 + .../src/features/items/locales/en/items.json | 4 +- .../src/features/items/locales/fr/items.json | 4 +- frontend/src/features/items/ui/list.jsx | 216 +++++++++++++----- 4 files changed, 165 insertions(+), 61 deletions(-) diff --git a/frontend/src/features/items/items.jsx b/frontend/src/features/items/items.jsx index aab0570a..605648e4 100644 --- a/frontend/src/features/items/items.jsx +++ b/frontend/src/features/items/items.jsx @@ -58,6 +58,8 @@ const Items = () => { noItemsLabel={t("no_items", "No items to display.")} showDeleted={showDeleted} onToggleShowDeleted={setShowDeleted} + confirmHardDeleteLabel={t("confirm_hard_delete", "Confirm Permanent Deletion")} + confirmHardDeleteLabelText={t("confirm_hard_delete_text", "Are you sure you want to permanently delete this item? This action cannot be undone.")} /> ); diff --git a/frontend/src/features/items/locales/en/items.json b/frontend/src/features/items/locales/en/items.json index e08108cb..edf79ad4 100644 --- a/frontend/src/features/items/locales/en/items.json +++ b/frontend/src/features/items/locales/en/items.json @@ -7,5 +7,7 @@ "description": "Description", "createdAt": "Created At", "updatedAt": "Updated At", - "author": "Author" + "author": "Author", + "confirm_hard_delete": "Confirm Permanent Deletion", + "confirm_hard_delete_text": "Are you sure you want to permanently delete this item? This action cannot be undone." } \ No newline at end of file diff --git a/frontend/src/features/items/locales/fr/items.json b/frontend/src/features/items/locales/fr/items.json index 0874bb1f..ba62a458 100644 --- a/frontend/src/features/items/locales/fr/items.json +++ b/frontend/src/features/items/locales/fr/items.json @@ -7,5 +7,7 @@ "description": "Description", "createdAt": "Créé le", "updatedAt": "Mis à jour le", - "author": "Auteur" + "author": "Auteur", + "confirm_hard_delete": "Confirmer la suppression permanente", + "confirm_hard_delete_text": "Êtes-vous sûr de vouloir supprimer définitivement cet élément ? Cette action est irréversible." } \ No newline at end of file diff --git a/frontend/src/features/items/ui/list.jsx b/frontend/src/features/items/ui/list.jsx index f10cb322..9c78722d 100644 --- a/frontend/src/features/items/ui/list.jsx +++ b/frontend/src/features/items/ui/list.jsx @@ -1,7 +1,7 @@ -import React from "react"; +import React from 'react'; -import { Button } from "@orif-informatique/react-components-library"; -import useAuthStore from "../../auth/authStore"; +import { Button, PopUp } from '@orif-informatique/react-components-library'; +import useAuthStore from '../../auth/authStore'; /** * A generic list/table component that dynamically generates columns @@ -21,25 +21,53 @@ import useAuthStore from "../../auth/authStore"; * @param {string} [props.showDeletedLabel] - Optional label for the show deleted checkbox. * @param {string} [props.noItemsLabel] - Optional label for when there are no items. */ -const List = ({ items = [], actions = {}, columns, columnLabels = {}, showDeleted = false, onToggleShowDeleted, actionsLabel, showDeletedLabel, noItemsLabel }) => { +const List = ({ + items = [], + actions = {}, + columns, + columnLabels = {}, + showDeleted = false, + onToggleShowDeleted, + actionsLabel, + showDeletedLabel, + noItemsLabel, + confirmHardDeleteLabel, + confirmHardDeleteLabelText, +}) => { + const [hardDeleteTarget, setHardDeleteTarget] = React.useState(null); const hasPermission = useAuthStore((state) => state.hasPermission); // Derive columns from the first item's keys if not explicitly provided const cols = columns ?? (items.length ? Object.keys(items[0]) : []); // Check permissions for each action - const canEdit = actions.edit && (!actions.edit.permission || hasPermission(actions.edit.permission)); - const canDelete = actions.delete && (!actions.delete.permission || hasPermission(actions.delete.permission)); - const canRestore = actions.restore && (!actions.restore.permission || hasPermission(actions.restore.permission)); - const canHardDelete = actions.hardDelete && (!actions.hardDelete.permission || hasPermission(actions.hardDelete.permission)); - const canViewDeleted = actions.viewDeleted && (!actions.viewDeleted.permission || hasPermission(actions.viewDeleted.permission)); - const hasVisibleActions = canEdit || canDelete || canRestore || canHardDelete; + const canEdit = + actions.edit && + (!actions.edit.permission || hasPermission(actions.edit.permission)); + const canDelete = + actions.delete && + (!actions.delete.permission || + hasPermission(actions.delete.permission)); + const canRestore = + actions.restore && + (!actions.restore.permission || + hasPermission(actions.restore.permission)); + const canHardDelete = + actions.hardDelete && + (!actions.hardDelete.permission || + hasPermission(actions.hardDelete.permission)); + const canViewDeleted = + actions.viewDeleted && + (!actions.viewDeleted.permission || + hasPermission(actions.viewDeleted.permission)); + const hasVisibleActions = + canEdit || canDelete || canRestore || canHardDelete; return (
{onToggleShowDeleted && canViewDeleted && (
- {items.length === 0 ? ( + {items.length === 0 ? - ) : ( - items.map((item, index) => ( - - {cols.map((col) => ( - - ))} - {hasVisibleActions && ( - - )} - - )))} + : items.map((item, index) => ( + + {cols.map((col) => ( + + ))} + {hasVisibleActions && ( + + )} + + )) + }
{t("actions", "Actions")}
+ {String(item[col] ?? "")} - {actions.map((action, actionIndex) => ( + {visibleActions.map((action, actionIndex) => ( - {t("actions", "Actions")} + + {actionsLabel ?? t("actions", "Actions")}
- {actionsLabel ?? "Actions"} + {actionsLabel ?? ""}
+ {String(item[col] ?? "")}
- {noItemsLabel ?? "No items to display."} + {noItemsLabel ?? 'No items to display.'}
- {String(item[col] ?? "")} - -
- {canEdit && !item.isDeleted && ( -
-
+ {String(item[col] ?? '')} + +
+ {canEdit && !item.isDeleted && ( +
+ + )} + + )} + +
From 857002159b2140c3103aac6a668a65582c95075d Mon Sep 17 00:00:00 2001 From: YohanKoch Date: Thu, 19 Mar 2026 09:19:15 +0100 Subject: [PATCH 11/66] feat(frontend): implement item creation and editing form with API integration --- frontend/src/features/items/api/api.js | 28 ++++++++++++++++++++++ frontend/src/features/items/itemDetail.jsx | 0 frontend/src/features/items/itemForm.jsx | 25 +++++++++++++++++++ frontend/src/features/items/items.jsx | 15 +++++++++++- 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 frontend/src/features/items/itemDetail.jsx create mode 100644 frontend/src/features/items/itemForm.jsx diff --git a/frontend/src/features/items/api/api.js b/frontend/src/features/items/api/api.js index b04b7550..2c6297b3 100644 --- a/frontend/src/features/items/api/api.js +++ b/frontend/src/features/items/api/api.js @@ -118,4 +118,32 @@ export const hardDeleteItem = async (id) => console.error(`Error while hard deleting item: ${error.message}`); return null; } +}; + +export const createItem = async (data) => +{ + try { + // Uncomment below to use the real backend. + /* + const response = await api.post(`/items`, data); + return response.data; + */ + const newItem = { + id: items.length ? Math.max(...items.map((item) => item.id)) + 1 : 1, + name: data.name, + description: data.description, + author: data.author || "Unknown", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + isDeleted: false, + + }; + items.push(newItem); + return newItem; + } + catch(error) + { + console.error(`Error while creating item: ${error.message}`); + return null; + } }; \ No newline at end of file diff --git a/frontend/src/features/items/itemDetail.jsx b/frontend/src/features/items/itemDetail.jsx new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/features/items/itemForm.jsx b/frontend/src/features/items/itemForm.jsx new file mode 100644 index 00000000..16818ea4 --- /dev/null +++ b/frontend/src/features/items/itemForm.jsx @@ -0,0 +1,25 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, InputText, Textarea, } from '@orif-informatique/react-components-library'; + +import { createItem, modifyItem } from "./api/api"; + +const ItemForm = ({ item, onClose }) => { + const { t } = useTranslation('items'); + + const [name, setName] = useState(item ? item.name : ""); + const [description, setDescription] = useState(item ? item.description : ""); + + return ( + <> + setName(e.target.value)} /> +