From 468c87b55073ae9967f0570bd34747d10121aa0d Mon Sep 17 00:00:00 2001 From: tsindt Date: Wed, 19 Aug 2026 17:34:36 +0200 Subject: [PATCH] =?UTF-8?q?Produktmanagement=20hinzugef=C3=BCgt=20und=20kl?= =?UTF-8?q?einere=20Bug=20behoben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.tmp/tsconfig.app.tsbuildinfo | 1 - .../.tmp/tsconfig.node.tsbuildinfo | 1 - .../material-editor/src/api/product.api.ts | 56 +++++ .../src/api/productGroup.api.ts | 73 ++++++ .../components/products/ProductFormDialog.tsx | 74 ++++++ .../src/components/products/ProductGrid.tsx | 230 ++++++++++++++++++ .../components/products/ProductGroupTree.tsx | 187 ++++++++++++++ .../src/components/products/Products.tsx | 38 +++ .../material-editor/src/data/product.ts | 7 + .../material-editor/src/data/productGroup.ts | 6 + .../material-editor/src/data/property.ts | 8 + .../material-editor/src/pages/HomePage.tsx | 3 +- .../server/repositories/productRepository.go | 2 +- buildtime/server/server/httpServer.go | 1 + 14 files changed, 683 insertions(+), 4 deletions(-) delete mode 100644 buildtime/client/material-editor/node_modules/.tmp/tsconfig.app.tsbuildinfo delete mode 100644 buildtime/client/material-editor/node_modules/.tmp/tsconfig.node.tsbuildinfo create mode 100644 buildtime/client/material-editor/src/api/product.api.ts create mode 100644 buildtime/client/material-editor/src/api/productGroup.api.ts create mode 100644 buildtime/client/material-editor/src/components/products/ProductFormDialog.tsx create mode 100644 buildtime/client/material-editor/src/components/products/ProductGrid.tsx create mode 100644 buildtime/client/material-editor/src/components/products/ProductGroupTree.tsx create mode 100644 buildtime/client/material-editor/src/components/products/Products.tsx create mode 100644 buildtime/client/material-editor/src/data/product.ts create mode 100644 buildtime/client/material-editor/src/data/productGroup.ts create mode 100644 buildtime/client/material-editor/src/data/property.ts diff --git a/buildtime/client/material-editor/node_modules/.tmp/tsconfig.app.tsbuildinfo b/buildtime/client/material-editor/node_modules/.tmp/tsconfig.app.tsbuildinfo deleted file mode 100644 index 5d6f3affc..000000000 --- a/buildtime/client/material-editor/node_modules/.tmp/tsconfig.app.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["../../src/app.tsx","../../src/layout.tsx","../../src/keycloak.ts","../../src/main.tsx","../../src/api/propertytype.api.ts","../../src/api/propertytypegroup.api.ts","../../src/api/value.api.ts","../../src/components/footer.tsx","../../src/components/navbar.tsx","../../src/components/properties/entityformdialog.tsx","../../src/components/properties/properties.tsx","../../src/components/properties/propertytypegrid.tsx","../../src/components/properties/propertytypegrouptree.tsx","../../src/components/properties/valuegrid.tsx","../../src/data/propertytype.ts","../../src/data/propertytypegrouptable.ts","../../src/data/value.ts","../../src/pages/homepage.tsx","../../src/pages/notfoundpage.tsx"],"errors":true,"version":"6.0.3"} \ No newline at end of file diff --git a/buildtime/client/material-editor/node_modules/.tmp/tsconfig.node.tsbuildinfo b/buildtime/client/material-editor/node_modules/.tmp/tsconfig.node.tsbuildinfo deleted file mode 100644 index 6d2646611..000000000 --- a/buildtime/client/material-editor/node_modules/.tmp/tsconfig.node.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["../../vite.config.ts"],"version":"6.0.3"} \ No newline at end of file diff --git a/buildtime/client/material-editor/src/api/product.api.ts b/buildtime/client/material-editor/src/api/product.api.ts new file mode 100644 index 000000000..833eae068 --- /dev/null +++ b/buildtime/client/material-editor/src/api/product.api.ts @@ -0,0 +1,56 @@ +import ky from 'ky' +import type { Product } from '../data/product' +import type { Property } from '../data/property'; + +const API_BASE_URL = 'http://localhost:8088' + +export async function createProduct(product: Product): Promise { + const response = await ky + .post(`${API_BASE_URL}/createProduct`, { json: product }) + .json<{ status: string; data: Product }>() + + return response.data +} + +export async function updateProduct(product: Product): Promise { + await ky.put(`${API_BASE_URL}/updateProduct`, { json: product }) +} + +export async function deleteProduct(id: number): Promise { + await ky.delete(`${API_BASE_URL}/deleteProduct/${id}`) +} + +export async function getProduct(id: number): Promise { + const response = await ky + .get(`${API_BASE_URL}/getProduct/${id}`) + .json<{ status: string; product: Product }>() + + return response.product +} + +export async function getProductsByGroupId(groupId: number): Promise { + const response = await ky + .get(`${API_BASE_URL}/getProductsByGroupId/${groupId}`) + .json<{ status: string; groupId: number; products: Product[] }>() + + // the backend returns null instead of [] when there are no matching products + return response.products ?? [] +} + +// note: the registered route only carries :productId, propertyId is sent in the body +export async function addPropertyToProductId(productId: number, propertyId: number): Promise { + await ky.post(`${API_BASE_URL}/addPropertyToProductId/${productId}`, { json: { propertyId } }) +} + +// note: the registered route only carries :productId, propertyId is sent in the body +export async function deletePropertyFromProductId(productId: number, propertyId: number): Promise { + await ky.delete(`${API_BASE_URL}/deletePropertyFromProductId/${productId}`, { json: { propertyId } }) +} + +export async function getPropertiesFromProductId(productId: number): Promise { + const response = await ky + .get(`${API_BASE_URL}/getPropertiesFromProductId/${productId}`) + .json<{ status: string; productId: number; properties: Property[] }>() + return response.properties ?? [] +} + diff --git a/buildtime/client/material-editor/src/api/productGroup.api.ts b/buildtime/client/material-editor/src/api/productGroup.api.ts new file mode 100644 index 000000000..f6041080b --- /dev/null +++ b/buildtime/client/material-editor/src/api/productGroup.api.ts @@ -0,0 +1,73 @@ +import ky from 'ky' +import type { ProductGroup } from '../data/productGroup' +import type { Property } from '../data/property' + +type ApiResponse = { + message: string; + data: T; +} + +const API_BASE_URL = 'http://localhost:8088' + +export async function createProductGroup(productGroup: ProductGroup): Promise { + const response = await ky + .post(`${API_BASE_URL}/createProductGroup`, { json: productGroup }) + .json>() + + return response.data +} + +export async function updateProductGroup(productGroup: ProductGroup): Promise { + const response = await ky + .put(`${API_BASE_URL}/updateProductGroup`, { json: productGroup }) + .json>() + + return response.data +} + +export async function deleteProductGroup(id: number): Promise { + await ky.delete(`${API_BASE_URL}/deleteProductGroup/${id}`) +} + +export async function getProductGroup(id: number): Promise { + const response = await ky + .get(`${API_BASE_URL}/getProductGroup/${id}`) + .json>() + + return response.data +} + +export async function getProductGroupsByParentId(parentId: number): Promise { + const response = await ky + .get(`${API_BASE_URL}/getProductGroupsByParentId/${parentId}`) + .json>() + + // the backend returns null instead of [] when there are no matching product groups + return response.data ?? [] +} + +export async function deleteProductFromProductGroupId(productId: number): Promise { + await ky.delete(`${API_BASE_URL}/deleteProductFromProductGroupId/${productId}`) +} + +export async function getPropertiesFromProductGroupId(productGroupId: number): Promise { + const response = await ky + .post(`${API_BASE_URL}/getPropertiesFromProductGroupId/${productGroupId}`) + .json>() + + // the backend returns null instead of [] when there are no matching properties + return response.data ?? [] +} + +export async function deletePropertyFromProductGroupId(propertyId: number, productGroupId: number): Promise { + await ky.delete(`${API_BASE_URL}/deletePropertyFromProductGroupId/${propertyId}/${productGroupId}`) +} + +export async function createPropertyToProductGroupId(propertyTypeId: number, productGroupId: number): Promise { + const response = await ky + .get(`${API_BASE_URL}/createPropertyToProductGroupId/${propertyTypeId}/${productGroupId}`) + .json>() + + return response.data +} + diff --git a/buildtime/client/material-editor/src/components/products/ProductFormDialog.tsx b/buildtime/client/material-editor/src/components/products/ProductFormDialog.tsx new file mode 100644 index 000000000..3d994bbd1 --- /dev/null +++ b/buildtime/client/material-editor/src/components/products/ProductFormDialog.tsx @@ -0,0 +1,74 @@ +import { useEffect, useState } from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Stack from '@mui/material/Stack'; + +export type ProductFormValues = { + label: string; + externalId: string; + description: string; +}; + +type ProductFormDialogProps = { + open: boolean; + onClose: () => void; + onSubmit: (values: ProductFormValues) => Promise | void; +}; + +const ProductFormDialog = ({ open, onClose, onSubmit }: ProductFormDialogProps) => { + const [label, setLabel] = useState(''); + const [externalId, setExternalId] = useState(''); + const [description, setDescription] = useState(''); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (open) { + setLabel(''); + setExternalId(''); + setDescription(''); + } + }, [open]); + + const handleSubmit = async () => { + if (!label.trim()) return; + setSubmitting(true); + try { + await onSubmit({ label: label.trim(), externalId: externalId.trim(), description: description.trim() }); + onClose(); + } finally { + setSubmitting(false); + } + }; + + return ( + + Add Product + + + setLabel(e.target.value)} autoFocus required fullWidth /> + setExternalId(e.target.value)} fullWidth /> + setDescription(e.target.value)} + fullWidth + multiline + minRows={2} + /> + + + + + + + + ); +}; + +export default ProductFormDialog; diff --git a/buildtime/client/material-editor/src/components/products/ProductGrid.tsx b/buildtime/client/material-editor/src/components/products/ProductGrid.tsx new file mode 100644 index 000000000..f72f83437 --- /dev/null +++ b/buildtime/client/material-editor/src/components/products/ProductGrid.tsx @@ -0,0 +1,230 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import InputAdornment from '@mui/material/InputAdornment'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import SearchIcon from '@mui/icons-material/Search'; +import { DataGrid } from '@mui/x-data-grid'; +import type { GridColDef, GridRowSelectionModel } from '@mui/x-data-grid'; + +import { createProduct, deleteProduct, getProductsByGroupId } from '../../api/product.api'; +import { getPropertiesFromProductGroupId } from '../../api/productGroup.api'; +import { getPropertyType } from '../../api/propertyType.api'; +import { getValuesFromPropertyId } from '../../api/value.api'; +import ProductFormDialog, { type ProductFormValues } from './ProductFormDialog'; + +const EMPTY_SELECTION: GridRowSelectionModel = { type: 'include', ids: new Set() }; + +function toSelectionModel(id: number | null): GridRowSelectionModel { + return id == null ? EMPTY_SELECTION : { type: 'include', ids: new Set([id]) }; +} + +function fromSelectionModel(model: GridRowSelectionModel): number | null { + const [id] = Array.from(model.ids); + return id == null ? null : Number(id); +} + +const propertyTypeField = (propertyTypeId: number) => `propertyType_${propertyTypeId}`; + +type PropertyTypeColumn = { + propertyTypeId: number; + label: string; +}; + +type ProductRow = { + id: number; + externalId: string; + label: string; + description: string; + [propertyTypeField: string]: string | number; +}; + +async function loadProductRows(groupId: number): Promise<{ rows: ProductRow[]; propertyTypeColumns: PropertyTypeColumn[] }> { + const [allProperties, products] = await Promise.all([ + getPropertiesFromProductGroupId(groupId), // Assuming this function fetches properties for all products in the group + getProductsByGroupId(groupId), + ]); + + // properties without a productId are the group-level templates that define the assigned property types + const templatePropertyTypeIds = Array.from( + new Set(allProperties.filter((property) => !property.productId).map((property) => property.propertyTypeId)), + ); + + const propertyTypeColumns = await Promise.all( + templatePropertyTypeIds.map(async (propertyTypeId) => ({ + propertyTypeId, + label: (await getPropertyType(propertyTypeId)).label, + })), + ); + + const rows = await Promise.all( + products.map(async (product) => { + const row: ProductRow = { + id: product.id, + externalId: product.externalId, + label: product.label, + description: product.description, + }; + + const ownProperties = allProperties.filter((property) => property.productId === product.id); + await Promise.all( + ownProperties.map(async (property) => { + const values = await getValuesFromPropertyId(property.id); + row[propertyTypeField(property.propertyTypeId)] = values.map((value) => value.label).join(','); + }), + ); + + return row; + }), + ); + + return { rows, propertyTypeColumns }; +} + +type ProductGridProps = { + groupId: number | null; + groupLabel: string | null; +}; + +const ProductGrid = ({ groupId, groupLabel }: ProductGridProps) => { + const [rows, setRows] = useState([]); + const [propertyTypeColumns, setPropertyTypeColumns] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [search, setSearch] = useState(''); + const [selectedProductId, setSelectedProductId] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + + const reload = useCallback(async () => { + if (groupId == null) { + setRows([]); + setPropertyTypeColumns([]); + return; + } + setLoading(true); + setError(null); + try { + const { rows: nextRows, propertyTypeColumns: nextColumns } = await loadProductRows(groupId); + setRows(nextRows); + setPropertyTypeColumns(nextColumns); + } catch { + setError('Failed to load products.'); + } finally { + setLoading(false); + } + }, [groupId]); + + useEffect(() => { + setSelectedProductId(null); + reload(); + }, [groupId, reload]); + + const columns = useMemo[]>( + () => [ + { field: 'id', headerName: 'ID', width: 80 }, + { field: 'externalId', headerName: 'External ID', width: 140 }, + { field: 'label', headerName: 'Label', flex: 1 }, + { field: 'description', headerName: 'Description', flex: 1 }, + ...propertyTypeColumns.map>((column) => ({ + field: propertyTypeField(column.propertyTypeId), + headerName: column.label, + flex: 1, + })), + ], + [propertyTypeColumns], + ); + + const filteredRows = useMemo(() => { + const normalizedQuery = search.trim().toLowerCase(); + if (!normalizedQuery) return rows; + return rows.filter( + (row) => + row.label.toLowerCase().includes(normalizedQuery) || + row.externalId.toLowerCase().includes(normalizedQuery) || + row.description.toLowerCase().includes(normalizedQuery), + ); + }, [rows, search]); + + const handleAddProduct = async ({ label, externalId, description }: ProductFormValues) => { + if (groupId == null) return; + await createProduct({ id: 0, label, externalId, description, productGroupId: groupId }); + await reload(); + }; + + const handleRemoveProduct = async () => { + if (selectedProductId == null) return; + if (!window.confirm('Remove selected product?')) return; + await deleteProduct(selectedProductId); + setSelectedProductId(null); + await reload(); + }; + + return ( + + Products{groupLabel ? ` - ${groupLabel}` : ''} + + setSearch(e.target.value)} + fullWidth + disabled={groupId == null} + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, + }} + /> + + + setDialogOpen(true)} disabled={groupId == null}> + + + + + + + + + + + + + {error && ( + + {error} + + )} + {groupId == null ? ( + + Select a product group to view its products. + + ) : ( + + setSelectedProductId(fromSelectionModel(model))} + disableMultipleRowSelection + /> + + )} + setDialogOpen(false)} onSubmit={handleAddProduct} /> + + ); +}; + +export default ProductGrid; diff --git a/buildtime/client/material-editor/src/components/products/ProductGroupTree.tsx b/buildtime/client/material-editor/src/components/products/ProductGroupTree.tsx new file mode 100644 index 000000000..fba1e247a --- /dev/null +++ b/buildtime/client/material-editor/src/components/products/ProductGroupTree.tsx @@ -0,0 +1,187 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { SyntheticEvent } from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import InputAdornment from '@mui/material/InputAdornment'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import SearchIcon from '@mui/icons-material/Search'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; + +import { createProductGroup, deleteProductGroup, getProductGroupsByParentId } from '../../api/productGroup.api'; +import EntityFormDialog, { type EntityFormValues } from '../properties/EntityFormDialog'; + +const ROOT_PARENT_GROUP_ID = 0; + +export type GroupTreeItem = { + id: string; + label: string; + children?: GroupTreeItem[]; +}; + +async function buildGroupTree(parentId: number): Promise { + const groups = await getProductGroupsByParentId(parentId); + return Promise.all( + groups.map(async (group) => { + const children = await buildGroupTree(group.id); + return { + id: String(group.id), + label: group.label, + children: children.length > 0 ? children : undefined, + }; + }), + ); +} + +function filterGroupTree(items: GroupTreeItem[], query: string): GroupTreeItem[] { + const normalizedQuery = query.trim().toLowerCase(); + if (!normalizedQuery) return items; + + const filterNodes = (nodes: GroupTreeItem[]): GroupTreeItem[] => + nodes.reduce((matches, node) => { + const filteredChildren = node.children ? filterNodes(node.children) : []; + const selfMatches = node.label.toLowerCase().includes(normalizedQuery); + if (selfMatches || filteredChildren.length > 0) { + matches.push({ ...node, children: selfMatches ? node.children : filteredChildren }); + } + return matches; + }, []); + + return filterNodes(items); +} + +function collectIds(items: GroupTreeItem[]): string[] { + return items.flatMap((item) => [item.id, ...(item.children ? collectIds(item.children) : [])]); +} + +function findLabel(items: GroupTreeItem[], id: string): string | null { + for (const item of items) { + if (item.id === id) return item.label; + const childLabel = item.children ? findLabel(item.children, id) : null; + if (childLabel != null) return childLabel; + } + return null; +} + +type ProductGroupTreeProps = { + selectedGroupId: number | null; + onSelectedGroupChange: (groupId: number | null, groupLabel: string | null) => void; +}; + +const ProductGroupTree = ({ selectedGroupId, onSelectedGroupChange }: ProductGroupTreeProps) => { + const [tree, setTree] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [search, setSearch] = useState(''); + const [expandedItems, setExpandedItems] = useState([]); + const [dialogOpen, setDialogOpen] = useState(false); + + const reloadTree = useCallback(async () => { + setLoading(true); + setError(null); + try { + setTree(await buildGroupTree(ROOT_PARENT_GROUP_ID)); + } catch { + setError('Failed to load product groups.'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + reloadTree(); + }, [reloadTree]); + + const filteredTree = useMemo(() => filterGroupTree(tree, search), [tree, search]); + + useEffect(() => { + if (search.trim()) { + setExpandedItems(collectIds(filteredTree)); + } + }, [search, filteredTree]); + + const handleSelectedItemsChange = (_event: SyntheticEvent | null, itemId: string | null) => { + onSelectedGroupChange(itemId ? Number(itemId) : null, itemId ? findLabel(tree, itemId) : null); + }; + + const handleAddGroup = async ({ label, description }: EntityFormValues) => { + await createProductGroup({ + id: 0, + label, + description, + parentGroupId: selectedGroupId ?? ROOT_PARENT_GROUP_ID, + }); + await reloadTree(); + }; + + const handleRemoveGroup = async () => { + if (selectedGroupId == null) return; + if (!window.confirm('Remove selected product group?')) return; + await deleteProductGroup(selectedGroupId); + onSelectedGroupChange(null, null); + await reloadTree(); + }; + + return ( + + Product Groups + + setSearch(e.target.value)} + fullWidth + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, + }} + /> + + setDialogOpen(true)}> + + + + + + + + + + + + {error && ( + + {error} + + )} + + setExpandedItems(itemIds)} + /> + + {loading && Loading...} + setDialogOpen(false)} + onSubmit={handleAddGroup} + /> + + ); +}; + +export default ProductGroupTree; diff --git a/buildtime/client/material-editor/src/components/products/Products.tsx b/buildtime/client/material-editor/src/components/products/Products.tsx new file mode 100644 index 000000000..03277f01b --- /dev/null +++ b/buildtime/client/material-editor/src/components/products/Products.tsx @@ -0,0 +1,38 @@ +import React, { useCallback, useState } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + +import ProductGroupTree from './ProductGroupTree'; +import ProductGrid from './ProductGrid'; + +const Products: React.FC = () => { + const [selectedGroupId, setSelectedGroupId] = useState(null); + const [selectedGroupLabel, setSelectedGroupLabel] = useState(null); + + const handleSelectedGroupChange = useCallback((groupId: number | null, groupLabel: string | null) => { + setSelectedGroupId(groupId); + setSelectedGroupLabel(groupLabel); + }, []); + + return ( + + + Product Management + + + + + + + ); +}; + +export default Products; diff --git a/buildtime/client/material-editor/src/data/product.ts b/buildtime/client/material-editor/src/data/product.ts new file mode 100644 index 000000000..35dbaa6ea --- /dev/null +++ b/buildtime/client/material-editor/src/data/product.ts @@ -0,0 +1,7 @@ +export type Product = { + id: number; + label: string; + externalId: string; + description: string; + productGroupId: number; +} \ No newline at end of file diff --git a/buildtime/client/material-editor/src/data/productGroup.ts b/buildtime/client/material-editor/src/data/productGroup.ts new file mode 100644 index 000000000..8fbf3e344 --- /dev/null +++ b/buildtime/client/material-editor/src/data/productGroup.ts @@ -0,0 +1,6 @@ +export type ProductGroup = { + id: number; + label: string; + description: string; + parentGroupId: number; +}; diff --git a/buildtime/client/material-editor/src/data/property.ts b/buildtime/client/material-editor/src/data/property.ts new file mode 100644 index 000000000..b10f7aa14 --- /dev/null +++ b/buildtime/client/material-editor/src/data/property.ts @@ -0,0 +1,8 @@ +export type Property = { + id: number; + label: string; + description: string; + propertyTypeId: number; + productId: number; + productGroupId: number; +}; diff --git a/buildtime/client/material-editor/src/pages/HomePage.tsx b/buildtime/client/material-editor/src/pages/HomePage.tsx index c7f171f11..22fbc1c08 100644 --- a/buildtime/client/material-editor/src/pages/HomePage.tsx +++ b/buildtime/client/material-editor/src/pages/HomePage.tsx @@ -7,6 +7,7 @@ import TabList from '@mui/lab/TabList'; import TabPanel from '@mui/lab/TabPanel'; import Properties from '../components/properties/Properties'; +import Products from '../components/products/Products'; const HomePage: React.FC = () => { const [value, setValue] = React.useState('1'); @@ -26,7 +27,7 @@ const HomePage: React.FC = () => { - Item Two + Item Three Item Three Item Three diff --git a/buildtime/server/repositories/productRepository.go b/buildtime/server/repositories/productRepository.go index c6704efa2..4a9335c98 100644 --- a/buildtime/server/repositories/productRepository.go +++ b/buildtime/server/repositories/productRepository.go @@ -34,7 +34,7 @@ func (pr *ProductRepository) CreateProduct(product *models.Product) (*models.Pro } for _, property := range properties { - _, err := pr.valueRepository.createPropertyFromProperty(0, product.ID, property) + _, err := pr.valueRepository.createPropertyFromProperty(product.ProductGroupID, product.ID, property) if err != nil { return nil, err } diff --git a/buildtime/server/server/httpServer.go b/buildtime/server/server/httpServer.go index 6e13e3caf..bdfcbb5da 100644 --- a/buildtime/server/server/httpServer.go +++ b/buildtime/server/server/httpServer.go @@ -85,6 +85,7 @@ func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { router.DELETE("/deleteProduct/:id", productController.DeleteProduct) // delete Product router.GET("/getProduct/:id", productController.GetProduct) // get Product by id router.GET("/getProductsByGroupId/:groupId", productController.GetProductsByGroupId) // get all Products by group ID + router.GET("/getPropertiesFromProductId/:productId", productController.GetPropertiesFromProductId) // get Properties from ProductGroup by ID router.POST("/addPropertyToProductId/:productId", productController.AddPropertyToProductId) // add Property to Product by ID router.DELETE("/deletePropertyFromProductId/:productId", productController.DeletePropertyFromProductId) // delete Property from Product by ID