This commit is contained in:
Flatlogic Bot 2026-01-16 13:20:05 +00:00
parent 4200cf61a5
commit eee374359b
6 changed files with 157 additions and 484 deletions

View File

@ -106,7 +106,7 @@ app.use('/api/customers', passport.authenticate('jwt', {session: false}), custom
app.use('/api/categories', passport.authenticate('jwt', {session: false}), categoriesRoutes);
app.use('/api/products', passport.authenticate('jwt', {session: false}), productsRoutes);
app.use('/api/products', productsRoutes);
app.use('/api/orders', passport.authenticate('jwt', {session: false}), ordersRoutes);

View File

@ -15,6 +15,93 @@ const {
checkCrudPermissions,
} = require('../middlewares/check-permissions');
/**
* @swagger
* /api/products:
* get:
* tags: [Products]
* summary: Get all products
* description: Get all products
* responses:
* 200:
* description: Products list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Products"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/', wrapAsync(async (req, res) => {
const filetype = req.query.filetype
const currentUser = req.currentUser;
const payload = await ProductsDBApi.findAll(
req.query, { currentUser }
);
if (filetype && filetype === 'csv') {
const fields = ['id','name','sku','description',
'stock',
'price',
];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv)
} catch (err) {
console.error(err);
}
} else {
res.status(200).send(payload);
}
}));
/**
* @swagger
* /api/products/{id}:
* get:
* tags: [Products]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Products"
* 400:
* description: Invalid ID supplied
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ProductsDBApi.findBy(
{ id: req.params.id },
);
res.status(200).send(payload);
}));
router.use(checkCrudPermissions('products'));
@ -108,7 +195,7 @@ router.post('/', wrapAsync(async (req, res) => {
* content:
* application/json:
* schema:
* properties:
* properties:
* data:
* description: Data of the updated items
* type: array
@ -267,174 +354,7 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
res.status(200).send(payload);
}));
/**
* @swagger
* /api/products:
* get:
* security:
* - bearerAuth: []
* tags: [Products]
* summary: Get all products
* description: Get all products
* responses:
* 200:
* description: Products list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Products"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/', wrapAsync(async (req, res) => {
const filetype = req.query.filetype
const currentUser = req.currentUser;
const payload = await ProductsDBApi.findAll(
req.query, { currentUser }
);
if (filetype && filetype === 'csv') {
const fields = ['id','name','sku','description',
'stock',
'price',
];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv)
} catch (err) {
console.error(err);
}
} else {
res.status(200).send(payload);
}
}));
/**
* @swagger
* /api/products/count:
* get:
* security:
* - bearerAuth: []
* tags: [Products]
* summary: Count all products
* description: Count all products
* responses:
* 200:
* description: Products count successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Products"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/count', wrapAsync(async (req, res) => {
const currentUser = req.currentUser;
const payload = await ProductsDBApi.findAll(
req.query,
null,
{ countOnly: true, currentUser }
);
res.status(200).send(payload);
}));
/**
* @swagger
* /api/products/autocomplete:
* get:
* security:
* - bearerAuth: []
* tags: [Products]
* summary: Find all products that match search criteria
* description: Find all products that match search criteria
* responses:
* 200:
* description: Products list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Products"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/autocomplete', async (req, res) => {
const payload = await ProductsDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
);
res.status(200).send(payload);
});
/**
* @swagger
* /api/products/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Products]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Products"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ProductsDBApi.findBy(
{ id: req.params.id },
);
res.status(200).send(payload);
}));
router.use('/', require('../helpers').commonErrorHandler);

View File

@ -103,7 +103,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
const generateFilterRequests = useMemo(() => {
let request = '&';
filterItems.forEach((item) => {
filterItems?.forEach((item) => {
const isRangeFilter = filters.find(
(filter) =>
filter.title === item.fields.selectedField &&

View File

@ -7,6 +7,8 @@ import LayoutAuthenticated from '../layouts/Authenticated'
import SectionMain from '../components/SectionMain'
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
import BaseIcon from "../components/BaseIcon";
import ProductsTable from '../components/Products/TableProducts';
import { useSampleClients } from '../hooks/sampleData';
import { getPageTitle } from '../config'
import Link from "next/link";
@ -22,6 +24,7 @@ const Dashboard = () => {
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const { clients } = useSampleClients();
const loadingMessage = 'Loading...';
@ -141,6 +144,13 @@ const Dashboard = () => {
{!!rolesWidgets.length && <hr className='my-6 text-skyBlueTheme-mainBG ' />}
<SectionTitleLineWithButton
icon={icon.mdiPackageVariant}
title="Products"
></SectionTitleLineWithButton>
<div className="mb-6">
<ProductsTable clients={clients} />
</div>
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>

View File

@ -1,166 +1,57 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch as fetchProducts } from '../stores/products/productsSlice';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import CardBox from '../components/CardBox';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import Head from 'next/head';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { mdiStorefrontOutline } from '@mdi/js';
const IndexPage = () => {
const dispatch = useAppDispatch();
const { products, loading } = useAppSelector((state) => state.products);
useEffect(() => {
dispatch(fetchProducts({}));
}, [dispatch]);
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('left');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Store Operations Dashboard'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<LayoutGuest>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Shop')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiStorefrontOutline} title="Our Products" main>
</SectionTitleLineWithButton>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Store Operations Dashboard app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
{loading && <div>Loading...</div>}
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
{!loading && products && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{products.map((product) => (
<CardBox key={product.id}>
<div className="h-48 overflow-hidden bg-gray-100 dark:bg-slate-800 flex items-center justify-center">
{product.images && product.images.length > 0 ? (
<img src={product.images[0].publicUrl} alt={product.name} className="w-full h-full object-cover"/>
) : (
<span className="text-gray-400">No Image</span>
)}
</div>
<div className="p-4">
<h4 className="text-xl font-bold">{product.name}</h4>
<p className="text-gray-500">${product.price}</p>
</div>
</CardBox>
))}
</div>
)}
</SectionMain>
</LayoutGuest>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
export default IndexPage;

View File

@ -1,166 +1,18 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableProducts from '../../components/Products/TableProducts'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/products/productsSlice';
import { useSampleClients } from '../../hooks/sampleData';
import { getPageTitle } from '../../config';
import ProductsTable from '../../components/Products/TableProducts';
import LayoutAuthenticated from '../../layouts/Authenticated';
import {hasPermission} from "../../helpers/userPermissions";
const ProductsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Name', title: 'name'},{label: 'SKU', title: 'sku'},{label: 'Description', title: 'description'},
{label: 'Stock', title: 'stock', number: 'true'},
{label: 'Price', title: 'price', number: 'true'},
{label: 'Category', title: 'category'},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PRODUCTS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const getProductsCSV = async () => {
const response = await axios({url: '/products?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'productsCSV.csv'
link.click()
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const ProductsPage = () => {
const { clients } = useSampleClients();
return (
<>
<Head>
<title>{getPageTitle('Products')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Products" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/products/products-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getProductsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
<TableProducts
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</>
)
}
<LayoutAuthenticated>
<title>{getPageTitle('Products')}</title>
<ProductsTable clients={clients} />
</LayoutAuthenticated>
);
};
ProductsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_PRODUCTS'}
>
{page}
</LayoutAuthenticated>
)
}
export default ProductsTablesPage
export default ProductsPage;