diff --git a/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/[id]/edit/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/[id]/edit/page.tsx new file mode 100644 index 0000000..6392314 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/[id]/edit/page.tsx @@ -0,0 +1,49 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Component Imports +import ProductAddHeader from '@views/apps/ecommerce/products/add/ProductAddHeader' +import ProductInformation from '@views/apps/ecommerce/products/add/ProductInformation' +import ProductImage from '@views/apps/ecommerce/products/add/ProductImage' +import ProductVariants from '@views/apps/ecommerce/products/add/ProductVariants' +import ProductInventory from '@views/apps/ecommerce/products/add/ProductInventory' +import ProductPricing from '@views/apps/ecommerce/products/add/ProductPricing' +import ProductOrganize from '@views/apps/ecommerce/products/add/ProductOrganize' + +const eCommerceProductsEdit = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default eCommerceProductsEdit diff --git a/src/hocs/AuthGuard.tsx b/src/hocs/AuthGuard.tsx index 336908c..2d65722 100644 --- a/src/hocs/AuthGuard.tsx +++ b/src/hocs/AuthGuard.tsx @@ -14,8 +14,6 @@ import { getLocalizedUrl } from '../utils/i18n' export default function AuthGuard({ children, locale }: ChildrenType & { locale: Locale }) { const { isAuthenticated } = useAuth() - console.log('isAuthenticated', isAuthenticated) - useEffect(() => { if (!isAuthenticated) { redirect(getLocalizedUrl('/login', locale)) diff --git a/src/redux-store/index.ts b/src/redux-store/index.ts index 0f9fffd..49d3695 100644 --- a/src/redux-store/index.ts +++ b/src/redux-store/index.ts @@ -6,13 +6,15 @@ import chatReducer from '@/redux-store/slices/chat' import calendarReducer from '@/redux-store/slices/calendar' import kanbanReducer from '@/redux-store/slices/kanban' import emailReducer from '@/redux-store/slices/email' +import productReducer from '@/redux-store/slices/product' export const store = configureStore({ reducer: { chatReducer, calendarReducer, kanbanReducer, - emailReducer + emailReducer, + productReducer }, middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false }) }) diff --git a/src/redux-store/slices/product.ts b/src/redux-store/slices/product.ts new file mode 100644 index 0000000..9b2ec59 --- /dev/null +++ b/src/redux-store/slices/product.ts @@ -0,0 +1,47 @@ +// Third-party Imports +import type { Draft, PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' + +// Type Imports + +// Data Imports +import { ProductRequest } from '../../types/services/product' + +const initialState: { productRequest: ProductRequest } = { + productRequest: { + category_id: '', + sku: '', + name: '', + description: '', + barcode: '', + price: 0, + cost: 0, + printer_type: '', + image_url: '', + variants: [] + } +} + +export const productSlice = createSlice({ + name: 'product', + initialState, + reducers: { + setProductField: ( + state: Draft<{ productRequest: ProductRequest }>, + action: PayloadAction<{ field: K; value: ProductRequest[K] }> + ) => { + const { field, value } = action.payload + state.productRequest[field] = value + }, + setProduct: (state, action: PayloadAction) => { + state.productRequest = action.payload + }, + resetProduct: state => { + state.productRequest = initialState.productRequest + } + } +}) + +export const { setProductField, setProduct, resetProduct } = productSlice.actions + +export default productSlice.reducer diff --git a/src/services/mutations/files.ts b/src/services/mutations/files.ts new file mode 100644 index 0000000..2fcece1 --- /dev/null +++ b/src/services/mutations/files.ts @@ -0,0 +1,25 @@ +import { useMutation } from '@tanstack/react-query' +import { api } from '../api' +import { toast } from 'react-toastify' + +export const useFilesMutation = { + uploadFile: () => { + return useMutation({ + mutationFn: async (newFile: FormData) => { + const response = await api.post('/files/upload', newFile, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + + return response.data.data + }, + onSuccess: data => { + toast.success('File uploaded successfully!') + }, + onError: (error: any) => { + toast.error(error.response.data.errors[0].cause) + } + }) + } +} diff --git a/src/services/mutations/products.ts b/src/services/mutations/products.ts new file mode 100644 index 0000000..a28d464 --- /dev/null +++ b/src/services/mutations/products.ts @@ -0,0 +1,21 @@ +import { useMutation } from '@tanstack/react-query' +import { api } from '../api' +import { toast } from 'react-toastify' +import { ProductRequest } from '../../types/services/product' + +export const useProductsMutation = { + createProduct: () => { + return useMutation({ + mutationFn: async (newProduct: ProductRequest) => { + const response = await api.post('/products', newProduct) + return response.data + }, + onSuccess: data => { + toast.success('Product created successfully!') + }, + onError: (error: any) => { + toast.error(error.response.data.errors[0].cause) + } + }) + } +} diff --git a/src/types/services/product.ts b/src/types/services/product.ts index 901a144..bb7002d 100644 --- a/src/types/services/product.ts +++ b/src/types/services/product.ts @@ -35,3 +35,22 @@ export type Products = { limit: number total_pages: number } + +export type ProductVariantRequest = { + name: string + price_modifier: number + cost: number +} + +export type ProductRequest = { + category_id: string + sku: string + name: string + description: string + barcode: string + price: number + cost: number + printer_type: string + image_url: string + variants: ProductVariantRequest[] +} diff --git a/src/views/apps/academy/dashboard/CourseTable.tsx b/src/views/apps/academy/dashboard/CourseTable.tsx index 3287b79..85a9524 100644 --- a/src/views/apps/academy/dashboard/CourseTable.tsx +++ b/src/views/apps/academy/dashboard/CourseTable.tsx @@ -329,7 +329,16 @@ const CourseTable = ({ courseData }: { courseData?: Course[] }) => { } + component={() => ( + { + table.setPageIndex(page - 1) + }} + /> + )} count={table.getFilteredRowModel().rows.length} rowsPerPage={table.getState().pagination.pageSize} page={table.getState().pagination.pageIndex} diff --git a/src/views/apps/ecommerce/products/add/ProductAddHeader.tsx b/src/views/apps/ecommerce/products/add/ProductAddHeader.tsx index 7bfcba6..eac9907 100644 --- a/src/views/apps/ecommerce/products/add/ProductAddHeader.tsx +++ b/src/views/apps/ecommerce/products/add/ProductAddHeader.tsx @@ -1,8 +1,30 @@ +'use client' + // MUI Imports import Button from '@mui/material/Button' import Typography from '@mui/material/Typography' +import { useProductsMutation } from '../../../../../services/mutations/products' +import { useDispatch, useSelector } from 'react-redux' +import { RootState } from '../../../../../redux-store' +import { CircularProgress } from '@mui/material' +import { resetProduct } from '../../../../../redux-store/slices/product' const ProductAddHeader = () => { + const dispatch = useDispatch() + const { mutate, isPending } = useProductsMutation.createProduct() + const { productRequest } = useSelector((state: RootState) => state.productReducer) + + const handleSubmit = () => { + const { cost, price, ...rest } = productRequest + const newProductRequest = { ...rest, cost: Number(cost), price: Number(price) } + + mutate(newProductRequest, { + onSuccess: () => { + dispatch(resetProduct()) + } + }) + } + return (
@@ -16,7 +38,9 @@ const ProductAddHeader = () => { Discard - +
) diff --git a/src/views/apps/ecommerce/products/add/ProductImage.tsx b/src/views/apps/ecommerce/products/add/ProductImage.tsx index e7b34ab..e675f55 100644 --- a/src/views/apps/ecommerce/products/add/ProductImage.tsx +++ b/src/views/apps/ecommerce/products/add/ProductImage.tsx @@ -4,16 +4,16 @@ import { useState } from 'react' // MUI Imports -import Card from '@mui/material/Card' -import CardHeader from '@mui/material/CardHeader' -import CardContent from '@mui/material/CardContent' +import type { BoxProps } from '@mui/material/Box' import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import CardHeader from '@mui/material/CardHeader' import IconButton from '@mui/material/IconButton' import List from '@mui/material/List' import ListItem from '@mui/material/ListItem' import Typography from '@mui/material/Typography' import { styled } from '@mui/material/styles' -import type { BoxProps } from '@mui/material/Box' // Third-party Imports import { useDropzone } from 'react-dropzone' @@ -24,6 +24,9 @@ import CustomAvatar from '@core/components/mui/Avatar' // Styled Component Imports import AppReactDropzone from '@/libs/styles/AppReactDropzone' +import { useDispatch } from 'react-redux' +import { useFilesMutation } from '../../../../../services/mutations/files' +import { setProductField } from '../../../../../redux-store/slices/product' type FileProp = { name: string @@ -46,9 +49,27 @@ const Dropzone = styled(AppReactDropzone)(({ theme }) => ({ })) const ProductImage = () => { + const dispatch = useDispatch() + const { mutate, isPending } = useFilesMutation.uploadFile() + // States const [files, setFiles] = useState([]) + const handleUpload = () => { + if (!files.length) return + + const formData = new FormData() + formData.append('file', files[0]) + formData.append('file_type', 'image') + formData.append('description', 'Product image') + + mutate(formData, { + onSuccess: data => { + dispatch(setProductField({ field: 'image_url', value: data.file_url })) + } + }) + } + // Hooks const { getRootProps, getInputProps } = useDropzone({ onDrop: (acceptedFiles: File[]) => { @@ -129,7 +150,9 @@ const ProductImage = () => { - + ) : null} diff --git a/src/views/apps/ecommerce/products/add/ProductInformation.tsx b/src/views/apps/ecommerce/products/add/ProductInformation.tsx index ea2835e..6b04d7e 100644 --- a/src/views/apps/ecommerce/products/add/ProductInformation.tsx +++ b/src/views/apps/ecommerce/products/add/ProductInformation.tsx @@ -24,6 +24,11 @@ import CustomTextField from '@core/components/mui/TextField' // Style Imports import '@/libs/styles/tiptapEditor.css' +import { useDispatch, useSelector } from 'react-redux' +import { RootState } from '../../../../../redux-store' +import { setProductField } from '@/redux-store/slices/product' +import { useEffect } from 'react' + const EditorToolbar = ({ editor }: { editor: Editor | null }) => { if (!editor) { return null @@ -114,6 +119,13 @@ const EditorToolbar = ({ editor }: { editor: Editor | null }) => { } const ProductInformation = () => { + const dispatch = useDispatch() + const { name, sku, barcode, description } = useSelector((state: RootState) => state.productReducer.productRequest) + + const handleInputChange = (field: any, value: any) => { + dispatch(setProductField({ field, value })) + } + const editor = useEditor({ extensions: [ StarterKit, @@ -128,24 +140,57 @@ const ProductInformation = () => { immediatelyRender: false, content: `

- Keep your account secure with authentication step. + ${description}

` }) + useEffect(() => { + if (!editor) return + + const updateListener = () => { + const html = editor.getHTML() + dispatch(setProductField({ field: 'description', value: html })) + } + + editor.on('update', updateListener) + + return () => { + editor.off('update', updateListener) + } + }, [editor]) + return ( - + handleInputChange('name', e.target.value)} + /> - + handleInputChange('sku', e.target.value)} + /> - + handleInputChange('barcode', e.target.value)} + /> Description (Optional) diff --git a/src/views/apps/ecommerce/products/add/ProductOrganize.tsx b/src/views/apps/ecommerce/products/add/ProductOrganize.tsx index 9bbd143..23ba204 100644 --- a/src/views/apps/ecommerce/products/add/ProductOrganize.tsx +++ b/src/views/apps/ecommerce/products/add/ProductOrganize.tsx @@ -1,48 +1,56 @@ 'use client' // React Imports -import { useState } from 'react' // MUI Imports import Card from '@mui/material/Card' -import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' +import CardHeader from '@mui/material/CardHeader' import MenuItem from '@mui/material/MenuItem' // Component Imports import CustomIconButton from '@core/components/mui/IconButton' import CustomTextField from '@core/components/mui/TextField' +import { useDispatch, useSelector } from 'react-redux' +import { RootState } from '../../../../../redux-store' +import { setProductField } from '../../../../../redux-store/slices/product' +import { useCategoriesQuery } from '../../../../../services/queries/categories' +import { Category } from '../../../../../types/services/category' const ProductOrganize = () => { - // States - const [vendor, setVendor] = useState('') - const [category, setCategory] = useState('') - const [collection, setCollection] = useState('') - const [status, setStatus] = useState('') + const dispatch = useDispatch() + const { category_id, printer_type } = useSelector((state: RootState) => state.productReducer.productRequest) + + const { data: categoriesApi } = useCategoriesQuery.getCategories() + + const handleSelectChange = (field: any, value: any) => { + dispatch(setProductField({ field, value })) + } return (
e.preventDefault()} className='flex flex-col gap-6'> - setVendor(e.target.value)}> - Men's Clothing - Women's Clothing - Kid's Clothing -
setCategory(e.target.value)} + value={category_id} + onChange={e => handleSelectChange('category_id', e.target.value)} > - Household - Office - Electronics - Management - Automotive + {categoriesApi?.categories.length ? ( + categoriesApi?.categories.map((item: Category, index: number) => ( + + {item.name} + + )) + ) : ( + + Loading categories... + + )} @@ -51,20 +59,18 @@ const ProductOrganize = () => { setCollection(e.target.value)} + label='Printer Type' + value={printer_type} + onChange={e => handleSelectChange('printer_type', e.target.value)} > - Men's Clothing - Women's Clothing - Kid's Clothing + Kitchen - setStatus(e.target.value)}> + {/* setStatus(e.target.value)}> Published Inactive Scheduled - + */} diff --git a/src/views/apps/ecommerce/products/add/ProductPricing.tsx b/src/views/apps/ecommerce/products/add/ProductPricing.tsx index c63a5ef..0376726 100644 --- a/src/views/apps/ecommerce/products/add/ProductPricing.tsx +++ b/src/views/apps/ecommerce/products/add/ProductPricing.tsx @@ -1,3 +1,5 @@ +'use client' + // MUI Imports import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' @@ -11,15 +13,39 @@ import Typography from '@mui/material/Typography' // Component Imports import Form from '@components/Form' import CustomTextField from '@core/components/mui/TextField' +import { useDispatch, useSelector } from 'react-redux' +import { RootState } from '../../../../../redux-store' +import { setProductField } from '../../../../../redux-store/slices/product' const ProductPricing = () => { + const { price, cost } = useSelector((state: RootState) => state.productReducer.productRequest) + const dispatch = useDispatch() + + const handleInputChange = (field: any, value: any) => { + dispatch(setProductField({ field, value })) + } + return (
- - + handleInputChange('price', e.target.value)} + /> + handleInputChange('cost', e.target.value)} + /> } label='Charge tax on this product' />
diff --git a/src/views/apps/ecommerce/products/add/ProductVariants.tsx b/src/views/apps/ecommerce/products/add/ProductVariants.tsx index 9fdcefc..b48e08a 100644 --- a/src/views/apps/ecommerce/products/add/ProductVariants.tsx +++ b/src/views/apps/ecommerce/products/add/ProductVariants.tsx @@ -1,30 +1,42 @@ 'use client' // React Imports -import { useState } from 'react' -import type { SyntheticEvent } from 'react' // MUI Imports -import Grid from '@mui/material/Grid2' -import Card from '@mui/material/Card' -import CardHeader from '@mui/material/CardHeader' -import CardContent from '@mui/material/CardContent' import Button from '@mui/material/Button' -import MenuItem from '@mui/material/MenuItem' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import CardHeader from '@mui/material/CardHeader' +import Grid from '@mui/material/Grid2' // Components Imports import CustomIconButton from '@core/components/mui/IconButton' import CustomTextField from '@core/components/mui/TextField' +import { useDispatch, useSelector } from 'react-redux' +import { RootState } from '../../../../../redux-store' +import { setProductField } from '../../../../../redux-store/slices/product' const ProductVariants = () => { - // States - const [count, setCount] = useState(1) + const dispatch = useDispatch() + const { variants } = useSelector((state: RootState) => state.productReducer.productRequest) - const deleteForm = (e: SyntheticEvent) => { - e.preventDefault() + const handleAddVariant = () => { + dispatch(setProductField({ field: 'variants', value: [...variants, { name: '', cost: 0, price_modifier: 0 }] })) + } - // @ts-ignore - e.target.closest('.repeater-item').remove() + const handleRemoveVariant = (index: number) => { + const updated = variants.filter((_, i) => i !== index) + dispatch(setProductField({ field: 'variants', value: updated })) + } + + const handleInputChange = (index: number, e: any) => { + const { name, value } = e.target + const updated = [...variants] + updated[index] = { + ...updated[index], + [name]: name === 'name' ? value : Number(value) + } + dispatch(setProductField({ field: 'variants', value: updated })) } return ( @@ -32,21 +44,40 @@ const ProductVariants = () => { - {Array.from(Array(count).keys()).map((item, index) => ( + {variants.map((variant, index) => ( - - Size - Color - Weight - Smell - + handleInputChange(index, e)} + /> - -
- - + + handleInputChange(index, e)} + /> + + +
+ handleInputChange(index, e)} + /> + handleRemoveVariant(index)} className='min-is-fit'>
@@ -55,7 +86,7 @@ const ProductVariants = () => {
))} -