Product View aufgebaut
This commit is contained in:
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user