diff --git a/package-lock.json b/package-lock.json index 693fcc2..4275235 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@fullcalendar/list": "6.1.15", "@fullcalendar/react": "6.1.15", "@fullcalendar/timegrid": "6.1.15", + "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "3.9.1", "@iconify/react": "^6.0.1", "@mui/lab": "6.0.0-beta.19", @@ -1228,6 +1229,23 @@ "@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": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", @@ -4974,6 +4992,15 @@ "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": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", @@ -9958,6 +9985,12 @@ "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": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", diff --git a/package.json b/package.json index ab00d67..c958205 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@fullcalendar/list": "6.1.15", "@fullcalendar/react": "6.1.15", "@fullcalendar/timegrid": "6.1.15", + "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "3.9.1", "@iconify/react": "^6.0.1", "@mui/lab": "6.0.0-beta.19", diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx index 210223e..be669f6 100644 --- a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx @@ -420,7 +420,13 @@ const DailyPOSReport = () => {
{Object.keys(groupedProducts) - .sort() + .sort((a, b) => { + const productsA = groupedProducts[a] + const productsB = groupedProducts[b] + const orderA = productsA[0]?.category_order ?? 999 + const orderB = productsB[0]?.category_order ?? 999 + return orderA - orderB + }) .map(categoryName => { const categoryProducts = groupedProducts[categoryName] const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0) diff --git a/src/services/api.ts b/src/services/api.ts index 2f7d1d8..5890323 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -6,8 +6,8 @@ const getToken = () => { } export const api = axios.create({ - baseURL: 'https://api-pos.apskel.id/api/v1', - // baseURL: 'http://127.0.0.1:4000/api/v1', + // baseURL: 'https://api-pos.apskel.id/api/v1', + baseURL: 'http://127.0.0.1:4000/api/v1', headers: { 'Content-Type': 'application/json' }, diff --git a/src/services/queries/categories.ts b/src/services/queries/categories.ts index ea0a49d..c0d6eb0 100644 --- a/src/services/queries/categories.ts +++ b/src/services/queries/categories.ts @@ -9,7 +9,7 @@ interface CategoriesQueryParams { } export function useCategories(params: CategoriesQueryParams = {}) { - const { page = 1, limit = 10, search = '', ...filters } = params + const { page = 1, limit = 50, search = '', ...filters } = params return useQuery({ queryKey: ['categories', { page, limit, search, ...filters }], @@ -30,7 +30,9 @@ export function useCategories(params: CategoriesQueryParams = {}) { }) const res = await api.get(`/categories?${queryParams.toString()}`) - return res.data.data + const data = res.data.data + + return data } }) } diff --git a/src/types/services/analytic.ts b/src/types/services/analytic.ts index 6b49b0e..3ee6997 100644 --- a/src/types/services/analytic.ts +++ b/src/types/services/analytic.ts @@ -33,6 +33,7 @@ export interface ProductData { product_name: string category_id: string category_name: string + category_order: number quantity_sold: number revenue: number average_price: number diff --git a/src/types/services/category.ts b/src/types/services/category.ts index d42b222..4480eab 100644 --- a/src/types/services/category.ts +++ b/src/types/services/category.ts @@ -1,25 +1,26 @@ export interface Category { - id: string; - organization_id: string; - name: string; - description: string | null; - business_type: string; - metadata: Record; - created_at: string; - updated_at: string; + id: string + organization_id: string + name: string + description: string | null + business_type: string + order: number + metadata: Record + created_at: string + updated_at: string } export interface Categories { - categories: Category[]; - total_count: number; - page: number; - limit: number; - total_pages: number; + categories: Category[] + total_count: number + page: number + limit: number + total_pages: number } - export interface CategoryRequest { - name: string; - description: string | null; - business_type: string; + name: string + description: string | null + business_type: string + order: number } diff --git a/src/views/apps/ecommerce/products/category/AddCategoryDrawer.tsx b/src/views/apps/ecommerce/products/category/AddCategoryDrawer.tsx index b03d0b3..cfc149e 100644 --- a/src/views/apps/ecommerce/products/category/AddCategoryDrawer.tsx +++ b/src/views/apps/ecommerce/products/category/AddCategoryDrawer.tsx @@ -33,7 +33,8 @@ const AddCategoryDrawer = (props: Props) => { const [formData, setFormData] = useState({ name: '', description: '', - business_type: '' + business_type: '', + order: 0 }) // 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 const handleReset = () => { handleClose() setFormData({ name: '', description: '', - business_type: '' + business_type: '', + order: 0 }) } @@ -99,6 +108,15 @@ const AddCategoryDrawer = (props: Props) => { > Restaurant + { const [formData, setFormData] = useState({ name: '', description: '', - business_type: '' + business_type: '', + order: 0 }) useEffect(() => { @@ -42,7 +43,8 @@ const EditCategoryDrawer = (props: Props) => { setFormData({ name: data.name, description: data.description, - business_type: data.business_type + business_type: data.business_type, + order: data.order }) } }, [data]) @@ -51,11 +53,14 @@ const EditCategoryDrawer = (props: Props) => { const handleFormSubmit = (e: any) => { e.preventDefault() - updateCategory({ id: data.id, payload: formData }, { - onSuccess: () => { - handleReset() + updateCategory( + { id: data.id, payload: formData }, + { + onSuccess: () => { + handleReset() + } } - }) + ) } 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 const handleReset = () => { handleClose() setFormData({ name: '', description: '', - business_type: '' + business_type: '', + order: 0 }) } @@ -110,6 +123,15 @@ const EditCategoryDrawer = (props: Props) => { > Restaurant + @@ -51,15 +54,8 @@ type CategoryWithActionsType = Category & { } const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { - // Rank the item const itemRank = rankItem(row.getValue(columnId), value) - - // Store the itemRank info - addMeta({ - itemRank - }) - - // Return if the item should be filtered in/out + addMeta({ itemRank }) return itemRank.passed } @@ -73,7 +69,6 @@ const DebouncedInput = ({ onChange: (value: string | number) => void debounce?: number } & Omit) => { - // States const [value, setValue] = useState(initialValue) useEffect(() => { @@ -86,47 +81,51 @@ const DebouncedInput = ({ }, debounce) return () => clearTimeout(timeout) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]) return setValue(e.target.value)} /> } -// Column Definitions const columnHelper = createColumnHelper() const ProductCategoryTable = () => { - // States const [addCategoryOpen, setAddCategoryOpen] = useState(false) const [editCategoryOpen, setEditCategoryOpen] = useState(false) const [rowSelection, setRowSelection] = useState({}) const [currentPage, setCurrentPage] = useState(1) - const [pageSize, setPageSize] = useState(10) + const [pageSize, setPageSize] = useState(50) const [categoryId, setCategoryId] = useState('') const [openConfirm, setOpenConfirm] = useState(false) const [currentCategory, setCurrentCategory] = useState() const [search, setSearch] = useState('') + const [localCategories, setLocalCategories] = useState([]) - // Fetch products with pagination and search const { data, isLoading, error, isFetching } = useCategories({ page: currentPage, limit: pageSize }) const { mutate: deleteCategory, isPending: isDeleting } = useCategoriesMutation().deleteCategory + const { mutate: updateCategory } = useCategoriesMutation().updateCategory const categories = data?.categories ?? [] 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) => { setCurrentPage(newPage) }, []) - // Handle page size change const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { - const newPageSize = parseInt(event.target.value, 10) + const newPageSize = parseInt(event.target.value, 50) setPageSize(newPageSize) - setCurrentPage(1) // Reset to first page + setCurrentPage(1) }, []) 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[]>( () => [ + { + id: 'drag', + header: '', + cell: () => ( +
+ +
+ ), + size: 50, + enableSorting: false + }, { id: 'select', header: ({ table }) => ( @@ -163,7 +210,6 @@ const ProductCategoryTable = () => { header: 'Name', cell: ({ row }) => (
- {/* */}
{row.original.name} @@ -173,7 +219,7 @@ const ProductCategoryTable = () => { ) }), columnHelper.accessor('description', { - header: 'Decription', + header: 'Description', cell: ({ row }) => {row.original.description || '-'} }), columnHelper.accessor('business_type', { @@ -219,12 +265,11 @@ const ProductCategoryTable = () => { enableSorting: false }) ], - // eslint-disable-next-line react-hooks/exhaustive-deps [data] ) const table = useReactTable({ - data: categories as Category[], + data: localCategories as Category[], columns, filterFns: { fuzzy: fuzzyFilter @@ -232,14 +277,13 @@ const ProductCategoryTable = () => { state: { rowSelection, pagination: { - pageIndex: currentPage, // <= penting! + pageIndex: currentPage, pageSize } }, - enableRowSelection: true, //enable row selection for all rows + enableRowSelection: true, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), - // Disable client-side pagination since we're handling it server-side manualPagination: true, pageCount: Math.ceil(totalCount / pageSize) }) @@ -261,9 +305,9 @@ const ProductCategoryTable = () => { onChange={handlePageSizeChange} className='flex-auto max-sm:is-full sm:is-[70px]' > - 10 - 15 - 25 + 50 + 75 + 100
-
+
{isLoading ? ( ) : ( - - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - + {row.getVisibleCells().map((cell, cellIndex) => ( + + ))} + + )} + + ) + }) + )} + {provided.placeholder} + + )} + +
- {header.isPlaceholder ? null : ( - <> + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + - ))} - - ))} - - {table.getFilteredRowModel().rows.length === 0 ? ( - - - - - - ) : ( - - {table - .getRowModel() - .rows.slice(0, table.getState().pagination.pageSize) - .map(row => { - return ( - - {row.getVisibleCells().map(cell => ( - - ))} + )} + + ))} + + ))} + + + {provided => ( + + {table.getFilteredRowModel().rows.length === 0 ? ( + + - ) - })} - - )} -
+ {header.isPlaceholder ? null : (
{ desc: }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
- - )} -
- No data available -
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ No data available +
+ ) : ( + table + .getRowModel() + .rows.slice(0, table.getState().pagination.pageSize) + .map((row, index) => { + const categoryId = row.original.id + return ( + + {(provided, snapshot) => ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ )} {isFetching && !isLoading && ( @@ -365,7 +429,7 @@ const ProductCategoryTable = () => { page={currentPage} onPageChange={handlePageChange} onRowsPerPageChange={handlePageSizeChange} - rowsPerPageOptions={[10, 25, 50]} + rowsPerPageOptions={[50, 75, 100]} disabled={isLoading} />