From 5f2bddd003516988ff3d2e421793f988fabefab0 Mon Sep 17 00:00:00 2001 From: ferdiansyah783 Date: Wed, 6 Aug 2025 13:18:19 +0700 Subject: [PATCH] feat: unis and product detail --- .../ecommerce/products/[id]/detail/page.tsx | 10 + .../ecommerce/products/ingredients/page.tsx | 8 + .../apps/ecommerce/products/units/page.tsx | 8 + .../layout/vertical/VerticalMenu.tsx | 10 +- src/data/dictionaries/ar.json | 2 + src/data/dictionaries/en.json | 2 + src/data/dictionaries/fr.json | 2 + src/services/mutations/units.ts | 60 +++ src/services/queries/units.ts | 39 ++ src/types/services/unit.ts | 29 ++ .../ecommerce/products/add/ProductImage.tsx | 52 ++- .../products/add/ProductVariants.tsx | 2 +- .../products/detail/ProductDetail.tsx | 333 +++++++++++++++ .../products/list/ProductListTable.tsx | 6 + .../products/units/AddUnitDrawer.tsx | 167 ++++++++ .../products/units/EditUnitDrawer.tsx | 180 ++++++++ .../products/units/ProductUnitTable.tsx | 391 ++++++++++++++++++ .../apps/stock/adjustment/StockListTable.tsx | 20 - 18 files changed, 1294 insertions(+), 27 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/[id]/detail/page.tsx create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/ingredients/page.tsx create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/units/page.tsx create mode 100644 src/services/mutations/units.ts create mode 100644 src/services/queries/units.ts create mode 100644 src/types/services/unit.ts create mode 100644 src/views/apps/ecommerce/products/detail/ProductDetail.tsx create mode 100644 src/views/apps/ecommerce/products/units/AddUnitDrawer.tsx create mode 100644 src/views/apps/ecommerce/products/units/EditUnitDrawer.tsx create mode 100644 src/views/apps/ecommerce/products/units/ProductUnitTable.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/[id]/detail/page.tsx new file mode 100644 index 0000000..0335800 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/[id]/detail/page.tsx @@ -0,0 +1,10 @@ +import ProductDetail from "../../../../../../../../../views/apps/ecommerce/products/detail/ProductDetail" + +// In your page or component +const productData = { + // Your product object here +} + +export default function ProductPage() { + return +} diff --git a/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/ingredients/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/ingredients/page.tsx new file mode 100644 index 0000000..19d4fe2 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/ingredients/page.tsx @@ -0,0 +1,8 @@ +// Component Imports +import ProductUnitTable from '../../../../../../../../views/apps/ecommerce/products/units/ProductUnitTable' + +const eCommerceProductsIngredient = () => { + return +} + +export default eCommerceProductsIngredient diff --git a/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/units/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/units/page.tsx new file mode 100644 index 0000000..589a7ca --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/ecommerce/products/units/page.tsx @@ -0,0 +1,8 @@ +// Component Imports +import ProductUnitTable from '../../../../../../../../views/apps/ecommerce/products/units/ProductUnitTable' + +const eCommerceProductsUnit = () => { + return +} + +export default eCommerceProductsUnit diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 34e8b07..95182d5 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -118,6 +118,12 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].category} + + {dictionary['navigation'].units} + + + {dictionary['navigation'].ingredients} + {dictionary['navigation'].list} @@ -139,10 +145,10 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].details} - + {/* {dictionary['navigation'].manageReviews} - {dictionary['navigation'].referrals} + {dictionary['navigation'].referrals} */} {dictionary['navigation'].settings} }> diff --git a/src/data/dictionaries/ar.json b/src/data/dictionaries/ar.json index 556938f..c0843cc 100644 --- a/src/data/dictionaries/ar.json +++ b/src/data/dictionaries/ar.json @@ -21,6 +21,8 @@ "add": "يضيف", "addjustment": "تعديل", "category": "فئة", + "units": "وحدات", + "ingredients": "مكونات", "orders": "أوامر", "details": "تفاصيل", "customers": "العملاء", diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index f11d7ea..09c7e45 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -21,6 +21,8 @@ "add": "Add", "addjustment": "Addjustment", "category": "Category", + "units": "Units", + "ingredients": "Ingredients", "orders": "Orders", "details": "Details", "customers": "Customers", diff --git a/src/data/dictionaries/fr.json b/src/data/dictionaries/fr.json index 5e6a580..9753464 100644 --- a/src/data/dictionaries/fr.json +++ b/src/data/dictionaries/fr.json @@ -21,6 +21,8 @@ "add": "Ajouter", "addjustment": "Ajustement", "category": "Catégorie", + "units": "Unites", + "ingredients": "Ingrédients", "orders": "Ordres", "details": "Détails", "customers": "Clientes", diff --git a/src/services/mutations/units.ts b/src/services/mutations/units.ts new file mode 100644 index 0000000..3660448 --- /dev/null +++ b/src/services/mutations/units.ts @@ -0,0 +1,60 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-toastify' +import { UnitRequest } from '../../types/services/unit' +import { api } from '../api' + +export const useUnitsMutation = { + createUnit: () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (newUnit: UnitRequest) => { + const response = await api.post('/units', newUnit) + return response.data + }, + onSuccess: () => { + toast.success('Unit created successfully!') + queryClient.invalidateQueries({ queryKey: ['units'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed') + } + }) + }, + + updateUnit: () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ id, payload }: { id: string; payload: UnitRequest }) => { + const response = await api.put(`/units/${id}`, payload) + return response.data + }, + onSuccess: () => { + toast.success('Unit updated successfully!') + queryClient.invalidateQueries({ queryKey: ['units'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed') + } + }) + }, + + deleteUnit: () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (id: string) => { + const response = await api.delete(`/units/${id}`) + return response.data + }, + onSuccess: () => { + toast.success('Unit deleted successfully!') + queryClient.invalidateQueries({ queryKey: ['units'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed') + } + }) + } +} diff --git a/src/services/queries/units.ts b/src/services/queries/units.ts new file mode 100644 index 0000000..2f218e6 --- /dev/null +++ b/src/services/queries/units.ts @@ -0,0 +1,39 @@ +import { useQuery } from '@tanstack/react-query' +import { Units } from '../../types/services/unit' +import { api } from '../api' + +interface UnitsQueryParams { + page?: number + limit?: number + search?: string +} + +export const useUnitsQuery = { + getUnits: (params: UnitsQueryParams = {}) => { + const { page = 1, limit = 10, search = '', ...filters } = params + + return useQuery({ + queryKey: ['units', { page, limit, search, ...filters }], + queryFn: async () => { + const queryParams = new URLSearchParams() + + queryParams.append('page', page.toString()) + queryParams.append('limit', limit.toString()) + + if (search) { + queryParams.append('search', search) + } + + // Add other filters + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + queryParams.append(key, value.toString()) + } + }) + + const res = await api.get(`/units?${queryParams.toString()}`) + return res.data.data + }, + }) + } +} diff --git a/src/types/services/unit.ts b/src/types/services/unit.ts new file mode 100644 index 0000000..c115f73 --- /dev/null +++ b/src/types/services/unit.ts @@ -0,0 +1,29 @@ +export interface Unit { + id: string + organization_id: string + outlet_id: string | null + name: string + abbreviation: string | null + is_active: boolean + created_at: string + updated_at: string +} + +export interface PaginationMeta { + page: number + limit: number + total_count: number + total_pages: number +} + +export interface Units { + data: Unit[] + pagination: PaginationMeta +} + +export interface UnitRequest { + name: string + abbreviation: string | null + outlet_id: string | null + is_active: boolean +} diff --git a/src/views/apps/ecommerce/products/add/ProductImage.tsx b/src/views/apps/ecommerce/products/add/ProductImage.tsx index e675f55..6f25717 100644 --- a/src/views/apps/ecommerce/products/add/ProductImage.tsx +++ b/src/views/apps/ecommerce/products/add/ProductImage.tsx @@ -1,7 +1,7 @@ 'use client' // React Imports -import { useState } from 'react' +import { useEffect, useState } from 'react' // MUI Imports import type { BoxProps } from '@mui/material/Box' @@ -24,9 +24,10 @@ import CustomAvatar from '@core/components/mui/Avatar' // Styled Component Imports import AppReactDropzone from '@/libs/styles/AppReactDropzone' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useFilesMutation } from '../../../../../services/mutations/files' import { setProductField } from '../../../../../redux-store/slices/product' +import { RootState } from '../../../../../redux-store' type FileProp = { name: string @@ -52,9 +53,13 @@ const ProductImage = () => { const dispatch = useDispatch() const { mutate, isPending } = useFilesMutation.uploadFile() + const { image_url } = useSelector((state: RootState) => state.productReducer.productRequest) + // States const [files, setFiles] = useState([]) + console.log(files) + const handleUpload = () => { if (!files.length) return @@ -66,6 +71,8 @@ const ProductImage = () => { mutate(formData, { onSuccess: data => { dispatch(setProductField({ field: 'image_url', value: data.file_url })) + // Clear the local files after successful upload + setFiles([]) } }) } @@ -73,7 +80,8 @@ const ProductImage = () => { // Hooks const { getRootProps, getInputProps } = useDropzone({ onDrop: (acceptedFiles: File[]) => { - setFiles(acceptedFiles.map((file: File) => Object.assign(file))) + // Replace files instead of adding to them + setFiles([acceptedFiles[0]]) // Only take the first file } }) @@ -92,6 +100,10 @@ const ProductImage = () => { setFiles([...filtered]) } + const handleRemoveCurrentImage = () => { + dispatch(setProductField({ field: 'image_url', value: '' })) + } + const fileList = files.map((file: FileProp) => (
@@ -136,13 +148,45 @@ const ProductImage = () => { - Drag and Drop Your Image Here. + + {image_url && !files.length ? 'Drop New Image to Replace' : 'Drag and Drop Your Image Here.'} + or
+ {/* Show current image if it exists */} + {image_url && !files.length && ( +
+ + Current Image: + +
+
+ Current product image +
+ + Current product image + + + Uploaded image + +
+
+ + + +
+
+ )} {files.length ? ( <> {fileList} diff --git a/src/views/apps/ecommerce/products/add/ProductVariants.tsx b/src/views/apps/ecommerce/products/add/ProductVariants.tsx index b48e08a..dfa6995 100644 --- a/src/views/apps/ecommerce/products/add/ProductVariants.tsx +++ b/src/views/apps/ecommerce/products/add/ProductVariants.tsx @@ -44,7 +44,7 @@ const ProductVariants = () => { - {variants.map((variant, index) => ( + {variants && variants.map((variant, index) => ( diff --git a/src/views/apps/ecommerce/products/detail/ProductDetail.tsx b/src/views/apps/ecommerce/products/detail/ProductDetail.tsx new file mode 100644 index 0000000..f71aec9 --- /dev/null +++ b/src/views/apps/ecommerce/products/detail/ProductDetail.tsx @@ -0,0 +1,333 @@ +'use client' + +import { + Avatar, + Badge, + Card, + CardContent, + CardMedia, + Chip, + Divider, + Grid, + List, + ListItem, + ListItemIcon, + ListItemText, + Typography +} from '@mui/material' +import { useParams } from 'next/navigation' +import React, { useEffect } from 'react' +import { useDispatch } from 'react-redux' +import Loading from '../../../../../components/layout/shared/Loading' +import { setProduct } from '../../../../../redux-store/slices/product' +import { useProductsQuery } from '../../../../../services/queries/products' +import { ProductVariant } from '../../../../../types/services/product' +// Tabler icons (using class names) +const TablerIcon = ({ name, className = '' }: { name: string; className?: string }) => ( + +) + +const ProductDetail = () => { + const dispatch = useDispatch() + const params = useParams() + + const { data: product, isLoading, error } = useProductsQuery.getProductById(params?.id as string) + + useEffect(() => { + if (product) { + dispatch(setProduct(product)) + } + }, [product, dispatch]) + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + const getBusinessTypeColor = (type: string) => { + switch (type.toLowerCase()) { + case 'restaurant': + return 'primary' + case 'retail': + return 'secondary' + case 'cafe': + return 'info' + default: + return 'default' + } + } + + const getPrinterTypeColor = (type: string) => { + switch (type.toLowerCase()) { + case 'kitchen': + return 'warning' + case 'bar': + return 'info' + case 'receipt': + return 'success' + default: + return 'default' + } + } + + if (isLoading) return + + return ( +
+ {/* Header Card */} + + + + + + + +
+
+
+ + {product.name} + +
+ } + label={product.sku} + size='small' + variant='outlined' + /> + } + label={product.is_active ? 'Active' : 'Inactive'} + color={product.is_active ? 'success' : 'error'} + size='small' + /> +
+
+
+ + {product.description && ( + + {product.description} + + )} + +
+
+ +
+ + Price + + + {formatCurrency(product.price)} + +
+
+
+ +
+ + Cost + + + {formatCurrency(product.cost)} + +
+
+
+ +
+ } + label={product.business_type} + color={getBusinessTypeColor(product.business_type)} + size='small' + /> + } + label={product.printer_type} + color={getPrinterTypeColor(product.printer_type)} + size='small' + /> +
+
+
+
+
+
+ + + {/* Product Information */} + + + + + + Product Information + +
+
+ + Product ID + + + {product.id} + +
+
+ + Category ID + + + {product.category_id} + +
+
+ + Organization ID + + + {product.organization_id} + +
+
+ + Profit Margin + + + {formatCurrency(product.price - product.cost)} + + ({(((product.price - product.cost) / product.cost) * 100).toFixed(1)}%) + + +
+
+
+
+ + {/* Variants Section */} + {product.variants && product.variants.length > 0 && ( + + + + + Product Variants + + + + {product.variants.map((variant: ProductVariant, index: number) => ( + + + + + {variant.name.charAt(0)} + + + + + {variant.name} + +
+ + +{formatCurrency(variant.price_modifier)} + + + Cost: {formatCurrency(variant.cost)} + +
+
+ } + secondary={ + + Total Price: {formatCurrency(product.price + variant.price_modifier)} + + } + /> +
+ {index < product.variants.length - 1 && } + + ))} + + + + )} + + + {/* Metadata & Timestamps */} + + + + + + Timestamps + +
+
+ + Created + + + {formatDate(product.created_at)} + +
+ +
+ + Last Updated + + + {formatDate(product.updated_at)} + +
+
+ + {Object.keys(product.metadata).length > 0 && ( + <> + + + Metadata + +
+ {Object.entries(product.metadata).map(([key, value]) => ( +
+ + {key.replace(/_/g, ' ')} + + + {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} + +
+ ))} +
+ + )} +
+
+
+ + + ) +} + +export default ProductDetail diff --git a/src/views/apps/ecommerce/products/list/ProductListTable.tsx b/src/views/apps/ecommerce/products/list/ProductListTable.tsx index 1152afa..57ded73 100644 --- a/src/views/apps/ecommerce/products/list/ProductListTable.tsx +++ b/src/views/apps/ecommerce/products/list/ProductListTable.tsx @@ -225,6 +225,12 @@ const ProductListTable = () => { header: 'Actions', cell: ({ row }) => (
+ + + void +} + +const AddUnitDrawer = (props: Props) => { + // Props + const { open, handleClose } = props + + // States + const [formData, setFormData] = useState({ + name: '', + abbreviation: '', + outlet_id: '', + is_active: true + }) + const [status, setStatus] = useState('') + const [outletInput, setOutletInput] = useState('') + const [outletDebouncedInput] = useDebounce(outletInput, 500) + + const { data: outlets, isLoading: outletsLoading } = useOutletsQuery.getOutlets({ + search: outletDebouncedInput + }) + const { mutate: createUnit, isPending: isCreating } = useUnitsMutation.createUnit() + + const outletOptions = useMemo(() => outlets?.outlets || [], [outlets]) + + // Handle Form Submit + const handleFormSubmit = (e: any) => { + e.preventDefault() + + createUnit( + { ...formData, is_active: status === 'active' }, + { + onSuccess: () => { + handleReset() + } + } + ) + } + + const handleInputChange = (e: any) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + // Handle Form Reset + const handleReset = () => { + handleClose() + setFormData({ + name: '', + abbreviation: '', + outlet_id: '', + is_active: true + }) + } + + return ( + +
+ Add Unit + + + +
+ +
+
+ + + option.name} + value={outletOptions.find(p => p.id === formData.outlet_id) || null} + onInputChange={(event, newOutlettInput) => { + setOutletInput(newOutlettInput) + }} + onChange={(event, newValue) => { + setFormData({ + ...formData, + outlet_id: newValue?.id || '' + }) + }} + renderInput={params => ( + + {outletsLoading && } + {params.InputProps.endAdornment} + + ) + }} + /> + )} + /> + setStatus(e.target.value)}> + Active + Inactive + +
+ + +
+ +
+
+ ) +} + +export default AddUnitDrawer diff --git a/src/views/apps/ecommerce/products/units/EditUnitDrawer.tsx b/src/views/apps/ecommerce/products/units/EditUnitDrawer.tsx new file mode 100644 index 0000000..a5a78b0 --- /dev/null +++ b/src/views/apps/ecommerce/products/units/EditUnitDrawer.tsx @@ -0,0 +1,180 @@ +// React Imports +import { useEffect, useMemo, useState } from 'react' + +// MUI Imports +import Button from '@mui/material/Button' +import Divider from '@mui/material/Divider' +import Drawer from '@mui/material/Drawer' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import Typography from '@mui/material/Typography' + +// Third-party Imports + +// Type Imports + +// Components Imports +import CustomTextField from '@core/components/mui/TextField' +import { useUnitsMutation } from '../../../../../services/mutations/units' +import { Unit, UnitRequest } from '../../../../../types/services/unit' +import { Autocomplete, CircularProgress } from '@mui/material' +import { useOutletsQuery } from '../../../../../services/queries/outlets' +import { useDebounce } from 'use-debounce' + +type Props = { + open: boolean + handleClose: () => void + data: Unit +} + +const EditUnitDrawer = (props: Props) => { + // Props + const { open, handleClose, data } = props + + // States + const [formData, setFormData] = useState({ + name: '', + abbreviation: '', + outlet_id: '', + is_active: true + }) + const [status, setStatus] = useState('') + const [outletInput, setOutletInput] = useState('') + const [outletDebouncedInput] = useDebounce(outletInput, 500) + + const { data: outlets, isLoading: outletsLoading } = useOutletsQuery.getOutlets({ + search: outletDebouncedInput + }) + + const { mutate: updateUnit, isPending: isCreating } = useUnitsMutation.updateUnit() + const outletOptions = useMemo(() => outlets?.outlets || [], [outlets]) + + useEffect(() => { + if (data) { + setFormData({ + name: data.name, + abbreviation: data.abbreviation!, + outlet_id: data.outlet_id!, + is_active: data.is_active + }) + setStatus(data.is_active ? 'active' : 'inactive') + } + }, [data]) + + // Handle Form Submit + const handleFormSubmit = (e: any) => { + e.preventDefault() + + updateUnit( + { id: data.id, payload: formData }, + { + onSuccess: () => { + handleReset() + } + } + ) + } + + const handleInputChange = (e: any) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + // Handle Form Reset + const handleReset = () => { + handleClose() + setFormData({ + name: '', + abbreviation: '', + outlet_id: '', + is_active: true + }) + } + + return ( + +
+ Edit Unit + + + +
+ +
+
+ + + option.name} + value={outletOptions.find(p => p.id === formData.outlet_id) || null} + onInputChange={(event, newOutlettInput) => { + setOutletInput(newOutlettInput) + }} + onChange={(event, newValue) => { + setFormData({ + ...formData, + outlet_id: newValue?.id || '' + }) + }} + renderInput={params => ( + + {outletsLoading && } + {params.InputProps.endAdornment} + + ) + }} + /> + )} + /> + setStatus(e.target.value)}> + Active + Inactive + +
+ + +
+ +
+
+ ) +} + +export default EditUnitDrawer diff --git a/src/views/apps/ecommerce/products/units/ProductUnitTable.tsx b/src/views/apps/ecommerce/products/units/ProductUnitTable.tsx new file mode 100644 index 0000000..4abc2f7 --- /dev/null +++ b/src/views/apps/ecommerce/products/units/ProductUnitTable.tsx @@ -0,0 +1,391 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import Checkbox from '@mui/material/Checkbox' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import TablePagination from '@mui/material/TablePagination' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Component Imports +import TablePaginationComponent from '@components/TablePaginationComponent' +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress } from '@mui/material' +import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete' +import Loading from '../../../../../components/layout/shared/Loading' +import { useUnitsMutation } from '../../../../../services/mutations/units' +import { useUnitsQuery } from '../../../../../services/queries/units' +import { Unit } from '../../../../../types/services/unit' +import AddUnitDrawer from './AddUnitDrawer' +import EditUnitDrawer from './EditUnitDrawer' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type UnitWithActionsType = Unit & { + actions?: string +} + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const ProductUnitTable = () => { + // States + const [addUnitOpen, setAddUnitOpen] = useState(false) + const [editUnitOpen, setEditUnitOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [unitId, setUnitId] = useState('') + const [openConfirm, setOpenConfirm] = useState(false) + const [currentUnit, setCurrentUnit] = useState() + + // Fetch products with pagination and search + const { data, isLoading, error, isFetching } = useUnitsQuery.getUnits({ + page: currentPage, + limit: pageSize + }) + + const { mutate: deleteUnit, isPending: isDeleting } = useUnitsMutation.deleteUnit() + + const units = data?.data ?? [] + const totalCount = data?.pagination.total_count ?? 0 + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + // Handle page size change + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) // Reset to first page + }, []) + + const handleDelete = () => { + deleteUnit(unitId, { + onSuccess: () => setOpenConfirm(false) + }) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('name', { + header: 'Name', + cell: ({ row }) => ( +
+ {/* */} +
+ + {row.original.name || '-'} + +
+
+ ) + }), + columnHelper.accessor('abbreviation', { + header: 'Abbreviation', + cell: ({ row }) => {row.original.abbreviation || '-'} + }), + columnHelper.accessor('is_active', { + header: 'Status', + cell: ({ row }) => {row.original.is_active ? 'Active' : 'Inactive'} + }), + columnHelper.accessor('created_at', { + header: 'Created Date', + cell: ({ row }) => {row.original.created_at} + }), + columnHelper.accessor('actions', { + header: 'Actions', + cell: ({ row }) => ( +
+ { + setCurrentUnit(row.original) + setEditUnitOpen(!editUnitOpen) + }} + > + + + { + setUnitId(row.original.id) + setOpenConfirm(true) + } + } + }, + { text: 'Duplicate', icon: 'tabler-copy' } + ]} + /> +
+ ), + enableSorting: false + }) + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [data] + ) + + const table = useReactTable({ + data: units as Unit[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, // <= penting! + pageSize + } + }, + enableRowSelection: true, //enable row selection for all rows + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + // Disable client-side pagination since we're handling it server-side + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + +
+ console.log(value)} + placeholder='Search Product' + className='max-sm:is-full' + /> +
+ table.setPageSize(Number(e.target.value))} + className='flex-auto max-sm:is-full sm:is-[70px]' + > + 10 + 15 + 25 + + +
+
+
+ {isLoading ? ( + + ) : ( + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {table.getFilteredRowModel().rows.length === 0 ? ( + + + + + + ) : ( + + {table + .getRowModel() + .rows.slice(0, table.getState().pagination.pageSize) + .map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ No data available +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ )} + + {isFetching && !isLoading && ( + + + + )} +
+ ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + disabled={isLoading} + /> +
+ + setAddUnitOpen(!addUnitOpen)} /> + + setEditUnitOpen(!editUnitOpen)} + data={currentUnit!} + /> + + setOpenConfirm(false)} + onConfirm={handleDelete} + isLoading={isDeleting} + title='Delete Unit' + message='Are you sure you want to delete this Unit? This action cannot be undone.' + /> + + ) +} + +export default ProductUnitTable diff --git a/src/views/apps/stock/adjustment/StockListTable.tsx b/src/views/apps/stock/adjustment/StockListTable.tsx index 1aa9cb1..fbf7fba 100644 --- a/src/views/apps/stock/adjustment/StockListTable.tsx +++ b/src/views/apps/stock/adjustment/StockListTable.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' // Next Imports -import { useParams } from 'next/navigation' // MUI Imports import Button from '@mui/material/Button' @@ -30,14 +29,12 @@ import classnames from 'classnames' // Component Imports import TablePaginationComponent from '@components/TablePaginationComponent' import CustomTextField from '@core/components/mui/TextField' -import OptionMenu from '@core/components/option-menu' // Util Imports // Style Imports import tableStyles from '@core/styles/table.module.css' import { Box, CircularProgress } from '@mui/material' -import ConfirmDeleteDialog from '../../../../components/dialogs/confirm-delete' import Loading from '../../../../components/layout/shared/Loading' import { useInventoriesMutation } from '../../../../services/mutations/inventories' import { useInventoriesQuery } from '../../../../services/queries/inventories' @@ -106,8 +103,6 @@ const StockListTable = () => { const [rowSelection, setRowSelection] = useState({}) const [currentPage, setCurrentPage] = useState(1) const [pageSize, setPageSize] = useState(10) - const [openConfirm, setOpenConfirm] = useState(false) - const [productId, setProductId] = useState('') const [addInventoryOpen, setAddInventoryOpen] = useState(false) // Fetch products with pagination and search @@ -132,12 +127,6 @@ const StockListTable = () => { setCurrentPage(0) // Reset to first page }, []) - const handleDelete = () => { - deleteInventory(productId, { - onSuccess: () => setOpenConfirm(false) - }) - } - const columns = useMemo[]>( () => [ { @@ -376,15 +365,6 @@ const StockListTable = () => { setAddInventoryOpen(!addInventoryOpen)} /> - - setOpenConfirm(false)} - onConfirm={handleDelete} - isLoading={isDeleting} - title='Delete Inventory' - message='Are you sure you want to delete this inventory? This action cannot be undone.' - /> ) }