Product View aufgebaut

This commit is contained in:
2026-08-13 18:45:08 +02:00
parent 55a3332938
commit 10e4b54335
8 changed files with 288 additions and 7 deletions
@@ -0,0 +1,26 @@
import ky from 'ky'
import type { Product } from '../../data/product'
export const getProducts = async (): Promise<Product[]> => {
return ky.get(`http://localhost:8088/products`).json<Product[]>()
}
export const getProductById = async (id: number): Promise<Product> => {
return ky.get(`http://localhost:8088/product/${id}`).json<Product>()
}
export const createProduct = async (id: string): Promise<Product> => {
return ky.post(`http://localhost:8088/product`, { json: { id } }).json<Product>()
}
export const updateProduct = async (product: Product): Promise<void> => {
await ky.put(`http://localhost:8088/product`, { json: product })
}
export const deleteProduct = async (id: number): Promise<void> => {
await ky.delete(`http://localhost:8088/product/${id}`)
}
export const getProductsByProductGroupId = async (productGroupId: number): Promise<Product[]> => {
return ky.get(`http://localhost:8088/products/productGroup/${productGroupId}`).json<Product[]>()
}
@@ -59,7 +59,7 @@ const LanguagesTableView = ({ languages, setSelectedRow, refreshLanguages }: { l
slots={{ slots={{
toolbar: () => ( toolbar: () => (
<CrudTableToolbar <CrudTableToolbar
title="Here you see all your available languages for translations." title="Available languages:"
disableDelete={isDeleteDisabled(rowSelectionModel)} disableDelete={isDeleteDisabled(rowSelectionModel)}
onDelete={handleDeleteLanguage} onDelete={handleDeleteLanguage}
onAdd={() => setIsCreateDialogOpen(true)} onAdd={() => setIsCreateDialogOpen(true)}
@@ -0,0 +1,74 @@
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import CreateEntityDialog from './CreateEntityDialog';
import type { Product } from '../data/product';
type ProductCreateDialogProps = {
open: boolean;
onClose: () => void;
onCreated: (product: Product) => void | Promise<void>;
};
type ProductFormState = {
product_identifier: string;
label: string;
};
const initialFormState: ProductFormState = {
product_identifier: '',
label: '',
};
const check = (formState: ProductFormState) => {
return formState.product_identifier.trim() !== '' && formState.label.trim() !== '';
};
const ProductCreateDialog = ({ open, onClose, onCreated }: ProductCreateDialogProps) => {
return (
<CreateEntityDialog
open={open}
title="New Product"
initialState={initialFormState}
onClose={onClose}
onCreated={onCreated}
createEntity={async (formState) => {
if (!check(formState)) {
throw new Error('Invalid product form');
}
return {
id: 0,
product_identifier: formState.product_identifier,
label: formState.label,
properties: [],
products: [],
productStructureNodes: [],
isEqualPropertiesEqualValues: false,
} satisfies Product;
}}
errorMessageText="Creating the product failed."
>
{({ formState, setFormState }) => (
<Stack spacing={2} sx={{ pt: 1 }}>
<TextField
autoFocus
required
label="Product Identifier"
value={formState.product_identifier}
onChange={(event) => setFormState((currentState) => ({ ...currentState, product_identifier: event.target.value }))}
/>
<TextField
required
label="Label"
value={formState.label}
onChange={(event) => setFormState((currentState) => ({ ...currentState, label: event.target.value }))}
/>
</Stack>
)}
</CreateEntityDialog>
);
};
export default ProductCreateDialog;
@@ -7,9 +7,10 @@ import Divider from '@mui/material/Divider';
type ProductGroupTreeViewProps = { type ProductGroupTreeViewProps = {
ProductGroups?: ProductGroup[]; ProductGroups?: ProductGroup[];
setSelectedRow: (id: number) => void;
}; };
export default function ProductGroupTreeView({ ProductGroups = [] }: ProductGroupTreeViewProps) { export default function ProductGroupTreeView({ ProductGroups = [], setSelectedRow }: ProductGroupTreeViewProps) {
const renderTree = (node: ProductGroup) => ( const renderTree = (node: ProductGroup) => (
<TreeItem key={node.id} itemId={node.id.toString()} label={node.label}> <TreeItem key={node.id} itemId={node.id.toString()} label={node.label}>
@@ -20,7 +21,7 @@ export default function ProductGroupTreeView({ ProductGroups = [] }: ProductGrou
); );
return ( return (
<Paper sx={{ height: 600, width: '10vw', marginRight: 2, border: '1px solid', borderColor: 'primary.main', borderRadius: 1, p: 1, bgcolor: '#f9f9f9' }}> <Paper sx={{ height: 585, width: '10vw', marginRight: 2, border: '1px solid', borderColor: 'primary.main', borderRadius: 2, p: 1, bgcolor: '#f9f9f9' }}>
<Typography variant="body1" component="div" sx={{ flexGrow: 1, p: 1}}> <Typography variant="body1" component="div" sx={{ flexGrow: 1, p: 1}}>
Product Groups: Product Groups:
</Typography> </Typography>
@@ -28,6 +29,7 @@ export default function ProductGroupTreeView({ ProductGroups = [] }: ProductGrou
<SimpleTreeView <SimpleTreeView
aria-label="Product Group Tree View" aria-label="Product Group Tree View"
sx={{ height: 500, flexGrow: 1, overflowY: 'auto' }} sx={{ height: 500, flexGrow: 1, overflowY: 'auto' }}
onItemClick={(_event, itemId) => setSelectedRow(Number(itemId))}
> >
{ProductGroups.map((productGroup) => renderTree(productGroup))} {ProductGroups.map((productGroup) => renderTree(productGroup))}
</SimpleTreeView> </SimpleTreeView>
@@ -0,0 +1,150 @@
import * as React from 'react';
import Alert from '@mui/material/Alert';
import CrudDataGrid from './CrudDataGrid';
import CrudTableToolbar from './CrudTableToolbar';
import { alternatingRowClassName, createEmptyRowSelectionModel, getSelectedNumericRowId, isDeleteDisabled } from './tableViewUtils';
import type { GridRowSelectionModel } from '@mui/x-data-grid';
import {getProductsByProductGroupId} from '../api/language/products.api';
import {getProductGroupById} from '../api/language/productGroup.api';
import type { ProductGroup } from '../data/productGroup';
import type { Product } from '../data/product';
import ProductCreateDialog from './ProductCreateDialog';
import { deleteProduct } from '../api/language/products.api';
const ProductTableView = ({ selectedRow }: { selectedRow: number }) => {
const [products, setProducts] = React.useState<Product[]>([]);
const [productGroup, setProductGroup] = React.useState<ProductGroup | null>(null);
const [rowSelectionModel, setRowSelectionModel] = React.useState<GridRowSelectionModel>(createEmptyRowSelectionModel);
const [isCreateDialogOpen, setIsCreateDialogOpen] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState('');
const loadProducts = React.useCallback(() => {
if (selectedRow <= 0) {
setProducts([]);
setRowSelectionModel(createEmptyRowSelectionModel());
return;
}
getProductsByProductGroupId(selectedRow)
.then(setProducts)
.catch(() => setErrorMessage('Loading products failed.'));
}, [selectedRow]);
React.useEffect(() => {
loadProducts();
}, [loadProducts]);
React.useEffect(() => {
if (selectedRow <= 0) {
setProductGroup(null);
return;
}
getProductGroupById(selectedRow).then(setProductGroup);
}, [selectedRow]);
const handleProductCreated = () => {
setIsCreateDialogOpen(false);
setErrorMessage('');
loadProducts();
};
const handleDeleteProduct = async () => {
const selectedProductId = getSelectedNumericRowId(rowSelectionModel);
if (selectedProductId == null) {
setErrorMessage('Select a product before deleting it.');
return;
}
try {
await deleteProduct(selectedProductId);
setRowSelectionModel(createEmptyRowSelectionModel());
setErrorMessage('');
loadProducts();
} catch {
setErrorMessage('Deleting the product failed.');
}
};
const rows = React.useMemo(
() =>
products.map((product) => {
const propertyValues = Object.fromEntries(
(productGroup?.propertyTypes ?? []).map((propertyType) => {
const matchingProperty = (product.properties as Array<Record<string, unknown>> | undefined)?.find(
(property) =>
Number(property?.propertyTypeID ?? property?.propertyTypeId ?? property?.propertyType_id) === propertyType.id,
);
return [
`property_${propertyType.id}`,
matchingProperty?.value ?? '',
];
}),
);
return {
...product,
...propertyValues,
};
}),
[products, productGroup],
);
const columns = React.useMemo(
() => [
{ field: 'id', headerName: 'ID', width: 100, editable: false },
{ field: 'product_identifier', headerName: 'Product Identifier', width: 200, editable: true },
{ field: 'label', headerName: 'Label', width: 200, editable: true },
...(productGroup?.propertyTypes?.map((propertyType) => ({
field: `property_${propertyType.id}`,
headerName: propertyType.label,
width: 200,
editable: true,
})) ?? []),
],
[productGroup],
);
return (
<div>
{errorMessage !== '' && <Alert severity="error" sx={{ mb: 2 }}>{errorMessage}</Alert>}
<CrudDataGrid
paperSx={{ height: 600, width: '50vw' }}
initialState={{
sorting: {
sortModel: [{ field: 'original', sort: 'asc' }],
},
}}
rows={rows}
columns={columns}
slots={{
toolbar: () => (
<CrudTableToolbar
title={`Products for ${productGroup?.label}.`}
disableAdd={selectedRow <= 0}
disableDelete={isDeleteDisabled(rowSelectionModel)}
onDelete={handleDeleteProduct}
onAdd={() => setIsCreateDialogOpen(true)}
/>
),
}}
processRowUpdate={async (updatedRow) => {
setErrorMessage('');
return updatedRow;
}}
onProcessRowUpdateError={() => setErrorMessage('Saving the translation failed.')}
getRowClassName={alternatingRowClassName}
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={(newRowSelectionModel) => setRowSelectionModel(newRowSelectionModel)}
/>
<ProductCreateDialog
open={isCreateDialogOpen}
onClose={() => setIsCreateDialogOpen(false)}
onCreated={handleProductCreated}
/>
</div>
);
};
export default ProductTableView;
@@ -8,6 +8,6 @@ export type ProductGroup = {
isVisible: boolean; isVisible: boolean;
path: string; path: string;
childProductGroups: ProductGroup[]; childProductGroups: ProductGroup[];
PropertyTypes: PropertyType[]; propertyTypes: PropertyType[];
Products: Product[]; products: Product[];
}; };
@@ -4,9 +4,12 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import type { ProductGroup } from '../data/productGroup'; import type { ProductGroup } from '../data/productGroup';
import { getProductGroupById } from '../api/language/productGroup.api'; import { getProductGroupById } from '../api/language/productGroup.api';
import ProductTableView from '../components/ProductTableView';
const ProductPage: React.FC = () => { const ProductPage: React.FC = () => {
const [productGroup, setProductGroup] = useState<ProductGroup | null>(null); const [productGroup, setProductGroup] = useState<ProductGroup | null>(null);
const [selectedRow, setSelectedRow] = useState(3);
const loadProductGroup = React.useCallback(async () => { const loadProductGroup = React.useCallback(async () => {
const loadedProductGroup = await getProductGroupById(3); // Assuming 3 is the ID for product groups const loadedProductGroup = await getProductGroupById(3); // Assuming 3 is the ID for product groups
@@ -31,10 +34,12 @@ const ProductPage: React.FC = () => {
bgcolor: "#f5f5f5" bgcolor: "#f5f5f5"
}} }}
> >
<ProductGroupTreeView ProductGroups={productGroup ? [productGroup] : []} /> <ProductGroupTreeView ProductGroups={productGroup ? [productGroup] : []} setSelectedRow={setSelectedRow} />
<ProductTableView selectedRow={selectedRow} />
</Box> </Box>
</Box> </Box>
); );
}; };
export default ProductPage; export default ProductPage;
@@ -216,7 +216,7 @@ func (rr ProductgroupRepository) GetProductgroup(ProductgroupId int) (*models.Pr
} }
// get property types of this product group // get property types of this product group
propertyTypes, err3 := rr.GetPropertyTypesByProductGroupID(pg.ID) propertyTypes, err3 := rr.GetPropertyTypesByProductGroup(pg)
if err3 != nil { if err3 != nil {
return nil, &models.ResponseError{ return nil, &models.ResponseError{
Message: err3.Message, Message: err3.Message,
@@ -233,6 +233,30 @@ func (rr ProductgroupRepository) GetProductgroup(ProductgroupId int) (*models.Pr
return pg, nil return pg, nil
} }
func (rr ProductgroupRepository) GetPropertyTypesByProductGroup(productGroup *models.Productgroup) ([]*models.PropertyType, *models.ResponseError) {
if productGroup.Label == "Simply CPQ" {
return rr.GetPropertyTypesByProductGroupID(productGroup.ID)
} else {
PropertyTypesParent, err := rr.GetPropertyTypesByProductGroupID(productGroup.ParentProductGroupID)
if err != nil {
return nil, &models.ResponseError{
Message: err.Message,
Status: http.StatusInternalServerError,
}
}
PropertyTypesChild, err := rr.GetPropertyTypesByProductGroupID(productGroup.ID)
if err != nil {
return nil, &models.ResponseError{
Message: err.Message,
Status: http.StatusInternalServerError,
}
}
return append(PropertyTypesParent, PropertyTypesChild...), nil
}
}
func (rr ProductgroupRepository) GetPropertyTypesByProductGroupID(productGroupID int) ([]*models.PropertyType, *models.ResponseError) { func (rr ProductgroupRepository) GetPropertyTypesByProductGroupID(productGroupID int) ([]*models.PropertyType, *models.ResponseError) {
var propertyTypes []*models.PropertyType var propertyTypes []*models.PropertyType
query := `SELECT property_type_id FROM product_group_property_types_list WHERE product_group_id=$1` query := `SELECT property_type_id FROM product_group_property_types_list WHERE product_group_id=$1`