384 lines
16 KiB
JavaScript
384 lines
16 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import classNames from 'classnames';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Button } from 'primereact/button';
|
|
import { FileUpload } from 'primereact/fileupload';
|
|
import { Rating } from 'primereact/rating';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
import { RadioButton } from 'primereact/radiobutton';
|
|
import { InputNumber } from 'primereact/inputnumber';
|
|
import { Dialog } from 'primereact/dialog';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { ProductService } from '../service/ProductService';
|
|
|
|
const Crud = () => {
|
|
let emptyProduct = {
|
|
id: null,
|
|
name: '',
|
|
image: null,
|
|
description: '',
|
|
category: null,
|
|
price: 0,
|
|
quantity: 0,
|
|
rating: 0,
|
|
inventoryStatus: 'INSTOCK'
|
|
};
|
|
|
|
const [products, setProducts] = useState(null);
|
|
const [productDialog, setProductDialog] = useState(false);
|
|
const [deleteProductDialog, setDeleteProductDialog] = useState(false);
|
|
const [deleteProductsDialog, setDeleteProductsDialog] = useState(false);
|
|
const [product, setProduct] = useState(emptyProduct);
|
|
const [selectedProducts, setSelectedProducts] = useState(null);
|
|
const [submitted, setSubmitted] = useState(false);
|
|
const [globalFilter, setGlobalFilter] = useState(null);
|
|
const toast = useRef(null);
|
|
const dt = useRef(null);
|
|
|
|
useEffect(() => {
|
|
const productService = new ProductService();
|
|
productService.getProducts().then(data => setProducts(data));
|
|
}, []);
|
|
|
|
const formatCurrency = (value) => {
|
|
return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
|
|
}
|
|
|
|
const openNew = () => {
|
|
setProduct(emptyProduct);
|
|
setSubmitted(false);
|
|
setProductDialog(true);
|
|
}
|
|
|
|
const hideDialog = () => {
|
|
setSubmitted(false);
|
|
setProductDialog(false);
|
|
}
|
|
|
|
const hideDeleteProductDialog = () => {
|
|
setDeleteProductDialog(false);
|
|
}
|
|
|
|
const hideDeleteProductsDialog = () => {
|
|
setDeleteProductsDialog(false);
|
|
}
|
|
|
|
const saveProduct = () => {
|
|
setSubmitted(true);
|
|
|
|
if (product.name.trim()) {
|
|
let _products = [...products];
|
|
let _product = { ...product };
|
|
if (product.id) {
|
|
const index = findIndexById(product.id);
|
|
|
|
_products[index] = _product;
|
|
toast.current.show({ severity: 'success', summary: 'Successful', detail: 'Product Updated', life: 3000 });
|
|
}
|
|
else {
|
|
_product.id = createId();
|
|
_product.image = 'product-placeholder.svg';
|
|
_products.push(_product);
|
|
toast.current.show({ severity: 'success', summary: 'Successful', detail: 'Product Created', life: 3000 });
|
|
}
|
|
|
|
setProducts(_products);
|
|
setProductDialog(false);
|
|
setProduct(emptyProduct);
|
|
}
|
|
}
|
|
|
|
const editProduct = (product) => {
|
|
setProduct({ ...product });
|
|
setProductDialog(true);
|
|
}
|
|
|
|
const confirmDeleteProduct = (product) => {
|
|
setProduct(product);
|
|
setDeleteProductDialog(true);
|
|
}
|
|
|
|
const deleteProduct = () => {
|
|
let _products = products.filter(val => val.id !== product.id);
|
|
setProducts(_products);
|
|
setDeleteProductDialog(false);
|
|
setProduct(emptyProduct);
|
|
toast.current.show({ severity: 'success', summary: 'Successful', detail: 'Product Deleted', life: 3000 });
|
|
}
|
|
|
|
const findIndexById = (id) => {
|
|
let index = -1;
|
|
for (let i = 0; i < products.length; i++) {
|
|
if (products[i].id === id) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
const createId = () => {
|
|
let id = '';
|
|
let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
for (let i = 0; i < 5; i++) {
|
|
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return id;
|
|
}
|
|
|
|
const exportCSV = () => {
|
|
dt.current.exportCSV();
|
|
}
|
|
|
|
const confirmDeleteSelected = () => {
|
|
setDeleteProductsDialog(true);
|
|
}
|
|
|
|
const deleteSelectedProducts = () => {
|
|
let _products = products.filter(val => !selectedProducts.includes(val));
|
|
setProducts(_products);
|
|
setDeleteProductsDialog(false);
|
|
setSelectedProducts(null);
|
|
toast.current.show({ severity: 'success', summary: 'Successful', detail: 'Products Deleted', life: 3000 });
|
|
}
|
|
|
|
const onCategoryChange = (e) => {
|
|
let _product = { ...product };
|
|
_product['category'] = e.value;
|
|
setProduct(_product);
|
|
}
|
|
|
|
const onInputChange = (e, name) => {
|
|
const val = (e.target && e.target.value) || '';
|
|
let _product = { ...product };
|
|
_product[`${name}`] = val;
|
|
|
|
setProduct(_product);
|
|
}
|
|
|
|
const onInputNumberChange = (e, name) => {
|
|
const val = e.value || 0;
|
|
let _product = { ...product };
|
|
_product[`${name}`] = val;
|
|
|
|
setProduct(_product);
|
|
}
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<React.Fragment>
|
|
<div className="my-2">
|
|
<Button label="New" icon="pi pi-plus" className="p-button-success mr-2" onClick={openNew} />
|
|
<Button label="Delete" icon="pi pi-trash" className="p-button-danger" onClick={confirmDeleteSelected} disabled={!selectedProducts || !selectedProducts.length} />
|
|
</div>
|
|
</React.Fragment>
|
|
)
|
|
}
|
|
|
|
const rightToolbarTemplate = () => {
|
|
return (
|
|
<React.Fragment>
|
|
<FileUpload mode="basic" accept="image/*" maxFileSize={1000000} label="Import" chooseLabel="Import" className="mr-2 inline-block" />
|
|
<Button label="Export" icon="pi pi-upload" className="p-button-help" onClick={exportCSV} />
|
|
</React.Fragment>
|
|
)
|
|
}
|
|
|
|
const codeBodyTemplate = (rowData) => {
|
|
return (
|
|
<>
|
|
<span className="p-column-title">Code</span>
|
|
{rowData.code}
|
|
</>
|
|
);
|
|
}
|
|
|
|
const nameBodyTemplate = (rowData) => {
|
|
return (
|
|
<>
|
|
<span className="p-column-title">Name</span>
|
|
{rowData.name}
|
|
</>
|
|
);
|
|
}
|
|
|
|
const imageBodyTemplate = (rowData) => {
|
|
return (
|
|
<>
|
|
<span className="p-column-title">Image</span>
|
|
<img src={`assets/demo/images/product/${rowData.image}`} alt={rowData.image} className="shadow-2" width="100" />
|
|
</>
|
|
)
|
|
}
|
|
|
|
const priceBodyTemplate = (rowData) => {
|
|
return (
|
|
<>
|
|
<span className="p-column-title">Price</span>
|
|
{formatCurrency(rowData.price)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
const categoryBodyTemplate = (rowData) => {
|
|
return (
|
|
<>
|
|
<span className="p-column-title">Category</span>
|
|
{rowData.category}
|
|
</>
|
|
);
|
|
}
|
|
|
|
const ratingBodyTemplate = (rowData) => {
|
|
return (
|
|
<>
|
|
<span className="p-column-title">Reviews</span>
|
|
<Rating value={rowData.rating} readonly cancel={false} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
const statusBodyTemplate = (rowData) => {
|
|
return (
|
|
<>
|
|
<span className="p-column-title">Hi Status</span>
|
|
<span className={`product-badge status-${rowData.inventoryStatus.toLowerCase()}`}>{rowData.inventoryStatus}</span>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const actionBodyTemplate = (rowData) => {
|
|
return (
|
|
<div className="actions">
|
|
<Button icon="pi pi-pencil" className="p-button-rounded p-button-success mr-2" onClick={() => editProduct(rowData)} />
|
|
<Button icon="pi pi-trash" className="p-button-rounded p-button-warning mt-2" onClick={() => confirmDeleteProduct(rowData)} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const header = (
|
|
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
|
<h5 className="m-0">Manage Products</h5>
|
|
<span className="block mt-2 md:mt-0 p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<InputText type="search" onInput={(e) => setGlobalFilter(e.target.value)} placeholder="Search..." />
|
|
</span>
|
|
</div>
|
|
);
|
|
|
|
const productDialogFooter = (
|
|
<>
|
|
<Button label="Cancel" icon="pi pi-times" className="p-button-text" onClick={hideDialog} />
|
|
<Button label="Save" icon="pi pi-check" className="p-button-text" onClick={saveProduct} />
|
|
</>
|
|
);
|
|
const deleteProductDialogFooter = (
|
|
<>
|
|
<Button label="No" icon="pi pi-times" className="p-button-text" onClick={hideDeleteProductDialog} />
|
|
<Button label="Yes" icon="pi pi-check" className="p-button-text" onClick={deleteProduct} />
|
|
</>
|
|
);
|
|
const deleteProductsDialogFooter = (
|
|
<>
|
|
<Button label="No" icon="pi pi-times" className="p-button-text" onClick={hideDeleteProductsDialog} />
|
|
<Button label="Yes" icon="pi pi-check" className="p-button-text" onClick={deleteSelectedProducts} />
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<div className="grid crud-demo">
|
|
<div className="col-12">
|
|
<div className="card">
|
|
<Toast ref={toast} />
|
|
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate}></Toolbar>
|
|
|
|
<DataTable ref={dt} value={products} selection={selectedProducts} onSelectionChange={(e) => setSelectedProducts(e.value)}
|
|
dataKey="id" paginator rows={10} rowsPerPageOptions={[5, 10, 25]} className="datatable-responsive"
|
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
|
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} products"
|
|
globalFilter={globalFilter} emptyMessage="No products found." header={header} responsiveLayout="scroll">
|
|
<Column selectionMode="multiple" headerStyle={{ width: '3rem'}}></Column>
|
|
<Column field="code" header="Code" sortable body={codeBodyTemplate} headerStyle={{ width: '14%', minWidth: '10rem' }}></Column>
|
|
<Column field="name" header="Name" sortable body={nameBodyTemplate} headerStyle={{ width: '14%', minWidth: '10rem' }}></Column>
|
|
<Column header="Image" body={imageBodyTemplate} headerStyle={{ width: '14%', minWidth: '10rem' }}></Column>
|
|
<Column field="price" header="Price" body={priceBodyTemplate} sortable headerStyle={{ width: '14%', minWidth: '8rem' }}></Column>
|
|
<Column field="category" header="Category" sortable body={categoryBodyTemplate} headerStyle={{ width: '14%', minWidth: '10rem' }}></Column>
|
|
<Column field="rating" header="Reviews" body={ratingBodyTemplate} sortable headerStyle={{ width: '14%', minWidth: '10rem' }}></Column>
|
|
<Column field="inventoryStatus" header="Status" body={statusBodyTemplate} sortable headerStyle={{ width: '14%', minWidth: '10rem' }}></Column>
|
|
<Column body={actionBodyTemplate}></Column>
|
|
</DataTable>
|
|
|
|
<Dialog visible={productDialog} style={{ width: '450px' }} header="Product Details" modal className="p-fluid" footer={productDialogFooter} onHide={hideDialog}>
|
|
{product.image && <img src={`assets/demo/images/product/${product.image}`} alt={product.image} width="150" className="mt-0 mx-auto mb-5 block shadow-2" />}
|
|
<div className="field">
|
|
<label htmlFor="name">Name</label>
|
|
<InputText id="name" value={product.name} onChange={(e) => onInputChange(e, 'name')} required autoFocus className={classNames({ 'p-invalid': submitted && !product.name })} />
|
|
{submitted && !product.name && <small className="p-invalid">Name is required.</small>}
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="description">Description</label>
|
|
<InputTextarea id="description" value={product.description} onChange={(e) => onInputChange(e, 'description')} required rows={3} cols={20} />
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label className="mb-3">Category</label>
|
|
<div className="formgrid grid">
|
|
<div className="field-radiobutton col-6">
|
|
<RadioButton inputId="category1" name="category" value="Accessories" onChange={onCategoryChange} checked={product.category === 'Accessories'} />
|
|
<label htmlFor="category1">Accessories</label>
|
|
</div>
|
|
<div className="field-radiobutton col-6">
|
|
<RadioButton inputId="category2" name="category" value="Clothing" onChange={onCategoryChange} checked={product.category === 'Clothing'} />
|
|
<label htmlFor="category2">Clothing</label>
|
|
</div>
|
|
<div className="field-radiobutton col-6">
|
|
<RadioButton inputId="category3" name="category" value="Electronics" onChange={onCategoryChange} checked={product.category === 'Electronics'} />
|
|
<label htmlFor="category3">Electronics</label>
|
|
</div>
|
|
<div className="field-radiobutton col-6">
|
|
<RadioButton inputId="category4" name="category" value="Fitness" onChange={onCategoryChange} checked={product.category === 'Fitness'} />
|
|
<label htmlFor="category4">Fitness</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="formgrid grid">
|
|
<div className="field col">
|
|
<label htmlFor="price">Price</label>
|
|
<InputNumber id="price" value={product.price} onValueChange={(e) => onInputNumberChange(e, 'price')} mode="currency" currency="USD" locale="en-US" />
|
|
</div>
|
|
<div className="field col">
|
|
<label htmlFor="quantity">Quantity</label>
|
|
<InputNumber id="quantity" value={product.quantity} onValueChange={(e) => onInputNumberChange(e, 'quantity')} integeronly />
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
|
|
<Dialog visible={deleteProductDialog} style={{ width: '450px' }} header="Confirm" modal footer={deleteProductDialogFooter} onHide={hideDeleteProductDialog}>
|
|
<div className="flex align-items-center justify-content-center">
|
|
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
|
|
{product && <span>Are you sure you want to delete <b>{product.name}</b>?</span>}
|
|
</div>
|
|
</Dialog>
|
|
|
|
<Dialog visible={deleteProductsDialog} style={{ width: '450px' }} header="Confirm" modal footer={deleteProductsDialogFooter} onHide={hideDeleteProductsDialog}>
|
|
<div className="flex align-items-center justify-content-center">
|
|
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
|
|
{product && <span>Are you sure you want to delete the selected products?</span>}
|
|
</div>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const comparisonFn = function (prevProps, nextProps) {
|
|
return prevProps.location.pathname === nextProps.location.pathname;
|
|
};
|
|
|
|
export default React.memo(Crud, comparisonFn); |