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={{
|
||||
toolbar: () => (
|
||||
<CrudTableToolbar
|
||||
title="Here you see all your available languages for translations."
|
||||
title="Available languages:"
|
||||
disableDelete={isDeleteDisabled(rowSelectionModel)}
|
||||
onDelete={handleDeleteLanguage}
|
||||
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 = {
|
||||
ProductGroups?: ProductGroup[];
|
||||
setSelectedRow: (id: number) => void;
|
||||
};
|
||||
|
||||
export default function ProductGroupTreeView({ ProductGroups = [] }: ProductGroupTreeViewProps) {
|
||||
export default function ProductGroupTreeView({ ProductGroups = [], setSelectedRow }: ProductGroupTreeViewProps) {
|
||||
|
||||
const renderTree = (node: ProductGroup) => (
|
||||
<TreeItem key={node.id} itemId={node.id.toString()} label={node.label}>
|
||||
@@ -20,7 +21,7 @@ export default function ProductGroupTreeView({ ProductGroups = [] }: ProductGrou
|
||||
);
|
||||
|
||||
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}}>
|
||||
Product Groups:
|
||||
</Typography>
|
||||
@@ -28,6 +29,7 @@ export default function ProductGroupTreeView({ ProductGroups = [] }: ProductGrou
|
||||
<SimpleTreeView
|
||||
aria-label="Product Group Tree View"
|
||||
sx={{ height: 500, flexGrow: 1, overflowY: 'auto' }}
|
||||
onItemClick={(_event, itemId) => setSelectedRow(Number(itemId))}
|
||||
>
|
||||
{ProductGroups.map((productGroup) => renderTree(productGroup))}
|
||||
</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;
|
||||
path: string;
|
||||
childProductGroups: ProductGroup[];
|
||||
PropertyTypes: PropertyType[];
|
||||
Products: Product[];
|
||||
propertyTypes: PropertyType[];
|
||||
products: Product[];
|
||||
};
|
||||
@@ -4,9 +4,12 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import type { ProductGroup } from '../data/productGroup';
|
||||
import { getProductGroupById } from '../api/language/productGroup.api';
|
||||
import ProductTableView from '../components/ProductTableView';
|
||||
|
||||
|
||||
const ProductPage: React.FC = () => {
|
||||
const [productGroup, setProductGroup] = useState<ProductGroup | null>(null);
|
||||
const [selectedRow, setSelectedRow] = useState(3);
|
||||
|
||||
const loadProductGroup = React.useCallback(async () => {
|
||||
const loadedProductGroup = await getProductGroupById(3); // Assuming 3 is the ID for product groups
|
||||
@@ -31,10 +34,12 @@ const ProductPage: React.FC = () => {
|
||||
bgcolor: "#f5f5f5"
|
||||
}}
|
||||
>
|
||||
<ProductGroupTreeView ProductGroups={productGroup ? [productGroup] : []} />
|
||||
<ProductGroupTreeView ProductGroups={productGroup ? [productGroup] : []} setSelectedRow={setSelectedRow} />
|
||||
<ProductTableView selectedRow={selectedRow} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default ProductPage;
|
||||
|
||||
@@ -216,7 +216,7 @@ func (rr ProductgroupRepository) GetProductgroup(ProductgroupId int) (*models.Pr
|
||||
}
|
||||
|
||||
// get property types of this product group
|
||||
propertyTypes, err3 := rr.GetPropertyTypesByProductGroupID(pg.ID)
|
||||
propertyTypes, err3 := rr.GetPropertyTypesByProductGroup(pg)
|
||||
if err3 != nil {
|
||||
return nil, &models.ResponseError{
|
||||
Message: err3.Message,
|
||||
@@ -233,6 +233,30 @@ func (rr ProductgroupRepository) GetProductgroup(ProductgroupId int) (*models.Pr
|
||||
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) {
|
||||
var propertyTypes []*models.PropertyType
|
||||
query := `SELECT property_type_id FROM product_group_property_types_list WHERE product_group_id=$1`
|
||||
|
||||
Reference in New Issue
Block a user