Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f65211432 | ||
|
|
ead773982a | ||
|
|
aae1d49721 | ||
|
|
347d00271b | ||
|
|
ac2fc4e920 | ||
|
|
e551ae7feb | ||
|
|
d059f31eaa | ||
|
|
f1cf351ca4 | ||
|
|
801fcdda2f | ||
|
|
4ba5b9def3 | ||
| 116f6b8009 | |||
| 8672f01261 | |||
| bd14adde23 | |||
| 35b1426384 | |||
| 9806400308 | |||
| f98b61e527 |
43
package-lock.json
generated
43
package-lock.json
generated
@ -24,6 +24,7 @@
|
|||||||
"@fullcalendar/list": "6.1.15",
|
"@fullcalendar/list": "6.1.15",
|
||||||
"@fullcalendar/react": "6.1.15",
|
"@fullcalendar/react": "6.1.15",
|
||||||
"@fullcalendar/timegrid": "6.1.15",
|
"@fullcalendar/timegrid": "6.1.15",
|
||||||
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@hookform/resolvers": "3.9.1",
|
"@hookform/resolvers": "3.9.1",
|
||||||
"@iconify/react": "^6.0.1",
|
"@iconify/react": "^6.0.1",
|
||||||
"@mui/lab": "6.0.0-beta.19",
|
"@mui/lab": "6.0.0-beta.19",
|
||||||
@ -54,6 +55,7 @@
|
|||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
|
"jspdf-autotable": "^5.0.2",
|
||||||
"keen-slider": "6.8.6",
|
"keen-slider": "6.8.6",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"mapbox-gl": "3.9.0",
|
"mapbox-gl": "3.9.0",
|
||||||
@ -1228,6 +1230,23 @@
|
|||||||
"@fullcalendar/core": "~6.1.15"
|
"@fullcalendar/core": "~6.1.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hello-pangea/dnd": {
|
||||||
|
"version": "18.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
|
||||||
|
"integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.7",
|
||||||
|
"css-box-model": "^1.2.1",
|
||||||
|
"raf-schd": "^4.0.3",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"redux": "^5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@hookform/resolvers": {
|
"node_modules/@hookform/resolvers": {
|
||||||
"version": "3.9.1",
|
"version": "3.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",
|
||||||
@ -4974,6 +4993,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-box-model": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tiny-invariant": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-functions-list": {
|
"node_modules/css-functions-list": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz",
|
||||||
@ -8268,6 +8296,15 @@
|
|||||||
"html2canvas": "^1.0.0-rc.5"
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf-autotable": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"jspdf": "^2 || ^3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsx-ast-utils": {
|
"node_modules/jsx-ast-utils": {
|
||||||
"version": "3.3.5",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||||
@ -9958,6 +9995,12 @@
|
|||||||
"performance-now": "^2.1.0"
|
"performance-now": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
"@fullcalendar/list": "6.1.15",
|
"@fullcalendar/list": "6.1.15",
|
||||||
"@fullcalendar/react": "6.1.15",
|
"@fullcalendar/react": "6.1.15",
|
||||||
"@fullcalendar/timegrid": "6.1.15",
|
"@fullcalendar/timegrid": "6.1.15",
|
||||||
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@hookform/resolvers": "3.9.1",
|
"@hookform/resolvers": "3.9.1",
|
||||||
"@iconify/react": "^6.0.1",
|
"@iconify/react": "^6.0.1",
|
||||||
"@mui/lab": "6.0.0-beta.19",
|
"@mui/lab": "6.0.0-beta.19",
|
||||||
@ -60,6 +61,7 @@
|
|||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
|
"jspdf-autotable": "^5.0.2",
|
||||||
"keen-slider": "6.8.6",
|
"keen-slider": "6.8.6",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"mapbox-gl": "3.9.0",
|
"mapbox-gl": "3.9.0",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,12 @@ import { TextField, Typography, useTheme } from '@mui/material'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Loading from '../../../../../../components/layout/shared/Loading'
|
import Loading from '../../../../../../components/layout/shared/Loading'
|
||||||
import { useDashboardAnalytics } from '../../../../../../services/queries/analytics'
|
import { useDashboardAnalytics } from '../../../../../../services/queries/analytics'
|
||||||
import { formatDateDDMMYYYY, formatForInputDate, formatShortCurrency } from '../../../../../../utils/transform'
|
import {
|
||||||
|
formatCurrency,
|
||||||
|
formatDateDDMMYYYY,
|
||||||
|
formatForInputDate,
|
||||||
|
formatShortCurrency
|
||||||
|
} from '../../../../../../utils/transform'
|
||||||
import OrdersReport from '../../../../../../views/dashboards/orders/OrdersReport'
|
import OrdersReport from '../../../../../../views/dashboards/orders/OrdersReport'
|
||||||
import PaymentMethodReport from '../../../../../../views/dashboards/payment-methods/PaymentMethodReport'
|
import PaymentMethodReport from '../../../../../../views/dashboards/payment-methods/PaymentMethodReport'
|
||||||
import ProductSales from '../../../../../../views/dashboards/products/ProductSales'
|
import ProductSales from '../../../../../../views/dashboards/products/ProductSales'
|
||||||
@ -115,14 +120,14 @@ const DashboardOverview = () => {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
iconClass='tabler-cash'
|
iconClass='tabler-cash'
|
||||||
title='Total Sales'
|
title='Total Sales'
|
||||||
value={formatShortCurrency(salesData.overview.total_sales)}
|
value={formatCurrency(salesData.overview.total_sales)}
|
||||||
bgColor='bg-green-500'
|
bgColor='bg-green-500'
|
||||||
isCurrency={true}
|
isCurrency={true}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
iconClass='tabler-trending-up'
|
iconClass='tabler-trending-up'
|
||||||
title='Average Order Value'
|
title='Average Order Value'
|
||||||
value={formatShortCurrency(salesData.overview.average_order_value)}
|
value={formatCurrency(salesData.overview.average_order_value)}
|
||||||
bgColor='bg-purple-500'
|
bgColor='bg-purple-500'
|
||||||
isCurrency={true}
|
isCurrency={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -162,21 +162,21 @@ const DashboardProfitloss = () => {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
iconClass='tabler-currency-dollar'
|
iconClass='tabler-currency-dollar'
|
||||||
title='Total Revenue'
|
title='Total Revenue'
|
||||||
value={formatShortCurrency(profitData.summary.total_revenue)}
|
value={formatCurrency(profitData.summary.total_revenue)}
|
||||||
bgColor='bg-green-500'
|
bgColor='bg-green-500'
|
||||||
isCurrency={true}
|
isCurrency={true}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
iconClass='tabler-receipt'
|
iconClass='tabler-receipt'
|
||||||
title='Total Cost'
|
title='Total Cost'
|
||||||
value={formatShortCurrency(profitData.summary.total_cost)}
|
value={formatCurrency(profitData.summary.total_cost)}
|
||||||
bgColor='bg-red-500'
|
bgColor='bg-red-500'
|
||||||
isCurrency={true}
|
isCurrency={true}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
iconClass='tabler-trending-up'
|
iconClass='tabler-trending-up'
|
||||||
title='Gross Profit'
|
title='Gross Profit'
|
||||||
value={formatShortCurrency(profitData.summary.gross_profit)}
|
value={formatCurrency(profitData.summary.gross_profit)}
|
||||||
subtitle={`Margin: ${formatPercentage(profitData.summary.gross_profit_margin)}`}
|
subtitle={`Margin: ${formatPercentage(profitData.summary.gross_profit_margin)}`}
|
||||||
bgColor='bg-blue-500'
|
bgColor='bg-blue-500'
|
||||||
isNegative={profitData.summary.gross_profit < 0}
|
isNegative={profitData.summary.gross_profit < 0}
|
||||||
@ -186,7 +186,7 @@ const DashboardProfitloss = () => {
|
|||||||
iconClass='tabler-percentage'
|
iconClass='tabler-percentage'
|
||||||
title='Profitability Ratio'
|
title='Profitability Ratio'
|
||||||
value={formatPercentage(profitData.summary.profitability_ratio)}
|
value={formatPercentage(profitData.summary.profitability_ratio)}
|
||||||
subtitle={`Avg Profit: ${formatShortCurrency(profitData.summary.average_profit)}`}
|
subtitle={`Avg Profit: ${formatCurrency(profitData.summary.average_profit)}`}
|
||||||
bgColor='bg-purple-500'
|
bgColor='bg-purple-500'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -199,7 +199,7 @@ const DashboardProfitloss = () => {
|
|||||||
<h3 className='text-lg font-semibold text-gray-900'>Net Profit</h3>
|
<h3 className='text-lg font-semibold text-gray-900'>Net Profit</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-3xl font-bold text-green-600 mb-2'>
|
<p className='text-3xl font-bold text-green-600 mb-2'>
|
||||||
Rp {formatShortCurrency(profitData.summary.net_profit)}
|
{formatCurrency(profitData.summary.net_profit)}
|
||||||
</p>
|
</p>
|
||||||
<p className='text-sm text-gray-600'>
|
<p className='text-sm text-gray-600'>
|
||||||
Margin: {formatPercentage(profitData.summary.net_profit_margin)}
|
Margin: {formatPercentage(profitData.summary.net_profit_margin)}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ interface CategoriesQueryParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useCategories(params: CategoriesQueryParams = {}) {
|
export function useCategories(params: CategoriesQueryParams = {}) {
|
||||||
const { page = 1, limit = 10, search = '', ...filters } = params
|
const { page = 1, limit = 50, search = '', ...filters } = params
|
||||||
|
|
||||||
return useQuery<Categories>({
|
return useQuery<Categories>({
|
||||||
queryKey: ['categories', { page, limit, search, ...filters }],
|
queryKey: ['categories', { page, limit, search, ...filters }],
|
||||||
@ -30,7 +30,9 @@ export function useCategories(params: CategoriesQueryParams = {}) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const res = await api.get(`/categories?${queryParams.toString()}`)
|
const res = await api.get(`/categories?${queryParams.toString()}`)
|
||||||
return res.data.data
|
const data = res.data.data
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,8 +31,10 @@ export interface SalesReport {
|
|||||||
export interface ProductData {
|
export interface ProductData {
|
||||||
product_id: string
|
product_id: string
|
||||||
product_name: string
|
product_name: string
|
||||||
|
product_sku: string
|
||||||
category_id: string
|
category_id: string
|
||||||
category_name: string
|
category_name: string
|
||||||
|
category_order: number
|
||||||
quantity_sold: number
|
quantity_sold: number
|
||||||
revenue: number
|
revenue: number
|
||||||
average_price: number
|
average_price: number
|
||||||
|
|||||||
@ -1,25 +1,26 @@
|
|||||||
export interface Category {
|
export interface Category {
|
||||||
id: string;
|
id: string
|
||||||
organization_id: string;
|
organization_id: string
|
||||||
name: string;
|
name: string
|
||||||
description: string | null;
|
description: string | null
|
||||||
business_type: string;
|
business_type: string
|
||||||
metadata: Record<string, any>;
|
order: number
|
||||||
created_at: string;
|
metadata: Record<string, any>
|
||||||
updated_at: string;
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Categories {
|
export interface Categories {
|
||||||
categories: Category[];
|
categories: Category[]
|
||||||
total_count: number;
|
total_count: number
|
||||||
page: number;
|
page: number
|
||||||
limit: number;
|
limit: number
|
||||||
total_pages: number;
|
total_pages: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface CategoryRequest {
|
export interface CategoryRequest {
|
||||||
name: string;
|
name: string
|
||||||
description: string | null;
|
description: string | null
|
||||||
business_type: string;
|
business_type: string
|
||||||
|
order: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,8 @@ const AddCategoryDrawer = (props: Props) => {
|
|||||||
const [formData, setFormData] = useState<CategoryRequest>({
|
const [formData, setFormData] = useState<CategoryRequest>({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
business_type: ''
|
business_type: '',
|
||||||
|
order: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle Form Submit
|
// Handle Form Submit
|
||||||
@ -54,13 +55,21 @@ const AddCategoryDrawer = (props: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOrderChange = (e: any) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
order: parseInt(e.target.value) || 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Form Reset
|
// Handle Form Reset
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
handleClose()
|
handleClose()
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
business_type: ''
|
business_type: '',
|
||||||
|
order: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +108,15 @@ const AddCategoryDrawer = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<MenuItem value='restaurant'>Restaurant</MenuItem>
|
<MenuItem value='restaurant'>Restaurant</MenuItem>
|
||||||
</CustomTextField>
|
</CustomTextField>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Order'
|
||||||
|
name='order'
|
||||||
|
type='number'
|
||||||
|
value={formData.order}
|
||||||
|
onChange={handleOrderChange}
|
||||||
|
placeholder='0'
|
||||||
|
/>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Description'
|
label='Description'
|
||||||
|
|||||||
@ -34,7 +34,8 @@ const EditCategoryDrawer = (props: Props) => {
|
|||||||
const [formData, setFormData] = useState<CategoryRequest>({
|
const [formData, setFormData] = useState<CategoryRequest>({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
business_type: ''
|
business_type: '',
|
||||||
|
order: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -42,7 +43,8 @@ const EditCategoryDrawer = (props: Props) => {
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
business_type: data.business_type
|
business_type: data.business_type,
|
||||||
|
order: data.order
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data])
|
||||||
@ -51,11 +53,14 @@ const EditCategoryDrawer = (props: Props) => {
|
|||||||
const handleFormSubmit = (e: any) => {
|
const handleFormSubmit = (e: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
updateCategory({ id: data.id, payload: formData }, {
|
updateCategory(
|
||||||
|
{ id: data.id, payload: formData },
|
||||||
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
handleReset()
|
handleReset()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInputChange = (e: any) => {
|
const handleInputChange = (e: any) => {
|
||||||
@ -65,13 +70,21 @@ const EditCategoryDrawer = (props: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOrderChange = (e: any) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
order: parseInt(e.target.value) || 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Form Reset
|
// Handle Form Reset
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
handleClose()
|
handleClose()
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
business_type: ''
|
business_type: '',
|
||||||
|
order: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +123,15 @@ const EditCategoryDrawer = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<MenuItem value='restaurant'>Restaurant</MenuItem>
|
<MenuItem value='restaurant'>Restaurant</MenuItem>
|
||||||
</CustomTextField>
|
</CustomTextField>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Order'
|
||||||
|
name='order'
|
||||||
|
type='number'
|
||||||
|
value={formData.order}
|
||||||
|
onChange={handleOrderChange}
|
||||||
|
placeholder='0'
|
||||||
|
/>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Description'
|
label='Description'
|
||||||
|
|||||||
@ -12,6 +12,7 @@ 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 Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { Box, CircularProgress } from '@mui/material'
|
||||||
|
|
||||||
// Third-party Imports
|
// Third-party Imports
|
||||||
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
||||||
@ -19,24 +20,26 @@ import { rankItem } from '@tanstack/match-sorter-utils'
|
|||||||
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
|
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
|
||||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd'
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import TablePaginationComponent from '@components/TablePaginationComponent'
|
import TablePaginationComponent from '@components/TablePaginationComponent'
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
import CustomTextField from '@core/components/mui/TextField'
|
||||||
import OptionMenu from '@core/components/option-menu'
|
import OptionMenu from '@core/components/option-menu'
|
||||||
import AddCategoryDrawer from './AddCategoryDrawer'
|
import AddCategoryDrawer from './AddCategoryDrawer'
|
||||||
|
import EditCategoryDrawer from './EditCategoryDrawer'
|
||||||
// Style Imports
|
|
||||||
import tableStyles from '@core/styles/table.module.css'
|
|
||||||
import { Box, CircularProgress } from '@mui/material'
|
|
||||||
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
|
import ConfirmDeleteDialog from '../../../../../components/dialogs/confirm-delete'
|
||||||
import Loading from '../../../../../components/layout/shared/Loading'
|
import Loading from '../../../../../components/layout/shared/Loading'
|
||||||
|
|
||||||
|
// Services Imports
|
||||||
import { useCategoriesMutation } from '../../../../../services/mutations/categories'
|
import { useCategoriesMutation } from '../../../../../services/mutations/categories'
|
||||||
import { useCategories } from '../../../../../services/queries/categories'
|
import { useCategories } from '../../../../../services/queries/categories'
|
||||||
import { Category } from '../../../../../types/services/category'
|
import { Category } from '../../../../../types/services/category'
|
||||||
import EditCategoryDrawer from './EditCategoryDrawer'
|
|
||||||
import { formatDate } from '../../../../../utils/transform'
|
import { formatDate } from '../../../../../utils/transform'
|
||||||
|
|
||||||
|
// Style Imports
|
||||||
|
import tableStyles from '@core/styles/table.module.css'
|
||||||
|
|
||||||
declare module '@tanstack/table-core' {
|
declare module '@tanstack/table-core' {
|
||||||
interface FilterFns {
|
interface FilterFns {
|
||||||
fuzzy: FilterFn<unknown>
|
fuzzy: FilterFn<unknown>
|
||||||
@ -51,15 +54,8 @@ type CategoryWithActionsType = Category & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||||
// Rank the item
|
|
||||||
const itemRank = rankItem(row.getValue(columnId), value)
|
const itemRank = rankItem(row.getValue(columnId), value)
|
||||||
|
addMeta({ itemRank })
|
||||||
// Store the itemRank info
|
|
||||||
addMeta({
|
|
||||||
itemRank
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return if the item should be filtered in/out
|
|
||||||
return itemRank.passed
|
return itemRank.passed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +69,6 @@ const DebouncedInput = ({
|
|||||||
onChange: (value: string | number) => void
|
onChange: (value: string | number) => void
|
||||||
debounce?: number
|
debounce?: number
|
||||||
} & Omit<TextFieldProps, 'onChange'>) => {
|
} & Omit<TextFieldProps, 'onChange'>) => {
|
||||||
// States
|
|
||||||
const [value, setValue] = useState(initialValue)
|
const [value, setValue] = useState(initialValue)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -86,47 +81,51 @@ const DebouncedInput = ({
|
|||||||
}, debounce)
|
}, debounce)
|
||||||
|
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// Column Definitions
|
|
||||||
const columnHelper = createColumnHelper<CategoryWithActionsType>()
|
const columnHelper = createColumnHelper<CategoryWithActionsType>()
|
||||||
|
|
||||||
const ProductCategoryTable = () => {
|
const ProductCategoryTable = () => {
|
||||||
// States
|
|
||||||
const [addCategoryOpen, setAddCategoryOpen] = useState(false)
|
const [addCategoryOpen, setAddCategoryOpen] = useState(false)
|
||||||
const [editCategoryOpen, setEditCategoryOpen] = 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(50)
|
||||||
const [categoryId, setCategoryId] = useState('')
|
const [categoryId, setCategoryId] = useState('')
|
||||||
const [openConfirm, setOpenConfirm] = useState(false)
|
const [openConfirm, setOpenConfirm] = useState(false)
|
||||||
const [currentCategory, setCurrentCategory] = useState<Category>()
|
const [currentCategory, setCurrentCategory] = useState<Category>()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [localCategories, setLocalCategories] = useState<Category[]>([])
|
||||||
|
|
||||||
// Fetch products with pagination and search
|
|
||||||
const { data, isLoading, error, isFetching } = useCategories({
|
const { data, isLoading, error, isFetching } = useCategories({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
limit: pageSize
|
limit: pageSize
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutate: deleteCategory, isPending: isDeleting } = useCategoriesMutation().deleteCategory
|
const { mutate: deleteCategory, isPending: isDeleting } = useCategoriesMutation().deleteCategory
|
||||||
|
const { mutate: updateCategory } = useCategoriesMutation().updateCategory
|
||||||
|
|
||||||
const categories = data?.categories ?? []
|
const categories = data?.categories ?? []
|
||||||
const totalCount = data?.total_count ?? 0
|
const totalCount = data?.total_count ?? 0
|
||||||
|
|
||||||
|
// Update local categories when data changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (categories.length > 0) {
|
||||||
|
setLocalCategories(categories)
|
||||||
|
}
|
||||||
|
}, [categories])
|
||||||
|
|
||||||
const handlePageChange = useCallback((event: unknown, newPage: number) => {
|
const handlePageChange = useCallback((event: unknown, newPage: number) => {
|
||||||
setCurrentPage(newPage)
|
setCurrentPage(newPage)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Handle page size change
|
|
||||||
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newPageSize = parseInt(event.target.value, 10)
|
const newPageSize = parseInt(event.target.value, 50)
|
||||||
setPageSize(newPageSize)
|
setPageSize(newPageSize)
|
||||||
setCurrentPage(1) // Reset to first page
|
setCurrentPage(1)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
@ -135,8 +134,56 @@ const ProductCategoryTable = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination) return
|
||||||
|
|
||||||
|
const items = Array.from(localCategories)
|
||||||
|
const [reorderedItem] = items.splice(result.source.index, 1)
|
||||||
|
items.splice(result.destination.index, 0, reorderedItem)
|
||||||
|
|
||||||
|
setLocalCategories(items)
|
||||||
|
|
||||||
|
// Update order for all affected items
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const newOrder = index + 1 // Order dimulai dari 1
|
||||||
|
|
||||||
|
// Only update if order changed
|
||||||
|
if (item.order !== newOrder) {
|
||||||
|
const updateData = {
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
business_type: item.business_type,
|
||||||
|
order: newOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCategory(
|
||||||
|
{ id: item.id, payload: updateData },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
console.log(`Category ${item.name} order updated to ${newOrder}`)
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
console.error(`Failed to update category ${item.name}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<CategoryWithActionsType, any>[]>(
|
const columns = useMemo<ColumnDef<CategoryWithActionsType, any>[]>(
|
||||||
() => [
|
() => [
|
||||||
|
{
|
||||||
|
id: 'drag',
|
||||||
|
header: '',
|
||||||
|
cell: () => (
|
||||||
|
<div className='cursor-move flex items-center justify-center'>
|
||||||
|
<i className='tabler-grip-vertical text-textSecondary text-xl' />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
size: 50,
|
||||||
|
enableSorting: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'select',
|
id: 'select',
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@ -163,7 +210,6 @@ const ProductCategoryTable = () => {
|
|||||||
header: 'Name',
|
header: 'Name',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className='flex items-center gap-3'>
|
<div className='flex items-center gap-3'>
|
||||||
{/* <img src={row.original.image} width={38} height={38} className='rounded bg-actionHover' /> */}
|
|
||||||
<div className='flex flex-col items-start'>
|
<div className='flex flex-col items-start'>
|
||||||
<Typography className='font-medium' color='text.primary'>
|
<Typography className='font-medium' color='text.primary'>
|
||||||
{row.original.name}
|
{row.original.name}
|
||||||
@ -173,7 +219,7 @@ const ProductCategoryTable = () => {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('description', {
|
columnHelper.accessor('description', {
|
||||||
header: 'Decription',
|
header: 'Description',
|
||||||
cell: ({ row }) => <Typography>{row.original.description || '-'}</Typography>
|
cell: ({ row }) => <Typography>{row.original.description || '-'}</Typography>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('business_type', {
|
columnHelper.accessor('business_type', {
|
||||||
@ -219,12 +265,11 @@ const ProductCategoryTable = () => {
|
|||||||
enableSorting: false
|
enableSorting: false
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[data]
|
[data]
|
||||||
)
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: categories as Category[],
|
data: localCategories as Category[],
|
||||||
columns,
|
columns,
|
||||||
filterFns: {
|
filterFns: {
|
||||||
fuzzy: fuzzyFilter
|
fuzzy: fuzzyFilter
|
||||||
@ -232,14 +277,13 @@ const ProductCategoryTable = () => {
|
|||||||
state: {
|
state: {
|
||||||
rowSelection,
|
rowSelection,
|
||||||
pagination: {
|
pagination: {
|
||||||
pageIndex: currentPage, // <= penting!
|
pageIndex: currentPage,
|
||||||
pageSize
|
pageSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enableRowSelection: true, //enable row selection for all rows
|
enableRowSelection: true,
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
// Disable client-side pagination since we're handling it server-side
|
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
pageCount: Math.ceil(totalCount / pageSize)
|
pageCount: Math.ceil(totalCount / pageSize)
|
||||||
})
|
})
|
||||||
@ -261,9 +305,9 @@ const ProductCategoryTable = () => {
|
|||||||
onChange={handlePageSizeChange}
|
onChange={handlePageSizeChange}
|
||||||
className='flex-auto max-sm:is-full sm:is-[70px]'
|
className='flex-auto max-sm:is-full sm:is-[70px]'
|
||||||
>
|
>
|
||||||
<MenuItem value='10'>10</MenuItem>
|
<MenuItem value='50'>50</MenuItem>
|
||||||
<MenuItem value='15'>15</MenuItem>
|
<MenuItem value='75'>75</MenuItem>
|
||||||
<MenuItem value='25'>25</MenuItem>
|
<MenuItem value='100'>100</MenuItem>
|
||||||
</CustomTextField>
|
</CustomTextField>
|
||||||
<Button
|
<Button
|
||||||
variant='contained'
|
variant='contained'
|
||||||
@ -275,10 +319,11 @@ const ProductCategoryTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto relative'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
<table className={tableStyles.table}>
|
<table className={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
@ -286,7 +331,6 @@ const ProductCategoryTable = () => {
|
|||||||
{headerGroup.headers.map(header => (
|
{headerGroup.headers.map(header => (
|
||||||
<th key={header.id}>
|
<th key={header.id}>
|
||||||
{header.isPlaceholder ? null : (
|
{header.isPlaceholder ? null : (
|
||||||
<>
|
|
||||||
<div
|
<div
|
||||||
className={classnames({
|
className={classnames({
|
||||||
'flex items-center': header.column.getIsSorted(),
|
'flex items-center': header.column.getIsSorted(),
|
||||||
@ -300,38 +344,58 @@ const ProductCategoryTable = () => {
|
|||||||
desc: <i className='tabler-chevron-down text-xl' />
|
desc: <i className='tabler-chevron-down text-xl' />
|
||||||
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
|
<Droppable droppableId='category-table'>
|
||||||
|
{provided => (
|
||||||
|
<tbody {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
{table.getFilteredRowModel().rows.length === 0 ? (
|
{table.getFilteredRowModel().rows.length === 0 ? (
|
||||||
<tbody>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||||
No data available
|
No data available
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
|
||||||
) : (
|
) : (
|
||||||
<tbody>
|
table
|
||||||
{table
|
|
||||||
.getRowModel()
|
.getRowModel()
|
||||||
.rows.slice(0, table.getState().pagination.pageSize)
|
.rows.slice(0, table.getState().pagination.pageSize)
|
||||||
.map(row => {
|
.map((row, index) => {
|
||||||
|
const categoryId = row.original.id
|
||||||
return (
|
return (
|
||||||
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
<Draggable key={categoryId} draggableId={categoryId} index={index}>
|
||||||
{row.getVisibleCells().map(cell => (
|
{(provided, snapshot) => (
|
||||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
<tr
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
className={classnames({
|
||||||
|
selected: row.getIsSelected()
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
backgroundColor: snapshot.isDragging ? '#f3f4f6' : 'transparent'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell, cellIndex) => (
|
||||||
|
<td key={cell.id} {...(cellIndex === 0 ? provided.dragHandleProps : {})}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
)
|
)
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
|
{provided.placeholder}
|
||||||
</tbody>
|
</tbody>
|
||||||
)}
|
)}
|
||||||
|
</Droppable>
|
||||||
</table>
|
</table>
|
||||||
|
</DragDropContext>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isFetching && !isLoading && (
|
{isFetching && !isLoading && (
|
||||||
@ -365,7 +429,7 @@ const ProductCategoryTable = () => {
|
|||||||
page={currentPage}
|
page={currentPage}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onRowsPerPageChange={handlePageSizeChange}
|
onRowsPerPageChange={handlePageSizeChange}
|
||||||
rowsPerPageOptions={[10, 25, 50]}
|
rowsPerPageOptions={[50, 75, 100]}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import classnames from 'classnames'
|
|||||||
import CustomAvatar, { CustomAvatarProps } from '../../../@core/components/mui/Avatar'
|
import CustomAvatar, { CustomAvatarProps } from '../../../@core/components/mui/Avatar'
|
||||||
import { ThemeColor } from '../../../@core/types'
|
import { ThemeColor } from '../../../@core/types'
|
||||||
import { Skeleton, Typography } from '@mui/material'
|
import { Skeleton, Typography } from '@mui/material'
|
||||||
import { formatShortCurrency } from '../../../utils/transform'
|
import { formatCurrency, formatShortCurrency } from '../../../utils/transform'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
@ -46,7 +46,7 @@ const DistributedBarChartOrder = ({
|
|||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='text.primary' variant='h4'>
|
<Typography color='text.primary' variant='h4'>
|
||||||
{isCurrency ? 'Rp ' + formatShortCurrency(value) : formatShortCurrency(value)}
|
{isCurrency ? formatCurrency(value) : formatShortCurrency(value)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<CustomAvatar variant='rounded' skin={avatarSkin} size={52} color={avatarColor}>
|
<CustomAvatar variant='rounded' skin={avatarSkin} size={52} color={avatarColor}>
|
||||||
|
|||||||
@ -38,8 +38,8 @@ const ReportHeader: FC<ReportHeaderProps> = ({
|
|||||||
<Box sx={{ p: theme.spacing(8, 8, 6) }}>
|
<Box sx={{ p: theme.spacing(8, 8, 6) }}>
|
||||||
<Box sx={{ textAlign: 'center' }}>
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant='h4'
|
variant='h1'
|
||||||
component='h3'
|
component='h1'
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#222222'
|
color: '#222222'
|
||||||
@ -49,7 +49,7 @@ const ReportHeader: FC<ReportHeaderProps> = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
{periode && (
|
{periode && (
|
||||||
<Typography
|
<Typography
|
||||||
variant='body2'
|
variant='h5'
|
||||||
sx={{
|
sx={{
|
||||||
color: '#222222',
|
color: '#222222',
|
||||||
mt: 2
|
mt: 2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user