Compare commits

...

16 Commits
efril ... main

Author SHA1 Message Date
efrilm
5f65211432 base url 2025-10-09 20:02:49 +07:00
efrilm
ead773982a update report 2025-10-09 20:02:32 +07:00
efrilm
aae1d49721 update report 2025-10-09 19:10:45 +07:00
efrilm
347d00271b base url 2025-10-07 14:35:11 +07:00
efrilm
ac2fc4e920 category order 2025-10-07 00:01:53 +07:00
efrilm
e551ae7feb update report 2025-10-06 17:05:11 +07:00
efrilm
d059f31eaa update report 2025-10-04 17:17:50 +07:00
efrilm
f1cf351ca4 Format Currency 2025-09-27 18:36:47 +07:00
Aditya Siregar
801fcdda2f Update baseUrl 2025-09-25 20:37:47 +07:00
Aditya Siregar
4ba5b9def3 Update Report 2025-09-25 20:31:33 +07:00
116f6b8009 Merge pull request 'efril' (#18) from efril into main
Reviewed-on: #18
2025-09-24 14:55:52 +00:00
8672f01261 Merge pull request 'fix: report page' (#17) from efril into main
Reviewed-on: #17
2025-09-23 08:32:13 +00:00
bd14adde23 Merge pull request 'base url' (#16) from efril into main
Reviewed-on: #16
2025-09-21 13:38:26 +00:00
35b1426384 Merge pull request 'efril' (#15) from efril into main
Reviewed-on: #15
2025-09-20 11:04:26 +00:00
9806400308 Merge pull request 'api' (#14) from efril into main
Reviewed-on: #14
2025-09-18 09:32:44 +00:00
f98b61e527 Merge pull request 'efril' (#13) from efril into main
Reviewed-on: #13
2025-09-18 04:01:32 +00:00
13 changed files with 725 additions and 455 deletions

43
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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}
/> />

View File

@ -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)}

View File

@ -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
} }
}) })
} }

View File

@ -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

View File

@ -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
} }

View File

@ -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'

View File

@ -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(
onSuccess: () => { { id: data.id, payload: formData },
handleReset() {
onSuccess: () => {
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'

View File

@ -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,18 +319,18 @@ const ProductCategoryTable = () => {
</Button> </Button>
</div> </div>
</div> </div>
<div className='overflow-x-auto'> <div className='overflow-x-auto relative'>
{isLoading ? ( {isLoading ? (
<Loading /> <Loading />
) : ( ) : (
<table className={tableStyles.table}> <DragDropContext onDragEnd={handleDragEnd}>
<thead> <table className={tableStyles.table}>
{table.getHeaderGroups().map(headerGroup => ( <thead>
<tr key={headerGroup.id}> {table.getHeaderGroups().map(headerGroup => (
{headerGroup.headers.map(header => ( <tr key={headerGroup.id}>
<th key={header.id}> {headerGroup.headers.map(header => (
{header.isPlaceholder ? null : ( <th key={header.id}>
<> {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'>
{table.getFilteredRowModel().rows.length === 0 ? ( {provided => (
<tbody> <tbody {...provided.droppableProps} ref={provided.innerRef}>
<tr> {table.getFilteredRowModel().rows.length === 0 ? (
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'> <tr>
No data available <td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
</td> No data available
</tr> </td>
</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> </tr>
) ) : (
})} table
</tbody> .getRowModel()
)} .rows.slice(0, table.getState().pagination.pageSize)
</table> .map((row, index) => {
const categoryId = row.original.id
return (
<Draggable key={categoryId} draggableId={categoryId} index={index}>
{(provided, snapshot) => (
<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>
)}
</Draggable>
)
})
)}
{provided.placeholder}
</tbody>
)}
</Droppable>
</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>

View File

@ -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}>

View File

@ -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