fix: inventory

This commit is contained in:
ferdiansyah783 2025-08-06 03:57:45 +07:00
parent 799837e82e
commit 0906188c12
37 changed files with 2342 additions and 340 deletions

13
package-lock.json generated
View File

@ -69,6 +69,7 @@
"react-toastify": "10.0.6", "react-toastify": "10.0.6",
"react-use": "17.6.0", "react-use": "17.6.0",
"recharts": "2.15.0", "recharts": "2.15.0",
"use-debounce": "^10.0.5",
"valibot": "0.42.1" "valibot": "0.42.1"
}, },
"devDependencies": { "devDependencies": {
@ -12579,6 +12580,18 @@
} }
} }
}, },
"node_modules/use-debounce": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.5.tgz",
"integrity": "sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==",
"license": "MIT",
"engines": {
"node": ">= 16.0.0"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/use-sidecar": { "node_modules/use-sidecar": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",

View File

@ -74,6 +74,7 @@
"react-toastify": "10.0.6", "react-toastify": "10.0.6",
"react-use": "17.6.0", "react-use": "17.6.0",
"recharts": "2.15.0", "recharts": "2.15.0",
"use-debounce": "^10.0.5",
"valibot": "0.42.1" "valibot": "0.42.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,7 +2,6 @@
import OrderList from '@views/apps/ecommerce/orders/list' import OrderList from '@views/apps/ecommerce/orders/list'
// Data Imports // Data Imports
import { getEcommerceData } from '@/app/server/actions'
/** /**
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the * ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
@ -23,10 +22,8 @@ import { getEcommerceData } from '@/app/server/actions'
} */ } */
const OrdersListPage = async () => { const OrdersListPage = async () => {
// Vars
const data = await getEcommerceData()
return <OrderList orderData={data?.orderData} /> return <OrderList />
} }
export default OrdersListPage export default OrdersListPage

View File

@ -6,7 +6,6 @@ import ProductCard from '@views/apps/ecommerce/products/list/ProductCard'
import ProductListTable from '@views/apps/ecommerce/products/list/ProductListTable' import ProductListTable from '@views/apps/ecommerce/products/list/ProductListTable'
// Data Imports // Data Imports
import { getEcommerceData } from '@/app/server/actions'
/** /**
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the * ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
@ -27,8 +26,6 @@ import { getEcommerceData } from '@/app/server/actions'
} */ } */
const eCommerceProductsList = async () => { const eCommerceProductsList = async () => {
// Vars
const data = await getEcommerceData()
return ( return (
<Grid container spacing={6}> <Grid container spacing={6}>

View File

@ -0,0 +1,30 @@
// MUI Imports
// Component Imports
import StockListTable from '../../../../../../../views/apps/stock/adjustment/StockListTable'
// Data Imports
/**
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
* ! `.env` file found at root of your project and also update the API endpoints like `/apps/ecommerce` in below example.
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
* ! because we've used the server action for getting our static data.
*/
/* const getEcommerceData = async () => {
// Vars
const res = await fetch(`${process.env.API_URL}/apps/ecommerce`)
if (!res.ok) {
throw new Error('Failed to fetch ecommerce data')
}
return res.json()
} */
const StockAdjustment = async () => {
return <StockListTable />
}
export default StockAdjustment

View File

@ -0,0 +1,30 @@
// MUI Imports
// Component Imports
import StockListTable from '../../../../../../../views/apps/stock/list/StockListTable'
// Data Imports
/**
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
* ! `.env` file found at root of your project and also update the API endpoints like `/apps/ecommerce` in below example.
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
* ! because we've used the server action for getting our static data.
*/
/* const getEcommerceData = async () => {
// Vars
const res = await fetch(`${process.env.API_URL}/apps/ecommerce`)
if (!res.ok) {
throw new Error('Failed to fetch ecommerce data')
}
return res.json()
} */
const StockList = async () => {
return <StockListTable />
}
export default StockList

View File

@ -0,0 +1,38 @@
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material'
interface ConfirmDeleteDialogProps {
open: boolean
onClose: () => void
onConfirm: () => void
isLoading?: boolean
title?: string
message?: string
}
const ConfirmDeleteDialog = ({
open,
onClose,
onConfirm,
isLoading,
title = 'Delete Product',
message = 'Are you sure you want to delete this product? This action cannot be undone.'
}: ConfirmDeleteDialogProps) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<DialogContentText>{message}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={isLoading} color='inherit'>
Cancel
</Button>
<Button onClick={onConfirm} disabled={isLoading} color='error' variant='contained'>
{isLoading ? 'Deleting...' : 'Delete'}
</Button>
</DialogActions>
</Dialog>
)
}
export default ConfirmDeleteDialog

View File

@ -145,6 +145,10 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
<MenuItem href={`/${locale}/apps/ecommerce/referrals`}>{dictionary['navigation'].referrals}</MenuItem> <MenuItem href={`/${locale}/apps/ecommerce/referrals`}>{dictionary['navigation'].referrals}</MenuItem>
<MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem> <MenuItem href={`/${locale}/apps/ecommerce/settings`}>{dictionary['navigation'].settings}</MenuItem>
</SubMenu> </SubMenu>
<SubMenu label={dictionary['navigation'].stock} icon={<i className='tabler-basket-down' />}>
<MenuItem href={`/${locale}/apps/stock/list`}>{dictionary['navigation'].list}</MenuItem>
<MenuItem href={`/${locale}/apps/stock/adjustment`}>{dictionary['navigation'].addjustment}</MenuItem>
</SubMenu>
<SubMenu label={dictionary['navigation'].academy} icon={<i className='tabler-school' />}> <SubMenu label={dictionary['navigation'].academy} icon={<i className='tabler-school' />}>
<MenuItem href={`/${locale}/apps/academy/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem> <MenuItem href={`/${locale}/apps/academy/dashboard`}>{dictionary['navigation'].dashboard}</MenuItem>
<MenuItem href={`/${locale}/apps/academy/my-courses`}>{dictionary['navigation'].myCourses}</MenuItem> <MenuItem href={`/${locale}/apps/academy/my-courses`}>{dictionary['navigation'].myCourses}</MenuItem>

View File

@ -4,6 +4,7 @@
"crm": "إدارة علاقات العملاء", "crm": "إدارة علاقات العملاء",
"analytics": "تحليلات", "analytics": "تحليلات",
"eCommerce": "التجارة الإلكترونية", "eCommerce": "التجارة الإلكترونية",
"stock": "المخزون",
"academy": "أكاديمية", "academy": "أكاديمية",
"logistics": "اللوجستية", "logistics": "اللوجستية",
"frontPages": "الصفحات الأولى", "frontPages": "الصفحات الأولى",
@ -18,6 +19,7 @@
"products": "منتجات", "products": "منتجات",
"list": "قائمة", "list": "قائمة",
"add": "يضيف", "add": "يضيف",
"addjustment": "تعديل",
"category": "فئة", "category": "فئة",
"orders": "أوامر", "orders": "أوامر",
"details": "تفاصيل", "details": "تفاصيل",

View File

@ -4,6 +4,7 @@
"crm": "CRM", "crm": "CRM",
"analytics": "Analytics", "analytics": "Analytics",
"eCommerce": "eCommerce", "eCommerce": "eCommerce",
"stock": "Stock",
"academy": "Academy", "academy": "Academy",
"logistics": "Logistics", "logistics": "Logistics",
"frontPages": "Front Pages", "frontPages": "Front Pages",
@ -18,6 +19,7 @@
"products": "Products", "products": "Products",
"list": "List", "list": "List",
"add": "Add", "add": "Add",
"addjustment": "Addjustment",
"category": "Category", "category": "Category",
"orders": "Orders", "orders": "Orders",
"details": "Details", "details": "Details",

View File

@ -4,6 +4,7 @@
"crm": "GRC", "crm": "GRC",
"analytics": "Analytique", "analytics": "Analytique",
"eCommerce": "commerce électronique", "eCommerce": "commerce électronique",
"stock": "Stock",
"academy": "Académie", "academy": "Académie",
"logistics": "Logistique", "logistics": "Logistique",
"frontPages": "Premières pages", "frontPages": "Premières pages",
@ -18,6 +19,7 @@
"products": "Produits", "products": "Produits",
"list": "Liste", "list": "Liste",
"add": "Ajouter", "add": "Ajouter",
"addjustment": "Ajustement",
"category": "Catégorie", "category": "Catégorie",
"orders": "Ordres", "orders": "Ordres",
"details": "Détails", "details": "Détails",

View File

@ -0,0 +1,60 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../api'
import { toast } from 'react-toastify'
import { CategoryRequest } from '../../types/services/category'
export const useCategoriesMutation = {
createCategory: () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newCategory: CategoryRequest) => {
const response = await api.post('/categories', newCategory)
return response.data
},
onSuccess: () => {
toast.success('Category created successfully!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
}
})
},
updateCategory: () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, payload }: { id: string; payload: CategoryRequest }) => {
const response = await api.put(`/categories/${id}`, payload)
return response.data
},
onSuccess: () => {
toast.success('Category updated successfully!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
}
})
},
deleteCategory: () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string) => {
const response = await api.delete(`/categories/${id}`)
return response.data
},
onSuccess: () => {
toast.success('Category deleted successfully!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
}
})
}
}

View File

@ -0,0 +1,60 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../api'
import { toast } from 'react-toastify'
import { InventoryAdjustRequest, InventoryRequest } from '../../types/services/inventory'
export const useInventoriesMutation = {
createInventory: () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newInventory: InventoryRequest) => {
const response = await api.post('/inventory', newInventory)
return response.data
},
onSuccess: () => {
toast.success('Inventory created successfully!')
queryClient.invalidateQueries({ queryKey: ['inventories'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
}
})
},
adjustInventory: () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newInventory: InventoryAdjustRequest) => {
const response = await api.post('/inventory/adjust', newInventory)
return response.data
},
onSuccess: () => {
toast.success('Inventory adjusted successfully!')
queryClient.invalidateQueries({ queryKey: ['inventories'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
}
})
},
deleteInventory: () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string) => {
const response = await api.delete(`/inventory/${id}`)
return response.data
},
onSuccess: () => {
toast.success('Inventory deleted successfully!')
queryClient.invalidateQueries({ queryKey: ['inventories'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
}
})
}
}

View File

@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query' import { useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../api' import { api } from '../api'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { ProductRequest } from '../../types/services/product' import { ProductRequest } from '../../types/services/product'
@ -10,11 +10,44 @@ export const useProductsMutation = {
const response = await api.post('/products', newProduct) const response = await api.post('/products', newProduct)
return response.data return response.data
}, },
onSuccess: data => { onSuccess: () => {
toast.success('Product created successfully!') toast.success('Product created successfully!')
}, },
onError: (error: any) => { onError: (error: any) => {
toast.error(error.response.data.errors[0].cause) toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
}
})
},
updateProduct: () => {
return useMutation({
mutationFn: async ({ id, payload }: { id: string; payload: ProductRequest }) => {
const response = await api.put(`/products/${id}`, payload)
return response.data
},
onSuccess: () => {
toast.success('Product updated successfully!')
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
}
})
},
deleteProduct: () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string) => {
const response = await api.delete(`/products/${id}`)
return response.data
},
onSuccess: () => {
toast.success('Product deleted successfully!')
queryClient.invalidateQueries({ queryKey: ['products'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
} }
}) })
} }

View File

@ -34,8 +34,6 @@ export const useCategoriesQuery = {
const res = await api.get(`/categories?${queryParams.toString()}`) const res = await api.get(`/categories?${queryParams.toString()}`)
return res.data.data return res.data.data
}, },
// Cache for 5 minutes
staleTime: 5 * 60 * 1000
}) })
} }
} }

View File

@ -0,0 +1,39 @@
import { useQuery } from "@tanstack/react-query"
import { Inventories } from "../../types/services/inventory"
import { api } from "../api"
interface InventoriesQueryParams {
page?: number
limit?: number
search?: string
}
export const useInventoriesQuery = {
getInventories: (params: InventoriesQueryParams = {}) => {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<Inventories>({
queryKey: ['inventories', { 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(`/inventory?${queryParams.toString()}`)
return res.data.data
},
})
}
}

View File

@ -0,0 +1,39 @@
import { useQuery } from "@tanstack/react-query"
import { Orders } from "../../types/services/order"
import { api } from "../api"
interface OrdersQueryParams {
page?: number
limit?: number
search?: string
}
export const useOrdersQuery = {
getOrders: (params: OrdersQueryParams = {}) => {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<Orders>({
queryKey: ['orders', { 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(`/orders?${queryParams.toString()}`)
return res.data.data
},
})
}
}

View File

@ -0,0 +1,39 @@
import { useQuery } from "@tanstack/react-query"
import { Outlets } from "../../types/services/outlet"
import { api } from "../api"
interface OutletsQueryParams {
page?: number
limit?: number
search?: string
}
export const useOutletsQuery = {
getOutlets: (params: OutletsQueryParams = {}) => {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<Outlets>({
queryKey: ['outlets', { 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(`/outlets/list?${queryParams.toString()}`)
return res.data.data
}
})
}
}

View File

@ -37,6 +37,17 @@ export const useProductsQuery = {
const res = await api.get(`/products?${queryParams.toString()}`) const res = await api.get(`/products?${queryParams.toString()}`)
return res.data.data return res.data.data
}, },
})
},
getProductById: (id: string) => {
return useQuery({
queryKey: ['product', id],
queryFn: async ({ queryKey: [, id] }) => {
const res = await api.get(`/products/${id}`)
return res.data.data
},
// Cache for 5 minutes // Cache for 5 minutes
staleTime: 5 * 60 * 1000 staleTime: 5 * 60 * 1000
}) })

View File

@ -16,3 +16,10 @@ export interface Categories {
limit: number; limit: number;
total_pages: number; total_pages: number;
} }
export interface CategoryRequest {
name: string;
description: string | null;
business_type: string;
}

View File

@ -0,0 +1,30 @@
export interface Inventories {
inventory: Inventory[]
total_count: number
page: number
limit: number
total_pages: number
}
export interface Inventory {
id: string
outlet_id: string
product_id: string
quantity: number
reorder_level: number
is_low_stock: boolean
updated_at: string // ISO 8601 timestamp
}
export interface InventoryRequest {
product_id: string
outlet_id: string
quantity: number
}
export interface InventoryAdjustRequest {
product_id: string
outlet_id: string
delta: number
reason: string
}

View File

@ -0,0 +1,45 @@
export interface Orders {
orders: Order[]
total_count: number
page: number
limit: number
total_pages: number
}
export interface Order {
id: string
order_number: string
outlet_id: string
user_id: string
table_number: string
order_type: 'dineIn' | 'takeAway' | 'delivery'
status: 'pending' | 'inProgress' | 'completed' | 'cancelled'
subtotal: number
tax_amount: number
discount_amount: number
total_amount: number
notes: string | null
metadata: {
customer_name: string
}
created_at: string
updated_at: string
order_items: OrderItem[]
}
export interface OrderItem {
id: string
order_id: string
product_id: string
product_name: string
product_variant_id: string | null
product_variant_name?: string
quantity: number
unit_price: number
total_price: number
modifiers: any[]
notes: string
status: 'pending' | 'completed' | 'cancelled'
created_at: string
updated_at: string
}

View File

@ -0,0 +1,21 @@
export interface Outlet {
id: string;
organization_id: string;
name: string;
address: string;
phone_number: string | null;
business_type: 'restaurant' | 'retail' | string; // sesuaikan jika ada enum yang lebih pasti
currency: string;
tax_rate: number;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface Outlets {
outlets: Outlet[];
total_count: number;
page: number;
limit: number;
total_pages: number;
}

View File

@ -0,0 +1,19 @@
export interface PaymentMethods {
payment_methods: PaymentMethod[];
total_count: number;
page: number;
limit: number;
total_pages: number;
}
export interface PaymentMethod {
id: string;
organization_id: string;
name: string;
type: PaymentMethodType;
is_active: boolean;
created_at: string; // ISO 8601 timestamp
updated_at: string; // ISO 8601 timestamp
}
export type PaymentMethodType = "cash" | "card" | "edc" | "delivery" | string;

View File

@ -1,49 +1,38 @@
// React Imports // React Imports
import { useState, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports // Next Imports
import Link from 'next/link' import Link from 'next/link'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card' import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent' import CardContent from '@mui/material/CardContent'
import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography'
import Checkbox from '@mui/material/Checkbox' import Checkbox from '@mui/material/Checkbox'
import Chip from '@mui/material/Chip' import Chip from '@mui/material/Chip'
import MenuItem from '@mui/material/MenuItem'
import TablePagination from '@mui/material/TablePagination' import TablePagination from '@mui/material/TablePagination'
import type { TextFieldProps } from '@mui/material/TextField' import type { TextFieldProps } from '@mui/material/TextField'
import MenuItem from '@mui/material/MenuItem' import Typography from '@mui/material/Typography'
// Third-party Imports // Third-party Imports
import classnames from 'classnames'
import { rankItem } from '@tanstack/match-sorter-utils'
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
getFilteredRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFacetedMinMaxValues,
getPaginationRowModel,
getSortedRowModel
} from '@tanstack/react-table'
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
import type { RankingInfo } from '@tanstack/match-sorter-utils' 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'
// Type Imports // Type Imports
import type { ThemeColor } from '@core/types'
import type { OrderType } from '@/types/apps/ecommerceTypes' import type { OrderType } from '@/types/apps/ecommerceTypes'
import type { Locale } from '@configs/i18n' import type { Locale } from '@configs/i18n'
import type { ThemeColor } from '@core/types'
// Component Imports // Component Imports
import CustomAvatar from '@core/components/mui/Avatar'
import OptionMenu from '@core/components/option-menu'
import CustomTextField from '@core/components/mui/TextField'
import TablePaginationComponent from '@components/TablePaginationComponent' import TablePaginationComponent from '@components/TablePaginationComponent'
import CustomAvatar from '@core/components/mui/Avatar'
import CustomTextField from '@core/components/mui/TextField'
import OptionMenu from '@core/components/option-menu'
// Util Imports // Util Imports
import { getInitials } from '@/utils/getInitials' import { getInitials } from '@/utils/getInitials'
@ -51,6 +40,10 @@ import { getLocalizedUrl } from '@/utils/i18n'
// Style Imports // Style Imports
import tableStyles from '@core/styles/table.module.css' import tableStyles from '@core/styles/table.module.css'
import { useOrdersQuery } from '../../../../../services/queries/orders'
import { Order } from '../../../../../types/services/order'
import { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../../components/layout/shared/Loading'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -85,7 +78,7 @@ export const statusChipColor: { [key: string]: StatusChipColorType } = {
Dispatched: { color: 'warning' } Dispatched: { color: 'warning' }
} }
type ECommerceOrderTypeWithAction = OrderType & { type ECommerceOrderTypeWithAction = Order & {
action?: string action?: string
} }
@ -134,15 +127,34 @@ const DebouncedInput = ({
// Column Definitions // Column Definitions
const columnHelper = createColumnHelper<ECommerceOrderTypeWithAction>() const columnHelper = createColumnHelper<ECommerceOrderTypeWithAction>()
const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => { const OrderListTable = () => {
// States // States
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [data, setData] = useState(...[orderData]) const [currentPage, setCurrentPage] = useState(1)
const [globalFilter, setGlobalFilter] = useState('') const [pageSize, setPageSize] = useState(10)
const { data, isLoading, error, isFetching } = useOrdersQuery.getOrders({
page: currentPage,
limit: pageSize
})
// Hooks // Hooks
const { lang: locale } = useParams() const { lang: locale } = useParams()
const orders = data?.orders ?? []
const totalCount = data?.total_count ?? 0
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setCurrentPage(newPage)
}, [])
// Handle page size change
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page
}, [])
// Vars // Vars
const paypal = '/images/apps/ecommerce/paypal.png' const paypal = '/images/apps/ecommerce/paypal.png'
const mastercard = '/images/apps/ecommerce/mastercard.png' const mastercard = '/images/apps/ecommerce/mastercard.png'
@ -171,84 +183,95 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
/> />
) )
}, },
columnHelper.accessor('order', { columnHelper.accessor('order_number', {
header: 'Order', header: 'Order Number',
cell: ({ row }) => ( cell: ({ row }) => (
<Typography <Typography
component={Link} component={Link}
href={getLocalizedUrl(`/apps/ecommerce/orders/details/${row.original.order}`, locale as Locale)} href={getLocalizedUrl(`/apps/ecommerce/orders/details/${row.original.order_number}`, locale as Locale)}
color='primary.main' color='primary.main'
>{`#${row.original.order}`}</Typography> >{`#${row.original.order_number}`}</Typography>
) )
}), }),
columnHelper.accessor('date', { columnHelper.accessor('table_number', {
header: 'Date', header: 'Table',
cell: ({ row }) => ( cell: ({ row }) => <Typography>{row.original.table_number}</Typography>
<Typography>{`${new Date(row.original.date).toDateString()}, ${row.original.time}`}</Typography>
)
}), }),
columnHelper.accessor('customer', { columnHelper.accessor('order_type', {
header: 'Customers', header: 'Order Type',
cell: ({ row }) => ( cell: ({ row }) => <Typography>{row.original.order_type}</Typography>
<div className='flex items-center gap-3'>
{getAvatar({ avatar: row.original.avatar, customer: row.original.customer })}
<div className='flex flex-col'>
<Typography
component={Link}
href={getLocalizedUrl('/apps/ecommerce/customers/details/879861', locale as Locale)}
color='text.primary'
className='font-medium hover:text-primary'
>
{row.original.customer}
</Typography>
<Typography variant='body2'>{row.original.email}</Typography>
</div>
</div>
)
}),
columnHelper.accessor('payment', {
header: 'Payment',
cell: ({ row }) => (
<div className='flex items-center gap-1'>
<i
className={classnames(
'tabler-circle-filled bs-2.5 is-2.5',
paymentStatus[row.original.payment].colorClassName
)}
/>
<Typography color={`${paymentStatus[row.original.payment].color}.main`} className='font-medium'>
{paymentStatus[row.original.payment].text}
</Typography>
</div>
)
}), }),
// columnHelper.accessor('order_type', {
// header: 'Customers',
// cell: ({ row }) => (
// <div className='flex items-center gap-3'>
// {getAvatar({ avatar: row.original.avatar, customer: row.original.customer })}
// <div className='flex flex-col'>
// <Typography
// component={Link}
// href={getLocalizedUrl('/apps/ecommerce/customers/details/879861', locale as Locale)}
// color='text.primary'
// className='font-medium hover:text-primary'
// >
// {row.original.customer}
// </Typography>
// <Typography variant='body2'>{row.original.email}</Typography>
// </div>
// </div>
// )
// }),
// columnHelper.accessor('payment', {
// header: 'Payment',
// cell: ({ row }) => (
// <div className='flex items-center gap-1'>
// <i
// className={classnames(
// 'tabler-circle-filled bs-2.5 is-2.5',
// paymentStatus[row.original.payment].colorClassName
// )}
// />
// <Typography color={`${paymentStatus[row.original.payment].color}.main`} className='font-medium'>
// {paymentStatus[row.original.payment].text}
// </Typography>
// </div>
// )
// }),
columnHelper.accessor('status', { columnHelper.accessor('status', {
header: 'Status', header: 'Status',
cell: ({ row }) => ( cell: ({ row }) => <Chip label={row.original.status} color={'default'} variant='tonal' size='small' />
<Chip
label={row.original.status}
color={statusChipColor[row.original.status].color}
variant='tonal'
size='small'
/>
)
}), }),
columnHelper.accessor('method', { columnHelper.accessor('subtotal', {
header: 'Method', header: 'SubTotal',
cell: ({ row }) => ( cell: ({ row }) => <Typography>{row.original.subtotal}</Typography>
<div className='flex items-center'>
<div className='flex justify-center items-center bg-[#F6F8FA] rounded-sm is-[29px] bs-[18px]'>
<img
src={row.original.method === 'mastercard' ? mastercard : paypal}
height={row.original.method === 'mastercard' ? 11 : 14}
/>
</div>
<Typography>
{`...${row.original.method === 'mastercard' ? row.original.methodNumber : '@gmail.com'}`}
</Typography>
</div>
)
}), }),
columnHelper.accessor('total_amount', {
header: 'Total',
cell: ({ row }) => <Typography>{row.original.total_amount}</Typography>
}),
columnHelper.accessor('tax_amount', {
header: 'Tax',
cell: ({ row }) => <Typography>{row.original.tax_amount}</Typography>
}),
columnHelper.accessor('discount_amount', {
header: 'Discount',
cell: ({ row }) => <Typography>{row.original.discount_amount}</Typography>
}),
// columnHelper.accessor('method', {
// header: 'Method',
// cell: ({ row }) => (
// <div className='flex items-center'>
// <div className='flex justify-center items-center bg-[#F6F8FA] rounded-sm is-[29px] bs-[18px]'>
// <img
// src={row.original.method === 'mastercard' ? mastercard : paypal}
// height={row.original.method === 'mastercard' ? 11 : 14}
// />
// </div>
// <Typography>
// {`...${row.original.method === 'mastercard' ? row.original.methodNumber : '@gmail.com'}`}
// </Typography>
// </div>
// )
// }),
columnHelper.accessor('action', { columnHelper.accessor('action', {
header: 'Action', header: 'Action',
cell: ({ row }) => ( cell: ({ row }) => (
@ -260,14 +283,17 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
{ {
text: 'View', text: 'View',
icon: 'tabler-eye', icon: 'tabler-eye',
href: getLocalizedUrl(`/apps/ecommerce/orders/details/${row.original.order}`, locale as Locale), href: getLocalizedUrl(
`/apps/ecommerce/orders/details/${row.original.order_number}`,
locale as Locale
),
linkProps: { className: 'flex items-center gap-2 is-full plb-2 pli-4' } linkProps: { className: 'flex items-center gap-2 is-full plb-2 pli-4' }
}, },
{ {
text: 'Delete', text: 'Delete',
icon: 'tabler-trash', icon: 'tabler-trash',
menuItemProps: { menuItemProps: {
onClick: () => setData(data?.filter(order => order.id !== row.original.id)), onClick: () => {},
className: 'flex items-center' className: 'flex items-center'
} }
} }
@ -283,32 +309,24 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
) )
const table = useReactTable({ const table = useReactTable({
data: data as OrderType[], data: orders as Order[],
columns, columns,
filterFns: { filterFns: {
fuzzy: fuzzyFilter fuzzy: fuzzyFilter
}, },
state: { state: {
rowSelection, rowSelection,
globalFilter
},
initialState: {
pagination: { pagination: {
pageSize: 10 pageIndex: currentPage, // <= penting!
pageSize
} }
}, },
enableRowSelection: true, //enable row selection for all rows enableRowSelection: true, //enable row selection for all rows
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
globalFilterFn: fuzzyFilter,
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
onGlobalFilterChange: setGlobalFilter, // Disable client-side pagination since we're handling it server-side
getFilteredRowModel: getFilteredRowModel(), manualPagination: true,
getSortedRowModel: getSortedRowModel(), pageCount: Math.ceil(totalCount / pageSize)
getPaginationRowModel: getPaginationRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues()
}) })
const getAvatar = (params: Pick<OrderType, 'avatar' | 'customer'>) => { const getAvatar = (params: Pick<OrderType, 'avatar' | 'customer'>) => {
@ -329,8 +347,8 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
<Card> <Card>
<CardContent className='flex justify-between max-sm:flex-col sm:items-center gap-4'> <CardContent className='flex justify-between max-sm:flex-col sm:items-center gap-4'>
<DebouncedInput <DebouncedInput
value={globalFilter ?? ''} value={''}
onChange={value => setGlobalFilter(String(value))} onChange={value => console.log('click')}
placeholder='Search Order' placeholder='Search Order'
className='sm:is-auto' className='sm:is-auto'
/> />
@ -357,6 +375,9 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
</div> </div>
</CardContent> </CardContent>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
{isLoading ? (
<Loading />
) : (
<table className={tableStyles.table}> <table className={tableStyles.table}>
<thead> <thead>
{table.getHeaderGroups().map(headerGroup => ( {table.getHeaderGroups().map(headerGroup => (
@ -410,15 +431,42 @@ const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
</tbody> </tbody>
)} )}
</table> </table>
)}
{isFetching && !isLoading && (
<Box
position='absolute'
top={0}
left={0}
right={0}
bottom={0}
display='flex'
alignItems='center'
justifyContent='center'
bgcolor='rgba(255,255,255,0.7)'
zIndex={1}
>
<CircularProgress size={24} />
</Box>
)}
</div> </div>
<TablePagination <TablePagination
component={() => <TablePaginationComponent table={table} />} component={() => (
count={table.getFilteredRowModel().rows.length} <TablePaginationComponent
rowsPerPage={table.getState().pagination.pageSize} pageIndex={currentPage}
page={table.getState().pagination.pageIndex} pageSize={pageSize}
onPageChange={(_, page) => { totalCount={totalCount}
table.setPageIndex(page) onPageChange={handlePageChange}
}} />
)}
count={totalCount}
rowsPerPage={pageSize}
page={currentPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/> />
</Card> </Card>
) )

View File

@ -4,20 +4,19 @@
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
// Type Imports // Type Imports
import type { OrderType } from '@/types/apps/ecommerceTypes'
// Component Imports // Component Imports
import OrderCard from './OrderCard' import OrderCard from './OrderCard'
import OrderListTable from './OrderListTable' import OrderListTable from './OrderListTable'
const OrderList = ({ orderData }: { orderData?: OrderType[] }) => { const OrderList = () => {
return ( return (
<Grid container spacing={6}> <Grid container spacing={6}>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<OrderCard /> <OrderCard />
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<OrderListTable orderData={orderData} /> <OrderListTable />
</Grid> </Grid>
</Grid> </Grid>
) )

View File

@ -8,28 +8,46 @@ import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../../../redux-store' import { RootState } from '../../../../../redux-store'
import { CircularProgress } from '@mui/material' import { CircularProgress } from '@mui/material'
import { resetProduct } from '../../../../../redux-store/slices/product' import { resetProduct } from '../../../../../redux-store/slices/product'
import { useParams } from 'next/navigation'
const ProductAddHeader = () => { const ProductAddHeader = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const { mutate, isPending } = useProductsMutation.createProduct() const params = useParams()
const { mutate: createProduct, isPending: isCreating } = useProductsMutation.createProduct()
const { mutate: updateProduct, isPending: isUpdating } = useProductsMutation.updateProduct()
const { productRequest } = useSelector((state: RootState) => state.productReducer) const { productRequest } = useSelector((state: RootState) => state.productReducer)
const isEdit = !!params?.id
const handleSubmit = () => { const handleSubmit = () => {
const { cost, price, ...rest } = productRequest const { cost, price, ...rest } = productRequest
const newProductRequest = { ...rest, cost: Number(cost), price: Number(price) } const newProductRequest = { ...rest, cost: Number(cost), price: Number(price) }
mutate(newProductRequest, { if (isEdit) {
updateProduct(
{ id: params?.id as string, payload: newProductRequest },
{
onSuccess: () => {
dispatch(resetProduct())
}
}
)
} else {
createProduct(newProductRequest, {
onSuccess: () => { onSuccess: () => {
dispatch(resetProduct()) dispatch(resetProduct())
} }
}) })
} }
}
return ( return (
<div className='flex flex-wrap sm:items-center justify-between max-sm:flex-col gap-6'> <div className='flex flex-wrap sm:items-center justify-between max-sm:flex-col gap-6'>
<div> <div>
<Typography variant='h4' className='mbe-1'> <Typography variant='h4' className='mbe-1'>
Add a new product {isEdit ? 'Edit Product' : 'Add a new product'}
</Typography> </Typography>
<Typography>Orders placed across your store</Typography> <Typography>Orders placed across your store</Typography>
</div> </div>
@ -38,8 +56,9 @@ const ProductAddHeader = () => {
Discard Discard
</Button> </Button>
<Button variant='tonal'>Save Draft</Button> <Button variant='tonal'>Save Draft</Button>
<Button variant='contained' disabled={isPending} onClick={handleSubmit}> <Button variant='contained' disabled={isEdit ? isUpdating : isCreating} onClick={handleSubmit}>
Publish Product {isPending && <CircularProgress color='inherit' size={16} className='ml-2' />} {isEdit ? 'Update Product' : 'Publish Product'}
{(isCreating || isUpdating) && <CircularProgress color='inherit' size={16} className='ml-2' />}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -26,8 +26,11 @@ import '@/libs/styles/tiptapEditor.css'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../../../redux-store' import { RootState } from '../../../../../redux-store'
import { setProductField } from '@/redux-store/slices/product' import { setProduct, setProductField } from '@/redux-store/slices/product'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useParams } from 'next/navigation'
import { useProductsQuery } from '../../../../../services/queries/products'
import Loading from '../../../../../components/layout/shared/Loading'
const EditorToolbar = ({ editor }: { editor: Editor | null }) => { const EditorToolbar = ({ editor }: { editor: Editor | null }) => {
if (!editor) { if (!editor) {
@ -120,8 +123,19 @@ const EditorToolbar = ({ editor }: { editor: Editor | null }) => {
const ProductInformation = () => { const ProductInformation = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const params = useParams()
const { data: product, isLoading, error } = useProductsQuery.getProductById(params?.id as string)
const { name, sku, barcode, description } = useSelector((state: RootState) => state.productReducer.productRequest) const { name, sku, barcode, description } = useSelector((state: RootState) => state.productReducer.productRequest)
const isEdit = !!params?.id
useEffect(() => {
if (product) {
dispatch(setProduct(product))
}
}, [product, dispatch])
const handleInputChange = (field: any, value: any) => { const handleInputChange = (field: any, value: any) => {
dispatch(setProductField({ field, value })) dispatch(setProductField({ field, value }))
} }
@ -160,6 +174,8 @@ const ProductInformation = () => {
} }
}, [editor]) }, [editor])
if (isLoading) return <Loading />
return ( return (
<Card> <Card>
<CardHeader title='Product Information' /> <CardHeader title='Product Information' />
@ -170,7 +186,7 @@ const ProductInformation = () => {
fullWidth fullWidth
label='Product Name' label='Product Name'
placeholder='iPhone 14' placeholder='iPhone 14'
value={name} value={name || ''}
onChange={e => handleInputChange('name', e.target.value)} onChange={e => handleInputChange('name', e.target.value)}
/> />
</Grid> </Grid>
@ -179,7 +195,7 @@ const ProductInformation = () => {
fullWidth fullWidth
label='SKU' label='SKU'
placeholder='FXSK123U' placeholder='FXSK123U'
value={sku} value={sku || ''}
onChange={e => handleInputChange('sku', e.target.value)} onChange={e => handleInputChange('sku', e.target.value)}
/> />
</Grid> </Grid>
@ -188,7 +204,7 @@ const ProductInformation = () => {
fullWidth fullWidth
label='Barcode' label='Barcode'
placeholder='0123-4567' placeholder='0123-4567'
value={barcode} value={barcode || ''}
onChange={e => handleInputChange('barcode', e.target.value)} onChange={e => handleInputChange('barcode', e.target.value)}
/> />
</Grid> </Grid>

View File

@ -1,95 +1,67 @@
// React Imports // React Imports
import { useState, useRef } from 'react' import { useState } from 'react'
import type { ChangeEvent } from 'react'
// MUI Imports // MUI Imports
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
import Drawer from '@mui/material/Drawer' import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
import MenuItem from '@mui/material/MenuItem' import MenuItem from '@mui/material/MenuItem'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Divider from '@mui/material/Divider'
import InputAdornment from '@mui/material/InputAdornment'
// Third-party Imports // Third-party Imports
import { useForm, Controller } from 'react-hook-form'
// Type Imports // Type Imports
import type { categoryType } from './ProductCategoryTable'
// Components Imports // Components Imports
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import { useCategoriesMutation } from '../../../../../services/mutations/categories'
import { CategoryRequest } from '../../../../../types/services/category'
type Props = { type Props = {
open: boolean open: boolean
handleClose: () => void handleClose: () => void
categoryData: categoryType[]
setData: (data: categoryType[]) => void
}
type FormValues = {
title: string
description: string
} }
const AddCategoryDrawer = (props: Props) => { const AddCategoryDrawer = (props: Props) => {
// Props // Props
const { open, handleClose, categoryData, setData } = props const { open, handleClose } = props
const { mutate: createCategory, isPending: isCreating } = useCategoriesMutation.createCategory()
// States // States
const [fileName, setFileName] = useState('') const [formData, setFormData] = useState<CategoryRequest>({
const [category, setCategory] = useState('') name: '',
const [comment, setComment] = useState('') description: '',
const [status, setStatus] = useState('') business_type: ''
// Refs
const fileInputRef = useRef<HTMLInputElement>(null)
// Hooks
const {
control,
reset: resetForm,
handleSubmit,
formState: { errors }
} = useForm<FormValues>({
defaultValues: {
title: '',
description: ''
}
}) })
// Handle Form Submit // Handle Form Submit
const handleFormSubmit = (data: FormValues) => { const handleFormSubmit = (e: any) => {
const newData = { e.preventDefault()
id: categoryData.length + 1,
categoryTitle: data.title, createCategory(formData, {
description: data.description, onSuccess: () => {
totalProduct: Math.floor(Math.random() * 9000) + 1000, handleReset()
totalEarning: Math.floor(Math.random() * 90000) + 10000, }
image: `/images/apps/ecommerce/product-${Math.floor(Math.random() * 20) + 1}.png` })
} }
setData([...categoryData, newData]) const handleInputChange = (e: any) => {
handleReset() setFormData({
...formData,
[e.target.name]: e.target.value
})
} }
// Handle Form Reset // Handle Form Reset
const handleReset = () => { const handleReset = () => {
handleClose() handleClose()
resetForm({ title: '', description: '' }) setFormData({
setFileName('') name: '',
setCategory('') description: '',
setComment('') business_type: ''
setStatus('') })
}
// Handle File Upload
const handleFileUpload = (event: ChangeEvent<HTMLInputElement>) => {
const { files } = event.target
if (files && files.length !== 0) {
setFileName(files[0].name)
}
} }
return ( return (
@ -109,95 +81,37 @@ const AddCategoryDrawer = (props: Props) => {
</div> </div>
<Divider /> <Divider />
<div className='p-6'> <div className='p-6'>
<form onSubmit={handleSubmit(data => handleFormSubmit(data))} className='flex flex-col gap-5'> <form onSubmit={handleFormSubmit} className='flex flex-col gap-5'>
<Controller
name='title'
control={control}
rules={{ required: true }}
render={({ field }) => (
<CustomTextField <CustomTextField
{...field}
fullWidth fullWidth
label='Title' label='Title'
placeholder='Fashion' name='name'
{...(errors.title && { error: true, helperText: 'This field is required.' })} value={formData.name}
onChange={handleInputChange}
placeholder='Minuman'
/> />
)}
/>
<Controller
name='description'
control={control}
rules={{ required: true }}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
label='Description'
placeholder='Enter a description...'
{...(errors.description && { error: true, helperText: 'This field is required.' })}
/>
)}
/>
<div className='flex items-end gap-4'>
<CustomTextField
label='Attachment'
placeholder='No file chosen'
value={fileName}
className='flex-auto'
slotProps={{
input: {
readOnly: true,
endAdornment: fileName ? (
<InputAdornment position='end'>
<IconButton size='small' edge='end' onClick={() => setFileName('')}>
<i className='tabler-x' />
</IconButton>
</InputAdornment>
) : null
}
}}
/>
<Button component='label' variant='tonal' htmlFor='contained-button-file' className='min-is-fit'>
Choose
<input hidden id='contained-button-file' type='file' onChange={handleFileUpload} ref={fileInputRef} />
</Button>
</div>
<CustomTextField <CustomTextField
select select
fullWidth fullWidth
label='Parent Category' label='Business Type'
value={category} value={formData.business_type}
onChange={e => setCategory(e.target.value)} onChange={e => setFormData({ ...formData, business_type: e.target.value })}
> >
<MenuItem value='HouseHold'>HouseHold</MenuItem> <MenuItem value='restaurant'>Restaurant</MenuItem>
<MenuItem value='Management'>Management</MenuItem>
<MenuItem value='Electronics'>Electronics</MenuItem>
<MenuItem value='Office'>Office</MenuItem>
<MenuItem value='Accessories'>Accessories</MenuItem>
</CustomTextField> </CustomTextField>
<CustomTextField <CustomTextField
fullWidth fullWidth
label='Comment' label='Description'
value={comment} value={formData.description}
onChange={e => setComment(e.target.value)} name='description'
onChange={handleInputChange}
multiline multiline
rows={4} rows={4}
placeholder='Write a Comment...' placeholder='Write a Comment...'
/> />
<CustomTextField
select
fullWidth
label='Category Status'
value={status}
onChange={e => setStatus(e.target.value)}
>
<MenuItem value='Published'>Published</MenuItem>
<MenuItem value='Inactive'>Inactive</MenuItem>
<MenuItem value='Scheduled'>Scheduled</MenuItem>
</CustomTextField>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<Button variant='contained' type='submit'> <Button variant='contained' type='submit' disabled={isCreating}>
Add {isCreating ? 'Add...' : 'Add'}
</Button> </Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}> <Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard Discard

View File

@ -0,0 +1,137 @@
// React Imports
import { useEffect, 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 { useCategoriesMutation } from '../../../../../services/mutations/categories'
import { Category, CategoryRequest } from '../../../../../types/services/category'
type Props = {
open: boolean
handleClose: () => void
data: Category
}
const EditCategoryDrawer = (props: Props) => {
// Props
const { open, handleClose, data } = props
const { mutate: updateCategory, isPending: isCreating } = useCategoriesMutation.updateCategory()
// States
const [formData, setFormData] = useState<CategoryRequest>({
name: '',
description: '',
business_type: ''
})
useEffect(() => {
if (data) {
setFormData({
name: data.name,
description: data.description,
business_type: data.business_type
})
}
}, [data])
// Handle Form Submit
const handleFormSubmit = (e: any) => {
e.preventDefault()
updateCategory({ 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: '',
description: '',
business_type: ''
})
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between pli-6 plb-5'>
<Typography variant='h5'>Edit Category</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-textSecondary text-2xl' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleFormSubmit} className='flex flex-col gap-5'>
<CustomTextField
fullWidth
label='Title'
name='name'
value={formData.name}
onChange={handleInputChange}
placeholder='Minuman'
/>
<CustomTextField
select
fullWidth
label='Business Type'
value={formData.business_type}
onChange={e => setFormData({ ...formData, business_type: e.target.value })}
>
<MenuItem value='restaurant'>Restaurant</MenuItem>
</CustomTextField>
<CustomTextField
fullWidth
label='Description'
value={formData.description || ''}
name='description'
onChange={handleInputChange}
multiline
rows={4}
placeholder='Write a Comment...'
/>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit' disabled={isCreating}>
{isCreating ? 'Updating...' : 'Update'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default EditCategoryDrawer

View File

@ -43,6 +43,9 @@ import { useCategoriesQuery } from '../../../../../services/queries/categories'
import { Category } from '../../../../../types/services/category' import { Category } from '../../../../../types/services/category'
import { Box, CircularProgress } from '@mui/material' import { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../../components/layout/shared/Loading' import Loading from '../../../../../components/layout/shared/Loading'
import { useCategoriesMutation } from '../../../../../services/mutations/categories'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import EditCategoryDrawer from './EditCategoryDrawer'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -105,9 +108,13 @@ const columnHelper = createColumnHelper<CategoryWithActionsType>()
const ProductCategoryTable = () => { const ProductCategoryTable = () => {
// States // States
const [addCategoryOpen, setAddCategoryOpen] = useState(false) const [addCategoryOpen, setAddCategoryOpen] = useState(false)
const [editCategoryOpen, setEditCategoryOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
const [categoryId, setCategoryId] = useState('')
const [openConfirm, setOpenConfirm] = useState(false)
const [currentCategory, setCurrentCategory] = useState<Category>()
// Fetch products with pagination and search // Fetch products with pagination and search
const { data, isLoading, error, isFetching } = useCategoriesQuery.getCategories({ const { data, isLoading, error, isFetching } = useCategoriesQuery.getCategories({
@ -115,6 +122,8 @@ const ProductCategoryTable = () => {
limit: pageSize limit: pageSize
}) })
const { mutate: deleteCategory, isPending: isDeleting } = useCategoriesMutation.deleteCategory()
const categories = data?.categories ?? [] const categories = data?.categories ?? []
const totalCount = data?.total_count ?? 0 const totalCount = data?.total_count ?? 0
@ -129,6 +138,12 @@ const ProductCategoryTable = () => {
setCurrentPage(0) // Reset to first page setCurrentPage(0) // Reset to first page
}, []) }, [])
const handleDelete = () => {
deleteCategory(categoryId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<CategoryWithActionsType, any>[]>( const columns = useMemo<ColumnDef<CategoryWithActionsType, any>[]>(
() => [ () => [
{ {
@ -162,7 +177,6 @@ const ProductCategoryTable = () => {
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
{row.original.name} {row.original.name}
</Typography> </Typography>
<Typography variant='body2'>{row.original.description}</Typography>
</div> </div>
</div> </div>
) )
@ -175,11 +189,18 @@ const ProductCategoryTable = () => {
header: 'Business Type', header: 'Business Type',
cell: ({ row }) => <Typography>{row.original.business_type}</Typography> cell: ({ row }) => <Typography>{row.original.business_type}</Typography>
}), }),
columnHelper.accessor('created_at', {
header: 'Created At',
cell: ({ row }) => <Typography>{row.original.created_at}</Typography>
}),
columnHelper.accessor('actions', { columnHelper.accessor('actions', {
header: 'Actions', header: 'Actions',
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex items-center'> <div className='flex items-center'>
<IconButton> <IconButton onClick={() => {
setCurrentCategory(row.original)
setEditCategoryOpen(!editCategoryOpen)
}}>
<i className='tabler-edit text-textSecondary' /> <i className='tabler-edit text-textSecondary' />
</IconButton> </IconButton>
<OptionMenu <OptionMenu
@ -190,7 +211,12 @@ const ProductCategoryTable = () => {
{ {
text: 'Delete', text: 'Delete',
icon: 'tabler-trash', icon: 'tabler-trash',
menuItemProps: { onClick: () => console.log('click') } menuItemProps: {
onClick: () => {
setCategoryId(row.original.id)
setOpenConfirm(true)
}
}
}, },
{ text: 'Duplicate', icon: 'tabler-copy' } { text: 'Duplicate', icon: 'tabler-copy' }
]} ]}
@ -350,12 +376,26 @@ const ProductCategoryTable = () => {
disabled={isLoading} disabled={isLoading}
/> />
</Card> </Card>
<AddCategoryDrawer <AddCategoryDrawer
open={addCategoryOpen} open={addCategoryOpen}
categoryData={categories}
setData={() => {}}
handleClose={() => setAddCategoryOpen(!addCategoryOpen)} handleClose={() => setAddCategoryOpen(!addCategoryOpen)}
/> />
<EditCategoryDrawer
open={editCategoryOpen}
handleClose={() => setEditCategoryOpen(!editCategoryOpen)}
data={currentCategory!}
/>
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={isDeleting}
title='Delete Category'
message='Are you sure you want to delete this category? This action cannot be undone.'
/>
</> </>
) )
} }

View File

@ -45,6 +45,8 @@ import { Box, CircularProgress } from '@mui/material'
import Loading from '../../../../../components/layout/shared/Loading' import Loading from '../../../../../components/layout/shared/Loading'
import { useProductsQuery } from '../../../../../services/queries/products' import { useProductsQuery } from '../../../../../services/queries/products'
import { Product } from '../../../../../types/services/product' import { Product } from '../../../../../types/services/product'
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
import { useProductsMutation } from '../../../../../services/mutations/products'
declare module '@tanstack/table-core' { declare module '@tanstack/table-core' {
interface FilterFns { interface FilterFns {
@ -108,6 +110,8 @@ const ProductListTable = () => {
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false)
const [productId, setProductId] = useState('')
// Hooks // Hooks
const { lang: locale } = useParams() const { lang: locale } = useParams()
@ -118,6 +122,8 @@ const ProductListTable = () => {
limit: pageSize limit: pageSize
}) })
const { mutate: deleteProduct, isPending: isDeleting } = useProductsMutation.deleteProduct()
const products = data?.products ?? [] const products = data?.products ?? []
const totalCount = data?.total_count ?? 0 const totalCount = data?.total_count ?? 0
@ -132,6 +138,12 @@ const ProductListTable = () => {
setCurrentPage(0) // Reset to first page setCurrentPage(0) // Reset to first page
}, []) }, [])
const handleDelete = () => {
deleteProduct(productId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<ProductWithActionsType, any>[]>( const columns = useMemo<ColumnDef<ProductWithActionsType, any>[]>(
() => [ () => [
{ {
@ -213,7 +225,10 @@ const ProductListTable = () => {
header: 'Actions', header: 'Actions',
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex items-center'> <div className='flex items-center'>
<IconButton> <IconButton
LinkComponent={Link}
href={getLocalizedUrl(`/apps/ecommerce/products/${row.original.id}/edit`, locale as Locale)}
>
<i className='tabler-edit text-textSecondary' /> <i className='tabler-edit text-textSecondary' />
</IconButton> </IconButton>
<OptionMenu <OptionMenu
@ -224,7 +239,12 @@ const ProductListTable = () => {
{ {
text: 'Delete', text: 'Delete',
icon: 'tabler-trash', icon: 'tabler-trash',
menuItemProps: { onClick: () => console.log('click') } menuItemProps: {
onClick: () => {
setOpenConfirm(true)
setProductId(row.original.id)
}
}
}, },
{ text: 'Duplicate', icon: 'tabler-copy' } { text: 'Duplicate', icon: 'tabler-copy' }
]} ]}
@ -397,6 +417,13 @@ const ProductListTable = () => {
disabled={isLoading} disabled={isLoading}
/> />
</Card> </Card>
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={isDeleting}
/>
</> </>
) )
} }

View File

@ -0,0 +1,203 @@
// React Imports
import { 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 Typography from '@mui/material/Typography'
// Third-party Imports
// Type Imports
// Components Imports
import CustomTextField from '@core/components/mui/TextField'
import { Autocomplete, CircularProgress } from '@mui/material'
import { useDebounce } from 'use-debounce'
import { useInventoriesMutation } from '../../../../services/mutations/inventories'
import { useOutletsQuery } from '../../../../services/queries/outlets'
import { useProductsQuery } from '../../../../services/queries/products'
import { InventoryAdjustRequest } from '../../../../types/services/inventory'
type Props = {
open: boolean
handleClose: () => void
}
const AdjustmentStockDrawer = (props: Props) => {
// Props
const { open, handleClose } = props
const { mutate: adjustInventory, isPending: isCreating } = useInventoriesMutation.adjustInventory()
// States
const [productInput, setProductInput] = useState('')
const [productDebouncedInput] = useDebounce(productInput, 500) // debounce for better UX
const [outletInput, setOutletInput] = useState('')
const [outletDebouncedInput] = useDebounce(outletInput, 500) // debounce for better UX
const [formData, setFormData] = useState<InventoryAdjustRequest>({
product_id: '',
outlet_id: '',
delta: 0,
reason: ''
})
const { data: outlets, isLoading: outletsLoading } = useOutletsQuery.getOutlets({
search: outletDebouncedInput
})
const { data: products, isLoading } = useProductsQuery.getProducts({
search: productDebouncedInput
})
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
const options = useMemo(() => products?.products || [], [products])
// Handle Form Submit
const handleFormSubmit = (e: any) => {
e.preventDefault()
adjustInventory(
{ ...formData, delta: Number(formData.delta) },
{
onSuccess: () => {
handleReset()
}
}
)
}
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
// Handle Form Reset
const handleReset = () => {
handleClose()
setFormData({
product_id: '',
outlet_id: '',
delta: 0,
reason: ''
})
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between pli-6 plb-5'>
<Typography variant='h5'>Adjust Inventory</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-textSecondary text-2xl' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleFormSubmit} className='flex flex-col gap-5'>
<Autocomplete
options={outletOptions}
loading={outletsLoading}
getOptionLabel={option => 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 => (
<CustomTextField
{...params}
className=''
label='Outlet'
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{outletsLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<Autocomplete
options={options}
loading={isLoading}
getOptionLabel={option => option.name}
value={options.find(p => p.id === formData.product_id) || null}
onInputChange={(event, newProductInput) => {
setProductInput(newProductInput)
}}
onChange={(event, newValue) => {
setFormData({
...formData,
product_id: newValue?.id || ''
})
}}
renderInput={params => (
<CustomTextField
{...params}
className=''
label='Product'
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<CustomTextField
fullWidth
label='Delta'
name='delta'
value={formData.delta}
onChange={handleInputChange}
placeholder='0'
/>
<CustomTextField
fullWidth
label='Reason'
value={formData.reason}
name='reason'
onChange={handleInputChange}
multiline
rows={4}
placeholder='Write a Comment...'
/>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit' disabled={isCreating}>
{isCreating ? 'Adjusting...' : 'Adjust'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default AdjustmentStockDrawer

View File

@ -0,0 +1,392 @@
'use client'
// React Imports
import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports
import { useParams } from 'next/navigation'
// MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import Checkbox from '@mui/material/Checkbox'
import Chip from '@mui/material/Chip'
import Divider from '@mui/material/Divider'
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'
// Type Imports
// 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'
import { Inventory } from '../../../../types/services/inventory'
import AdjustmentStockDrawer from './AdjustmentStockDrawer'
declare module '@tanstack/table-core' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
type InventoryWithActionsType = Inventory & {
actions?: string
}
const fuzzyFilter: FilterFn<any> = (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<TextFieldProps, 'onChange'>) => {
// 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 <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
}
// Column Definitions
const columnHelper = createColumnHelper<InventoryWithActionsType>()
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
const { data, isLoading, error, isFetching } = useInventoriesQuery.getInventories({
page: currentPage,
limit: pageSize
})
const { mutate: deleteInventory, isPending: isDeleting } = useInventoriesMutation.deleteInventory()
const inventories = data?.inventory ?? []
const totalCount = data?.total_count ?? 0
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setCurrentPage(newPage)
}, [])
// Handle page size change
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page
}, [])
const handleDelete = () => {
deleteInventory(productId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<InventoryWithActionsType, any>[]>(
() => [
{
id: 'select',
header: ({ table }) => (
<Checkbox
{...{
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler()
}}
/>
),
cell: ({ row }) => (
<Checkbox
{...{
checked: row.getIsSelected(),
disabled: !row.getCanSelect(),
indeterminate: row.getIsSomeSelected(),
onChange: row.getToggleSelectedHandler()
}}
/>
)
},
columnHelper.accessor('product_id', {
header: 'Product',
cell: ({ row }) => <Typography>{row.original.product_id}</Typography>
}),
columnHelper.accessor('quantity', {
header: 'Quantity',
cell: ({ row }) => <Typography>{row.original.quantity}</Typography>
}),
columnHelper.accessor('reorder_level', {
header: 'Reorder Level',
cell: ({ row }) => <Typography>{row.original.reorder_level}</Typography>
}),
columnHelper.accessor('is_low_stock', {
header: 'Status',
cell: ({ row }) => (
<Chip
label={row.original.is_low_stock ? 'Low' : 'Normal'}
variant='tonal'
color={row.original.is_low_stock ? 'error' : 'success'}
size='small'
/>
)
}),
// columnHelper.accessor('actions', {
// header: 'Actions',
// cell: ({ row }) => (
// <div className='flex items-center'>
// <OptionMenu
// iconButtonProps={{ size: 'medium' }}
// iconClassName='text-textSecondary'
// options={[
// { text: 'Download', icon: 'tabler-download' },
// {
// text: 'Delete',
// icon: 'tabler-trash',
// menuItemProps: {
// onClick: () => {
// setOpenConfirm(true)
// setProductId(row.original.id)
// }
// }
// },
// { text: 'Duplicate', icon: 'tabler-copy' }
// ]}
// />
// </div>
// ),
// enableSorting: false
// })
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
const table = useReactTable({
data: inventories as Inventory[],
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 (
<>
<Card>
<CardHeader title='Filters' />
{/* <TableFilters setData={() => {}} productData={[]} /> */}
<Divider />
<div className='flex flex-wrap justify-between gap-4 p-6'>
<DebouncedInput
value={'search'}
onChange={value => console.log(value)}
placeholder='Search Product'
className='max-sm:is-full'
/>
<div className='flex flex-wrap items-center max-sm:flex-col gap-4 max-sm:is-full is-auto'>
<CustomTextField
select
value={table.getState().pagination.pageSize}
onChange={e => table.setPageSize(Number(e.target.value))}
className='flex-auto is-[70px] max-sm:is-full'
>
<MenuItem value='10'>10</MenuItem>
<MenuItem value='25'>25</MenuItem>
<MenuItem value='50'>50</MenuItem>
</CustomTextField>
<Button
color='secondary'
variant='tonal'
className='max-sm:is-full is-auto'
startIcon={<i className='tabler-upload' />}
>
Export
</Button>
<Button
variant='contained'
className='max-sm:is-full'
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
startIcon={<i className='tabler-plus' />}
>
Adjust Inventory
</Button>
</div>
</div>
<div className='overflow-x-auto'>
{isLoading ? (
<Loading />
) : (
<table className={tableStyles.table}>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder ? null : (
<>
<div
className={classnames({
'flex items-center': header.column.getIsSorted(),
'cursor-pointer select-none': header.column.getCanSort()
})}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: <i className='tabler-chevron-up text-xl' />,
desc: <i className='tabler-chevron-down text-xl' />
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
</div>
</>
)}
</th>
))}
</tr>
))}
</thead>
{table.getFilteredRowModel().rows.length === 0 ? (
<tbody>
<tr>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
No data available
</td>
</tr>
</tbody>
) : (
<tbody>
{table
.getRowModel()
.rows.slice(0, table.getState().pagination.pageSize)
.map(row => {
return (
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
)
})}
</tbody>
)}
</table>
)}
{isFetching && !isLoading && (
<Box
position='absolute'
top={0}
left={0}
right={0}
bottom={0}
display='flex'
alignItems='center'
justifyContent='center'
bgcolor='rgba(255,255,255,0.7)'
zIndex={1}
>
<CircularProgress size={24} />
</Box>
)}
</div>
<TablePagination
component={() => (
<TablePaginationComponent
pageIndex={currentPage}
pageSize={pageSize}
totalCount={totalCount}
onPageChange={handlePageChange}
/>
)}
count={totalCount}
rowsPerPage={pageSize}
page={currentPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/>
</Card>
<AdjustmentStockDrawer open={addInventoryOpen} handleClose={() => setAddInventoryOpen(!addInventoryOpen)} />
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={isDeleting}
title='Delete Inventory'
message='Are you sure you want to delete this inventory? This action cannot be undone.'
/>
</>
)
}
export default StockListTable

View File

@ -0,0 +1,191 @@
// React Imports
import { 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 Typography from '@mui/material/Typography'
// Third-party Imports
// Type Imports
// Components Imports
import CustomTextField from '@core/components/mui/TextField'
import { Autocomplete, CircularProgress } from '@mui/material'
import { useDebounce } from 'use-debounce'
import { useInventoriesMutation } from '../../../../services/mutations/inventories'
import { useOutletsQuery } from '../../../../services/queries/outlets'
import { useProductsQuery } from '../../../../services/queries/products'
import { InventoryRequest } from '../../../../types/services/inventory'
type Props = {
open: boolean
handleClose: () => void
}
const AddStockDrawer = (props: Props) => {
// Props
const { open, handleClose } = props
const { mutate: createInventory, isPending: isCreating } = useInventoriesMutation.createInventory()
// States
const [productInput, setProductInput] = useState('')
const [productDebouncedInput] = useDebounce(productInput, 500) // debounce for better UX
const [outletInput, setOutletInput] = useState('')
const [outletDebouncedInput] = useDebounce(outletInput, 500) // debounce for better UX
const [formData, setFormData] = useState<InventoryRequest>({
product_id: '',
outlet_id: '',
quantity: 0
})
const { data: outlets, isLoading: outletsLoading } = useOutletsQuery.getOutlets({
search: outletDebouncedInput
})
const { data: products, isLoading } = useProductsQuery.getProducts({
search: productDebouncedInput
})
const outletOptions = useMemo(() => outlets?.outlets || [], [outlets])
const options = useMemo(() => products?.products || [], [products])
// Handle Form Submit
const handleFormSubmit = (e: any) => {
e.preventDefault()
createInventory(
{ ...formData, quantity: Number(formData.quantity) },
{
onSuccess: () => {
handleReset()
}
}
)
}
const handleInputChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
// Handle Form Reset
const handleReset = () => {
handleClose()
setFormData({
product_id: '',
outlet_id: '',
quantity: 0
})
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between pli-6 plb-5'>
<Typography variant='h5'>Add Inventory</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-textSecondary text-2xl' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleFormSubmit} className='flex flex-col gap-5'>
<Autocomplete
options={outletOptions}
loading={outletsLoading}
getOptionLabel={option => 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 => (
<CustomTextField
{...params}
className=''
label='Outlet'
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{outletsLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<Autocomplete
options={options}
loading={isLoading}
getOptionLabel={option => option.name}
value={options.find(p => p.id === formData.product_id) || null}
onInputChange={(event, newProductInput) => {
setProductInput(newProductInput)
}}
onChange={(event, newValue) => {
setFormData({
...formData,
product_id: newValue?.id || ''
})
}}
renderInput={params => (
<CustomTextField
{...params}
className=''
label='Product'
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoading && <CircularProgress size={18} />}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<CustomTextField
fullWidth
label='Quantity'
name='quantity'
value={formData.quantity}
onChange={handleInputChange}
placeholder='0'
/>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit' disabled={isCreating}>
{isCreating ? 'Add...' : 'Add'}
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Discard
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default AddStockDrawer

View File

@ -0,0 +1,392 @@
'use client'
// React Imports
import { useCallback, useEffect, useMemo, useState } from 'react'
// Next Imports
import { useParams } from 'next/navigation'
// MUI Imports
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import Checkbox from '@mui/material/Checkbox'
import Chip from '@mui/material/Chip'
import Divider from '@mui/material/Divider'
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'
// Type Imports
// 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'
import { Inventory } from '../../../../types/services/inventory'
import AddStockDrawer from './AddStockDrawer'
declare module '@tanstack/table-core' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
type InventoryWithActionsType = Inventory & {
actions?: string
}
const fuzzyFilter: FilterFn<any> = (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<TextFieldProps, 'onChange'>) => {
// 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 <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
}
// Column Definitions
const columnHelper = createColumnHelper<InventoryWithActionsType>()
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
const { data, isLoading, error, isFetching } = useInventoriesQuery.getInventories({
page: currentPage,
limit: pageSize
})
const { mutate: deleteInventory, isPending: isDeleting } = useInventoriesMutation.deleteInventory()
const inventories = data?.inventory ?? []
const totalCount = data?.total_count ?? 0
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setCurrentPage(newPage)
}, [])
// Handle page size change
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10)
setPageSize(newPageSize)
setCurrentPage(0) // Reset to first page
}, [])
const handleDelete = () => {
deleteInventory(productId, {
onSuccess: () => setOpenConfirm(false)
})
}
const columns = useMemo<ColumnDef<InventoryWithActionsType, any>[]>(
() => [
{
id: 'select',
header: ({ table }) => (
<Checkbox
{...{
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler()
}}
/>
),
cell: ({ row }) => (
<Checkbox
{...{
checked: row.getIsSelected(),
disabled: !row.getCanSelect(),
indeterminate: row.getIsSomeSelected(),
onChange: row.getToggleSelectedHandler()
}}
/>
)
},
columnHelper.accessor('product_id', {
header: 'Product',
cell: ({ row }) => <Typography>{row.original.product_id}</Typography>
}),
columnHelper.accessor('quantity', {
header: 'Quantity',
cell: ({ row }) => <Typography>{row.original.quantity}</Typography>
}),
columnHelper.accessor('reorder_level', {
header: 'Reorder Level',
cell: ({ row }) => <Typography>{row.original.reorder_level}</Typography>
}),
columnHelper.accessor('is_low_stock', {
header: 'Status',
cell: ({ row }) => (
<Chip
label={row.original.is_low_stock ? 'Low' : 'Normal'}
variant='tonal'
color={row.original.is_low_stock ? 'error' : 'success'}
size='small'
/>
)
}),
columnHelper.accessor('actions', {
header: 'Actions',
cell: ({ row }) => (
<div className='flex items-center'>
<OptionMenu
iconButtonProps={{ size: 'medium' }}
iconClassName='text-textSecondary'
options={[
{ text: 'Download', icon: 'tabler-download' },
{
text: 'Delete',
icon: 'tabler-trash',
menuItemProps: {
onClick: () => {
setOpenConfirm(true)
setProductId(row.original.id)
}
}
},
{ text: 'Duplicate', icon: 'tabler-copy' }
]}
/>
</div>
),
enableSorting: false
})
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
const table = useReactTable({
data: inventories as Inventory[],
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 (
<>
<Card>
<CardHeader title='Filters' />
{/* <TableFilters setData={() => {}} productData={[]} /> */}
<Divider />
<div className='flex flex-wrap justify-between gap-4 p-6'>
<DebouncedInput
value={'search'}
onChange={value => console.log(value)}
placeholder='Search Product'
className='max-sm:is-full'
/>
<div className='flex flex-wrap items-center max-sm:flex-col gap-4 max-sm:is-full is-auto'>
<CustomTextField
select
value={table.getState().pagination.pageSize}
onChange={e => table.setPageSize(Number(e.target.value))}
className='flex-auto is-[70px] max-sm:is-full'
>
<MenuItem value='10'>10</MenuItem>
<MenuItem value='25'>25</MenuItem>
<MenuItem value='50'>50</MenuItem>
</CustomTextField>
<Button
color='secondary'
variant='tonal'
className='max-sm:is-full is-auto'
startIcon={<i className='tabler-upload' />}
>
Export
</Button>
<Button
variant='contained'
className='max-sm:is-full'
onClick={() => setAddInventoryOpen(!addInventoryOpen)}
startIcon={<i className='tabler-plus' />}
>
Add Inventory
</Button>
</div>
</div>
<div className='overflow-x-auto'>
{isLoading ? (
<Loading />
) : (
<table className={tableStyles.table}>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder ? null : (
<>
<div
className={classnames({
'flex items-center': header.column.getIsSorted(),
'cursor-pointer select-none': header.column.getCanSort()
})}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: <i className='tabler-chevron-up text-xl' />,
desc: <i className='tabler-chevron-down text-xl' />
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
</div>
</>
)}
</th>
))}
</tr>
))}
</thead>
{table.getFilteredRowModel().rows.length === 0 ? (
<tbody>
<tr>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
No data available
</td>
</tr>
</tbody>
) : (
<tbody>
{table
.getRowModel()
.rows.slice(0, table.getState().pagination.pageSize)
.map(row => {
return (
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
)
})}
</tbody>
)}
</table>
)}
{isFetching && !isLoading && (
<Box
position='absolute'
top={0}
left={0}
right={0}
bottom={0}
display='flex'
alignItems='center'
justifyContent='center'
bgcolor='rgba(255,255,255,0.7)'
zIndex={1}
>
<CircularProgress size={24} />
</Box>
)}
</div>
<TablePagination
component={() => (
<TablePaginationComponent
pageIndex={currentPage}
pageSize={pageSize}
totalCount={totalCount}
onPageChange={handlePageChange}
/>
)}
count={totalCount}
rowsPerPage={pageSize}
page={currentPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/>
</Card>
<AddStockDrawer open={addInventoryOpen} handleClose={() => setAddInventoryOpen(!addInventoryOpen)} />
<ConfirmDeleteDialog
open={openConfirm}
onClose={() => setOpenConfirm(false)}
onConfirm={handleDelete}
isLoading={isDeleting}
title='Delete Inventory'
message='Are you sure you want to delete this inventory? This action cannot be undone.'
/>
</>
)
}
export default StockListTable

View File

@ -0,0 +1,107 @@
// React Imports
import { useState, useEffect } from 'react'
// MUI Imports
import Grid from '@mui/material/Grid2'
import CardContent from '@mui/material/CardContent'
import MenuItem from '@mui/material/MenuItem'
// Type Imports
import type { ProductType } from '@/types/apps/ecommerceTypes'
// Component Imports
import CustomTextField from '@core/components/mui/TextField'
import { Product } from '../../../../types/services/product'
type ProductStockType = { [key: string]: boolean }
// Vars
const productStockObj: ProductStockType = {
'In Stock': true,
'Out of Stock': false
}
const TableFilters = ({ setData, productData }: { setData: (data: Product[]) => void; productData?: Product[] }) => {
// States
const [category, setCategory] = useState<Product['category_id']>('')
const [stock, setStock] = useState('')
const [status, setStatus] = useState<Product['name']>('')
useEffect(
() => {
const filteredData = productData?.filter(product => {
if (category && product.category_id !== category) return false
if (stock && product.name !== stock) return false
if (status && product.name !== status) return false
return true
})
setData(filteredData ?? [])
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[category, stock, status, productData]
)
return (
<CardContent>
<Grid container spacing={6}>
<Grid size={{ xs: 12, sm: 4 }}>
<CustomTextField
select
fullWidth
id='select-status'
value={status}
onChange={e => setStatus(e.target.value)}
slotProps={{
select: { displayEmpty: true }
}}
>
<MenuItem value=''>Select Status</MenuItem>
<MenuItem value='Scheduled'>Scheduled</MenuItem>
<MenuItem value='Published'>Publish</MenuItem>
<MenuItem value='Inactive'>Inactive</MenuItem>
</CustomTextField>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<CustomTextField
select
fullWidth
id='select-category'
value={category}
onChange={e => setCategory(e.target.value)}
slotProps={{
select: { displayEmpty: true }
}}
>
<MenuItem value=''>Select Category</MenuItem>
<MenuItem value='Accessories'>Accessories</MenuItem>
<MenuItem value='Home Decor'>Home Decor</MenuItem>
<MenuItem value='Electronics'>Electronics</MenuItem>
<MenuItem value='Shoes'>Shoes</MenuItem>
<MenuItem value='Office'>Office</MenuItem>
<MenuItem value='Games'>Games</MenuItem>
</CustomTextField>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<CustomTextField
select
fullWidth
id='select-stock'
value={stock}
onChange={e => setStock(e.target.value as string)}
slotProps={{
select: { displayEmpty: true }
}}
>
<MenuItem value=''>Select Stock</MenuItem>
<MenuItem value='In Stock'>In Stock</MenuItem>
<MenuItem value='Out of Stock'>Out of Stock</MenuItem>
</CustomTextField>
</Grid>
</Grid>
</CardContent>
)
}
export default TableFilters