first commit
This commit is contained in:
parent
3986fd953b
commit
4efeaa9dfe
40
package-lock.json
generated
40
package-lock.json
generated
@ -31,6 +31,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.522.0",
|
"lucide-react": "^0.522.0",
|
||||||
"mantine-datatable": "^1.7.17",
|
"mantine-datatable": "^1.7.17",
|
||||||
|
"nuqs": "^2.4.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.58.1",
|
"react-hook-form": "^7.58.1",
|
||||||
@ -16067,6 +16068,12 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mitt": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
@ -16241,6 +16248,39 @@
|
|||||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nuqs": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-BgtlYpvRwLYiJuWzxt34q2bXu/AIS66sLU1QePIMr2LWkb+XH0vKXdbLSgn9t6p7QKzwI7f38rX3Wl9llTXQ8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mitt": "^3.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/franky47"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@remix-run/react": ">=2",
|
||||||
|
"next": ">=14.2.0",
|
||||||
|
"react": ">=18.2.0 || ^19.0.0-0",
|
||||||
|
"react-router": "^6 || ^7",
|
||||||
|
"react-router-dom": "^6 || ^7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@remix-run/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-router": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-router-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nwsapi": {
|
"node_modules/nwsapi": {
|
||||||
"version": "2.2.13",
|
"version": "2.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz",
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.522.0",
|
"lucide-react": "^0.522.0",
|
||||||
"mantine-datatable": "^1.7.17",
|
"mantine-datatable": "^1.7.17",
|
||||||
|
"nuqs": "^2.4.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.58.1",
|
"react-hook-form": "^7.58.1",
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { IRootState } from '../../store';
|
|||||||
import { toggleSidebar } from '../../store/themeConfigSlice';
|
import { toggleSidebar } from '../../store/themeConfigSlice';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Setting from './Setting';
|
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import Portals from '../../components/Portals';
|
import Portals from '../../components/Portals';
|
||||||
|
import IconLoader from '../Icon/IconLoader';
|
||||||
|
|
||||||
const DefaultLayout = ({ children }: PropsWithChildren) => {
|
const DefaultLayout = ({ children }: PropsWithChildren) => {
|
||||||
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
|
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
|
||||||
@ -89,7 +89,13 @@ const DefaultLayout = ({ children }: PropsWithChildren) => {
|
|||||||
{/* END TOP NAVBAR */}
|
{/* END TOP NAVBAR */}
|
||||||
|
|
||||||
{/* BEGIN CONTENT AREA */}
|
{/* BEGIN CONTENT AREA */}
|
||||||
<Suspense>
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex h-screen w-screen items-center justify-center">
|
||||||
|
<IconLoader className="animate-spin w-16 h-16" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className={`${themeConfig.animation} p-6 animate__animated`}>{children}</div>
|
<div className={`${themeConfig.animation} p-6 animate__animated`}>{children}</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{/* END CONTENT AREA */}
|
{/* END CONTENT AREA */}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
65
src/features/auth/components/LogoutModal.tsx
Normal file
65
src/features/auth/components/LogoutModal.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
import { DialogPanel, TransitionChild } from '@headlessui/react';
|
||||||
|
import { useLogout } from '../../../lib/auth';
|
||||||
|
import IconX from '../../../components/Icon/IconX';
|
||||||
|
import IconLoader from '../../../components/Icon/IconLoader';
|
||||||
|
|
||||||
|
interface LogoutModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogoutModal = ({ isOpen, onClose }: LogoutModalProps) => {
|
||||||
|
const logout = useLogout();
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout.mutateAsync({});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||||
|
{/* BACKDROP */}
|
||||||
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
|
||||||
|
<div className="fixed inset-0 bg-black/60" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
{/* PANEL */}
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<DialogPanel className="w-full max-w-md transform overflow-hidden rounded-xl bg-white p-6 text-center align-middle shadow-xl transition-all dark:bg-[#121c2c]">
|
||||||
|
{/* CLOSE */}
|
||||||
|
<button type="button" onClick={onClose} className="absolute right-4 top-4 text-gray-400 hover:text-gray-800 dark:hover:text-white">
|
||||||
|
<IconX />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Dialog.Title as="h3" className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
Confirm Logout
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-4 text-sm text-gray-600 dark:text-gray-300">Are you sure you want to logout?</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-center gap-4">
|
||||||
|
<button type="button" onClick={onClose} className="btn btn-outline-primary">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleLogout} className="btn btn-danger disabled:opacity-50" disabled={logout.isPending}>
|
||||||
|
{logout.isPending ? <IconLoader /> : 'Logout'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -22,7 +22,7 @@ export const useCreateProductCategory = ({ mutationConfig }: UseCreateProductCat
|
|||||||
mutationFn: createProductCategory,
|
mutationFn: createProductCategory,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductCategoriesQueryOptions().queryKey,
|
queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const useDeleteProductCategory = ({ mutationConfig }: UseDeleteProductCat
|
|||||||
mutationFn: deleteProductCategory,
|
mutationFn: deleteProductCategory,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductCategoriesQueryOptions({}).queryKey,
|
queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient';
|
|||||||
import { ProductCategoryResponse } from '../types/api';
|
import { ProductCategoryResponse } from '../types/api';
|
||||||
|
|
||||||
type ProductCategoriesQueryParams = {
|
type ProductCategoriesQueryParams = {
|
||||||
q?: string;
|
search?: string | null;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductCategories = ({ q, page, limit }: ProductCategoriesQueryParams = {}): Promise<ProductCategoryResponse> => {
|
export const getProductCategories = ({ search, page, limit }: ProductCategoriesQueryParams = {}): Promise<ProductCategoryResponse> => {
|
||||||
return api.get('/inventory/categories', {
|
return api.get('/inventory/categories', {
|
||||||
params: { q, page, limit },
|
params: { search, page, limit },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductCategoriesQueryOptions = ({ q, page, limit }: ProductCategoriesQueryParams = {}) => {
|
export const getProductCategoriesQueryOptions = ({ search, page, limit }: ProductCategoriesQueryParams = {}) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryKey: ['productCategories', { q, page, limit }],
|
queryKey: ['productCategories', { search, page, limit }],
|
||||||
queryFn: () => getProductCategories({ q, page, limit }),
|
queryFn: () => getProductCategories({ search, page, limit }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,9 +26,9 @@ type UseProductCategoriesOptions = {
|
|||||||
queryConfig?: QueryConfig<typeof getProductCategoriesQueryOptions>;
|
queryConfig?: QueryConfig<typeof getProductCategoriesQueryOptions>;
|
||||||
} & ProductCategoriesQueryParams;
|
} & ProductCategoriesQueryParams;
|
||||||
|
|
||||||
export const useProductCategories = ({ q, page, limit, queryConfig }: UseProductCategoriesOptions = {}) => {
|
export const useProductCategories = ({ search, page, limit, queryConfig }: UseProductCategoriesOptions = {}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
...getProductCategoriesQueryOptions({ q, page, limit }),
|
...getProductCategoriesQueryOptions({ search, page, limit }),
|
||||||
...queryConfig,
|
...queryConfig,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const useUpdateProductCategory = ({ mutationConfig }: UseUpdateProductCat
|
|||||||
mutationFn: updateProductCategory,
|
mutationFn: updateProductCategory,
|
||||||
onSuccess: (data, ...args) => {
|
onSuccess: (data, ...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductCategoriesQueryOptions().queryKey,
|
queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(data, ...args);
|
onSuccess?.(data, ...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { ProductCategory } from '../types/api';
|
|||||||
import { EditProductCategoryModal } from './EditProductCategoryModal';
|
import { EditProductCategoryModal } from './EditProductCategoryModal';
|
||||||
import { DeleteProductCategoryModal } from './DeleteProductCategoryModal';
|
import { DeleteProductCategoryModal } from './DeleteProductCategoryModal';
|
||||||
import { DetailProductCategoryModal } from './DetailProductCategoryModal';
|
import { DetailProductCategoryModal } from './DetailProductCategoryModal';
|
||||||
|
import { useQueryState } from 'nuqs';
|
||||||
|
import { useDebounce } from '../../../hooks/useDebounce';
|
||||||
|
|
||||||
const PAGE_SIZES = [10, 20, 30, 50];
|
const PAGE_SIZES = [10, 20, 30, 50];
|
||||||
|
|
||||||
@ -17,7 +19,10 @@ export const ProductCategoriesList = () => {
|
|||||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||||
const [selectedCategory, setSelectedCategory] = useState<ProductCategory | null>(null);
|
const [selectedCategory, setSelectedCategory] = useState<ProductCategory | null>(null);
|
||||||
|
|
||||||
const categoryQuery = useProductCategories();
|
const [searchParam, setSearchParam] = useQueryState('search');
|
||||||
|
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||||
|
|
||||||
|
const categoryQuery = useProductCategories({ search: debouncedSearchParam });
|
||||||
const categories = categoryQuery.data ?? [];
|
const categories = categoryQuery.data ?? [];
|
||||||
|
|
||||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(categories, {
|
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(categories, {
|
||||||
@ -25,10 +30,6 @@ export const ProductCategoriesList = () => {
|
|||||||
direction: 'desc',
|
direction: 'desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const filteredRecords = paginatedRecords.filter((category) => category.name.toLowerCase().includes(search.toLowerCase()));
|
|
||||||
|
|
||||||
const onDelete = (category: ProductCategory) => {
|
const onDelete = (category: ProductCategory) => {
|
||||||
setSelectedCategory(category);
|
setSelectedCategory(category);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -47,12 +48,12 @@ export const ProductCategoriesList = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||||
<ProductCategoryListHeader search={search} setSearch={setSearch} />
|
<ProductCategoryListHeader search={searchParam} setSearch={setSearchParam} />
|
||||||
|
|
||||||
<div className="datatables pagination-padding px-5">
|
<div className="datatables pagination-padding px-5">
|
||||||
<DataTable
|
<DataTable
|
||||||
className="whitespace-nowrap table-hover"
|
className="whitespace-nowrap table-hover"
|
||||||
records={filteredRecords}
|
records={paginatedRecords}
|
||||||
columns={productCategoryColumns({ onEdit, onView, onDelete })}
|
columns={productCategoryColumns({ onEdit, onView, onDelete })}
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
minHeight={200}
|
minHeight={200}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CreateProductCategoryModal } from './CreateProductCategoryModal';
|
import { CreateProductCategoryModal } from './CreateProductCategoryModal';
|
||||||
|
|
||||||
export const ProductCategoryListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
export const ProductCategoryListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isOpenModalCreateCategory, setIsOpenModalCreateCategory] = useState(false);
|
const [isOpenModalCreateCategory, setIsOpenModalCreateCategory] = useState(false);
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ export const ProductCategoryListHeader = ({ search, setSearch }: { search: strin
|
|||||||
<>
|
<>
|
||||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||||
<div className="ltr:mr-auto rtl:ml-auto">
|
<div className="ltr:mr-auto rtl:ml-auto">
|
||||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateCategory(true)}>
|
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateCategory(true)}>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const useCreateProductCollection = ({ mutationConfig }: UseCreateProductC
|
|||||||
mutationFn: createProductCollection,
|
mutationFn: createProductCollection,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductCollectionsQueryOptions().queryKey,
|
queryKey: [getProductCollectionsQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export const useDeleteProductCollection = ({ mutationConfig }: UseDeleteProductC
|
|||||||
mutationFn: deleteProductCollection,
|
mutationFn: deleteProductCollection,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductCollectionsQueryOptions().queryKey,
|
queryKey: [getProductCollectionsQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient';
|
|||||||
import { ProductCollectionResponse } from '../types/api';
|
import { ProductCollectionResponse } from '../types/api';
|
||||||
|
|
||||||
type ProductCollectionsQueryParams = {
|
type ProductCollectionsQueryParams = {
|
||||||
q?: string;
|
search?: string | null;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductCollections = ({ q, page, limit }: ProductCollectionsQueryParams = {}): Promise<ProductCollectionResponse> => {
|
export const getProductCollections = ({ search, page, limit }: ProductCollectionsQueryParams = {}): Promise<ProductCollectionResponse> => {
|
||||||
return api.get('/inventory/collections', {
|
return api.get('/inventory/collections', {
|
||||||
params: { q, page, limit },
|
params: { search, page, limit },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductCollectionsQueryOptions = ({ q, page, limit }: ProductCollectionsQueryParams = {}) => {
|
export const getProductCollectionsQueryOptions = ({ search, page, limit }: ProductCollectionsQueryParams = {}) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryKey: ['productCollections', { q, page, limit }],
|
queryKey: ['productCollections', { search, page, limit }],
|
||||||
queryFn: () => getProductCollections({ q, page, limit }),
|
queryFn: () => getProductCollections({ search, page, limit }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,9 +26,9 @@ type UseProductCollectionsOptions = {
|
|||||||
queryConfig?: QueryConfig<typeof getProductCollectionsQueryOptions>;
|
queryConfig?: QueryConfig<typeof getProductCollectionsQueryOptions>;
|
||||||
} & ProductCollectionsQueryParams;
|
} & ProductCollectionsQueryParams;
|
||||||
|
|
||||||
export const useProductCollections = ({ q, page, limit, queryConfig }: UseProductCollectionsOptions = {}) => {
|
export const useProductCollections = ({ search, page, limit, queryConfig }: UseProductCollectionsOptions = {}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
...getProductCollectionsQueryOptions({ q, page, limit }),
|
...getProductCollectionsQueryOptions({ search, page, limit }),
|
||||||
...queryConfig,
|
...queryConfig,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const useUpdateProductCollection = ({ mutationConfig }: UseUpdateProductC
|
|||||||
mutationFn: updateProductCollection,
|
mutationFn: updateProductCollection,
|
||||||
onSuccess: (data, ...args) => {
|
onSuccess: (data, ...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductCollectionsQueryOptions().queryKey,
|
queryKey: [getProductCollectionsQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(data, ...args);
|
onSuccess?.(data, ...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CreateProductCollectionModal } from './CreateProductCollectionModal';
|
import { CreateProductCollectionModal } from './CreateProductCollectionModal';
|
||||||
|
|
||||||
export const ProductCollectionListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
export const ProductCollectionListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isOpenModalCreateCollection, setIsOpenModalCreateCollection] = useState(false);
|
const [isOpenModalCreateCollection, setIsOpenModalCreateCollection] = useState(false);
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ export const ProductCollectionListHeader = ({ search, setSearch }: { search: str
|
|||||||
<>
|
<>
|
||||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||||
<div className="ltr:mr-auto rtl:ml-auto">
|
<div className="ltr:mr-auto rtl:ml-auto">
|
||||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateCollection(true)}>
|
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateCollection(true)}>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { ProductCollection } from '../types/api';
|
|||||||
import { DeleteProductCollectionModal } from './DeleteProductCollectionModal';
|
import { DeleteProductCollectionModal } from './DeleteProductCollectionModal';
|
||||||
import { EditProductCollectionModal } from './EditProductCollectionModal';
|
import { EditProductCollectionModal } from './EditProductCollectionModal';
|
||||||
import { DetailProductCollectionModal } from './DetailProductCollectionModal';
|
import { DetailProductCollectionModal } from './DetailProductCollectionModal';
|
||||||
|
import { useQueryState } from 'nuqs';
|
||||||
|
import { useDebounce } from '../../../hooks/useDebounce';
|
||||||
|
|
||||||
const PAGE_SIZES = [10, 20, 30, 50];
|
const PAGE_SIZES = [10, 20, 30, 50];
|
||||||
|
|
||||||
@ -17,7 +19,10 @@ export const ProductCollectionsList = () => {
|
|||||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||||
const [selectedCollection, setSelectedCollection] = useState<ProductCollection | null>(null);
|
const [selectedCollection, setSelectedCollection] = useState<ProductCollection | null>(null);
|
||||||
|
|
||||||
const collectionQuery = useProductCollections();
|
const [searchParam, setSearchParam] = useQueryState('search');
|
||||||
|
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||||
|
|
||||||
|
const collectionQuery = useProductCollections({ search: debouncedSearchParam });
|
||||||
const collections = collectionQuery.data ?? [];
|
const collections = collectionQuery.data ?? [];
|
||||||
|
|
||||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(collections, {
|
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(collections, {
|
||||||
@ -25,10 +30,6 @@ export const ProductCollectionsList = () => {
|
|||||||
direction: 'desc',
|
direction: 'desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const filteredRecords = paginatedRecords.filter((collection) => collection.name.toLowerCase().includes(search.toLowerCase()));
|
|
||||||
|
|
||||||
const onDelete = (collection: ProductCollection) => {
|
const onDelete = (collection: ProductCollection) => {
|
||||||
setSelectedCollection(collection);
|
setSelectedCollection(collection);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -47,12 +48,12 @@ export const ProductCollectionsList = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||||
<ProductCollectionListHeader search={search} setSearch={setSearch} />
|
<ProductCollectionListHeader search={searchParam} setSearch={setSearchParam} />
|
||||||
|
|
||||||
<div className="datatables pagination-padding px-5">
|
<div className="datatables pagination-padding px-5">
|
||||||
<DataTable
|
<DataTable
|
||||||
className="whitespace-nowrap table-hover"
|
className="whitespace-nowrap table-hover"
|
||||||
records={filteredRecords}
|
records={paginatedRecords}
|
||||||
columns={productCollectionColumns({ onEdit, onView, onDelete })}
|
columns={productCollectionColumns({ onEdit, onView, onDelete })}
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
minHeight={200}
|
minHeight={200}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const useCreateProductColour = ({ mutationConfig }: UseCreateProductColou
|
|||||||
mutationFn: createProductColour,
|
mutationFn: createProductColour,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductColoursQueryOptions().queryKey,
|
queryKey: [getProductColoursQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export const useDeleteProductColour = ({ mutationConfig }: UseDeleteProductColou
|
|||||||
mutationFn: deleteProductColour,
|
mutationFn: deleteProductColour,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductColoursQueryOptions().queryKey,
|
queryKey: [getProductColoursQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient';
|
|||||||
import { ProductColourResponse } from '../types/api';
|
import { ProductColourResponse } from '../types/api';
|
||||||
|
|
||||||
type ProductColoursQueryParams = {
|
type ProductColoursQueryParams = {
|
||||||
q?: string;
|
search?: string | null;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductColours = ({ q, page, limit }: ProductColoursQueryParams = {}): Promise<ProductColourResponse> => {
|
export const getProductColours = ({ search, page, limit }: ProductColoursQueryParams = {}): Promise<ProductColourResponse> => {
|
||||||
return api.get('/inventory/colours', {
|
return api.get('/inventory/colours', {
|
||||||
params: { q, page, limit },
|
params: { search, page, limit },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductColoursQueryOptions = ({ q, page, limit }: ProductColoursQueryParams = {}) => {
|
export const getProductColoursQueryOptions = ({ search, page, limit }: ProductColoursQueryParams = {}) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryKey: ['productColours', { q, page, limit }],
|
queryKey: ['productColours', { search, page, limit }],
|
||||||
queryFn: () => getProductColours({ q, page, limit }),
|
queryFn: () => getProductColours({ search, page, limit }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,9 +26,9 @@ type UseProductColoursOptions = {
|
|||||||
queryConfig?: QueryConfig<typeof getProductColoursQueryOptions>;
|
queryConfig?: QueryConfig<typeof getProductColoursQueryOptions>;
|
||||||
} & ProductColoursQueryParams;
|
} & ProductColoursQueryParams;
|
||||||
|
|
||||||
export const useProductColours = ({ q, page, limit, queryConfig }: UseProductColoursOptions = {}) => {
|
export const useProductColours = ({ search, page, limit, queryConfig }: UseProductColoursOptions = {}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
...getProductColoursQueryOptions({ q, page, limit }),
|
...getProductColoursQueryOptions({ search, page, limit }),
|
||||||
...queryConfig,
|
...queryConfig,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const useUpdateProductColour = ({ mutationConfig }: UseUpdateProductColou
|
|||||||
mutationFn: updateProductColour,
|
mutationFn: updateProductColour,
|
||||||
onSuccess: (data, ...args) => {
|
onSuccess: (data, ...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductColoursQueryOptions().queryKey,
|
queryKey: [getProductColoursQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(data, ...args);
|
onSuccess?.(data, ...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CreateProductColourModal } from './CreateProductColourModal';
|
import { CreateProductColourModal } from './CreateProductColourModal';
|
||||||
|
|
||||||
export const ProductColourListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
export const ProductColourListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isOpenModalCreateColour, setIsOpenModalCreateColour] = useState(false);
|
const [isOpenModalCreateColour, setIsOpenModalCreateColour] = useState(false);
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ export const ProductColourListHeader = ({ search, setSearch }: { search: string;
|
|||||||
<>
|
<>
|
||||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||||
<div className="ltr:mr-auto rtl:ml-auto">
|
<div className="ltr:mr-auto rtl:ml-auto">
|
||||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateColour(true)}>
|
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateColour(true)}>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { ProductColour } from '../types/api';
|
|||||||
import { DeleteProductColourModal } from './DeleteProductColourModal';
|
import { DeleteProductColourModal } from './DeleteProductColourModal';
|
||||||
import { EditProductColourModal } from './EditProductColourModal';
|
import { EditProductColourModal } from './EditProductColourModal';
|
||||||
import { DetailProductColourModal } from './DetailProductColourModal';
|
import { DetailProductColourModal } from './DetailProductColourModal';
|
||||||
|
import { useQueryState } from 'nuqs';
|
||||||
|
import { useDebounce } from '../../../hooks/useDebounce';
|
||||||
|
|
||||||
const PAGE_SIZES = [10, 20, 30, 50];
|
const PAGE_SIZES = [10, 20, 30, 50];
|
||||||
|
|
||||||
@ -17,7 +19,10 @@ export const ProductColoursList = () => {
|
|||||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||||
const [selectedColour, setSelectedColour] = useState<ProductColour | null>(null);
|
const [selectedColour, setSelectedColour] = useState<ProductColour | null>(null);
|
||||||
|
|
||||||
const colourQuery = useProductColours();
|
const [searchParam, setSearchParam] = useQueryState('search');
|
||||||
|
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||||
|
|
||||||
|
const colourQuery = useProductColours({ search: debouncedSearchParam });
|
||||||
const colours = colourQuery.data ?? [];
|
const colours = colourQuery.data ?? [];
|
||||||
|
|
||||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(colours, {
|
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(colours, {
|
||||||
@ -25,10 +30,6 @@ export const ProductColoursList = () => {
|
|||||||
direction: 'desc',
|
direction: 'desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const filteredRecords = paginatedRecords.filter((colour) => colour.name.toLowerCase().includes(search.toLowerCase()));
|
|
||||||
|
|
||||||
const onDelete = (colour: ProductColour) => {
|
const onDelete = (colour: ProductColour) => {
|
||||||
setSelectedColour(colour);
|
setSelectedColour(colour);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -47,12 +48,12 @@ export const ProductColoursList = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||||
<ProductColourListHeader search={search} setSearch={setSearch} />
|
<ProductColourListHeader search={searchParam} setSearch={setSearchParam} />
|
||||||
|
|
||||||
<div className="datatables pagination-padding px-5">
|
<div className="datatables pagination-padding px-5">
|
||||||
<DataTable
|
<DataTable
|
||||||
className="whitespace-nowrap table-hover"
|
className="whitespace-nowrap table-hover"
|
||||||
records={filteredRecords}
|
records={paginatedRecords}
|
||||||
columns={productColourColumns({ onEdit, onView, onDelete })}
|
columns={productColourColumns({ onEdit, onView, onDelete })}
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
minHeight={200}
|
minHeight={200}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const useCreateProductSize = ({ mutationConfig }: UseCreateProductSizeOpt
|
|||||||
mutationFn: createProductSize,
|
mutationFn: createProductSize,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductSizesQueryOptions().queryKey,
|
queryKey: [getProductSizesQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export const useDeleteProductSize = ({ mutationConfig }: UseDeleteProductSizeOpt
|
|||||||
mutationFn: deleteProductSize,
|
mutationFn: deleteProductSize,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductSizesQueryOptions().queryKey,
|
queryKey: [getProductSizesQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient';
|
|||||||
import { ProductSizeResponse } from '../types/api';
|
import { ProductSizeResponse } from '../types/api';
|
||||||
|
|
||||||
type ProductSizesQueryParams = {
|
type ProductSizesQueryParams = {
|
||||||
q?: string;
|
search?: string | null;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductSizes = ({ q, page, limit }: ProductSizesQueryParams = {}): Promise<ProductSizeResponse> => {
|
export const getProductSizes = ({ search, page, limit }: ProductSizesQueryParams = {}): Promise<ProductSizeResponse> => {
|
||||||
return api.get('/inventory/sizes', {
|
return api.get('/inventory/sizes', {
|
||||||
params: { q, page, limit },
|
params: { search, page, limit },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductSizesQueryOptions = ({ q, page, limit }: ProductSizesQueryParams = {}) => {
|
export const getProductSizesQueryOptions = ({ search, page, limit }: ProductSizesQueryParams = {}) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryKey: ['productSizes', { q, page, limit }],
|
queryKey: ['productSizes', { search, page, limit }],
|
||||||
queryFn: () => getProductSizes({ q, page, limit }),
|
queryFn: () => getProductSizes({ search, page, limit }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,9 +26,9 @@ type UseProductSizesOptions = {
|
|||||||
queryConfig?: QueryConfig<typeof getProductSizesQueryOptions>;
|
queryConfig?: QueryConfig<typeof getProductSizesQueryOptions>;
|
||||||
} & ProductSizesQueryParams;
|
} & ProductSizesQueryParams;
|
||||||
|
|
||||||
export const useProductSizes = ({ q, page, limit, queryConfig }: UseProductSizesOptions = {}) => {
|
export const useProductSizes = ({ search, page, limit, queryConfig }: UseProductSizesOptions = {}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
...getProductSizesQueryOptions({ q, page, limit }),
|
...getProductSizesQueryOptions({ search, page, limit }),
|
||||||
...queryConfig,
|
...queryConfig,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const useUpdateProductSize = ({ mutationConfig }: UseUpdateProductSizeOpt
|
|||||||
mutationFn: updateProductSize,
|
mutationFn: updateProductSize,
|
||||||
onSuccess: (data, ...args) => {
|
onSuccess: (data, ...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductSizesQueryOptions().queryKey,
|
queryKey: [getProductSizesQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(data, ...args);
|
onSuccess?.(data, ...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CreateProductSizeModal } from './CreateProductSizeModal';
|
import { CreateProductSizeModal } from './CreateProductSizeModal';
|
||||||
|
|
||||||
export const ProductSizeListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
export const ProductSizeListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isOpenModalCreateSize, setIsOpenModalCreateSize] = useState(false);
|
const [isOpenModalCreateSize, setIsOpenModalCreateSize] = useState(false);
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ export const ProductSizeListHeader = ({ search, setSearch }: { search: string; s
|
|||||||
<>
|
<>
|
||||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||||
<div className="ltr:mr-auto rtl:ml-auto">
|
<div className="ltr:mr-auto rtl:ml-auto">
|
||||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateSize(true)}>
|
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateSize(true)}>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { ProductSize } from '../types/api';
|
|||||||
import { DeleteProductSizeModal } from './DeleteProductSizeModal';
|
import { DeleteProductSizeModal } from './DeleteProductSizeModal';
|
||||||
import { EditProductSizeModal } from './EditProductSizeModal';
|
import { EditProductSizeModal } from './EditProductSizeModal';
|
||||||
import { DetailProductSizeModal } from './DetailProductSizeModal';
|
import { DetailProductSizeModal } from './DetailProductSizeModal';
|
||||||
|
import { useQueryState } from 'nuqs';
|
||||||
|
import { useDebounce } from '../../../hooks/useDebounce';
|
||||||
|
|
||||||
const PAGE_SIZES = [10, 20, 30, 50];
|
const PAGE_SIZES = [10, 20, 30, 50];
|
||||||
|
|
||||||
@ -17,7 +19,10 @@ export const ProductSizesList = () => {
|
|||||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||||
const [selectedSize, setSelectedSize] = useState<ProductSize | null>(null);
|
const [selectedSize, setSelectedSize] = useState<ProductSize | null>(null);
|
||||||
|
|
||||||
const sizeQuery = useProductSizes();
|
const [searchParam, setSearchParam] = useQueryState('search');
|
||||||
|
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||||
|
|
||||||
|
const sizeQuery = useProductSizes({ search: debouncedSearchParam });
|
||||||
const sizes = sizeQuery.data ?? [];
|
const sizes = sizeQuery.data ?? [];
|
||||||
|
|
||||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(sizes, {
|
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(sizes, {
|
||||||
@ -25,10 +30,6 @@ export const ProductSizesList = () => {
|
|||||||
direction: 'desc',
|
direction: 'desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const filteredRecords = paginatedRecords.filter((size) => size.name.toLowerCase().includes(search.toLowerCase()));
|
|
||||||
|
|
||||||
const onDelete = (size: ProductSize) => {
|
const onDelete = (size: ProductSize) => {
|
||||||
setSelectedSize(size);
|
setSelectedSize(size);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -47,12 +48,12 @@ export const ProductSizesList = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||||
<ProductSizeListHeader search={search} setSearch={setSearch} />
|
<ProductSizeListHeader search={searchParam} setSearch={setSearchParam} />
|
||||||
|
|
||||||
<div className="datatables pagination-padding px-5">
|
<div className="datatables pagination-padding px-5">
|
||||||
<DataTable
|
<DataTable
|
||||||
className="whitespace-nowrap table-hover"
|
className="whitespace-nowrap table-hover"
|
||||||
records={filteredRecords}
|
records={paginatedRecords}
|
||||||
columns={productSizeColumns({ onEdit, onView, onDelete })}
|
columns={productSizeColumns({ onEdit, onView, onDelete })}
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
minHeight={200}
|
minHeight={200}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const useCreateProduct = ({ mutationConfig }: UseCreateProductOptions = {
|
|||||||
mutationFn: createProduct,
|
mutationFn: createProduct,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductsQueryOptions({}).queryKey,
|
queryKey: [getProductsQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const useDeleteProduct = ({ mutationConfig }: UseDeleteProductOptions = {
|
|||||||
mutationFn: deleteProduct,
|
mutationFn: deleteProduct,
|
||||||
onSuccess: (...args) => {
|
onSuccess: (...args) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductsQueryOptions({}).queryKey,
|
queryKey: [getProductsQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(...args);
|
onSuccess?.(...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,25 +5,25 @@ import { api } from '../../../lib/apiClient';
|
|||||||
import { ProductResponse } from '../types/api';
|
import { ProductResponse } from '../types/api';
|
||||||
|
|
||||||
type ProductsQueryParams = {
|
type ProductsQueryParams = {
|
||||||
q?: string;
|
search?: string | null;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProducts = ({ q, page, limit }: ProductsQueryParams): Promise<ProductResponse> => {
|
export const getProducts = ({ search, page, limit }: ProductsQueryParams): Promise<ProductResponse> => {
|
||||||
return api.get('/inventory/products', {
|
return api.get('/inventory/products', {
|
||||||
params: {
|
params: {
|
||||||
q,
|
search,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductsQueryOptions = ({ q, page, limit }: ProductsQueryParams) => {
|
export const getProductsQueryOptions = ({ search, page, limit }: ProductsQueryParams) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryKey: ['products', { q, page, limit }],
|
queryKey: ['products', { search, page, limit }],
|
||||||
queryFn: () => getProducts({ q, page, limit }),
|
queryFn: () => getProducts({ search, page, limit }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,9 +31,9 @@ type UseProductsOptions = {
|
|||||||
queryConfig?: QueryConfig<typeof getProductsQueryOptions>;
|
queryConfig?: QueryConfig<typeof getProductsQueryOptions>;
|
||||||
} & ProductsQueryParams;
|
} & ProductsQueryParams;
|
||||||
|
|
||||||
export const useProducts = ({ q, page, limit, queryConfig }: UseProductsOptions = {}) => {
|
export const useProducts = ({ search, page, limit, queryConfig }: UseProductsOptions = {}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
...getProductsQueryOptions({ q, page, limit }),
|
...getProductsQueryOptions({ search, page, limit }),
|
||||||
...queryConfig,
|
...queryConfig,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,8 +21,8 @@ export const useUpdateProduct = ({ mutationConfig }: UseUpdateProductOptions = {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: updateProduct,
|
mutationFn: updateProduct,
|
||||||
onSuccess: (data, ...args) => {
|
onSuccess: (data, ...args) => {
|
||||||
queryClient.refetchQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getProductsQueryOptions({}).queryKey,
|
queryKey: [getProductsQueryOptions({}).queryKey[0]],
|
||||||
});
|
});
|
||||||
onSuccess?.(data, ...args);
|
onSuccess?.(data, ...args);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
|||||||
import { CreateProductModal } from './CreateProductModal';
|
import { CreateProductModal } from './CreateProductModal';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export const ProductListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
export const ProductListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isOpenModalCreateProduct, setIsOpenModalCreateProduct] = useState(false);
|
const [isOpenModalCreateProduct, setIsOpenModalCreateProduct] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||||
<div className="ltr:mr-auto rtl:ml-auto">
|
<div className="ltr:mr-auto rtl:ml-auto">
|
||||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateProduct(true)}>
|
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateProduct(true)}>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { DeleteProductModal } from './DeleteproductModal';
|
|||||||
import { Product } from '../types/api';
|
import { Product } from '../types/api';
|
||||||
import { EditProductModal } from './EditProductModal';
|
import { EditProductModal } from './EditProductModal';
|
||||||
import { DetailProductModal } from './DetailProductModal';
|
import { DetailProductModal } from './DetailProductModal';
|
||||||
|
import { useQueryState } from 'nuqs';
|
||||||
|
import { useDebounce } from '../../../hooks/useDebounce';
|
||||||
|
|
||||||
const PAGE_SIZES = [10, 20, 30, 50];
|
const PAGE_SIZES = [10, 20, 30, 50];
|
||||||
|
|
||||||
@ -17,7 +19,10 @@ export const ProductsList = () => {
|
|||||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||||
|
|
||||||
const productsQuery = useProducts();
|
const [searchParam, setSearchParam] = useQueryState('search');
|
||||||
|
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||||
|
|
||||||
|
const productsQuery = useProducts({ search: debouncedSearchParam });
|
||||||
const products = productsQuery.data ?? [];
|
const products = productsQuery.data ?? [];
|
||||||
|
|
||||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(products, {
|
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(products, {
|
||||||
@ -25,10 +30,6 @@ export const ProductsList = () => {
|
|||||||
direction: 'desc',
|
direction: 'desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const filteredRecords = paginatedRecords.filter((product) => product.name.toLowerCase().includes(search.toLowerCase()));
|
|
||||||
|
|
||||||
const onDelete = (product: Product) => {
|
const onDelete = (product: Product) => {
|
||||||
setSelectedProduct(product);
|
setSelectedProduct(product);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@ -47,12 +48,12 @@ export const ProductsList = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||||
<ProductListHeader search={search} setSearch={setSearch} />
|
<ProductListHeader search={searchParam} setSearch={setSearchParam} />
|
||||||
|
|
||||||
<div className="datatables pagination-padding px-5">
|
<div className="datatables pagination-padding px-5">
|
||||||
<DataTable
|
<DataTable
|
||||||
className="whitespace-nowrap table-hover"
|
className="whitespace-nowrap table-hover"
|
||||||
records={filteredRecords}
|
records={paginatedRecords}
|
||||||
columns={productColumns({ onEdit, onView, onDelete })}
|
columns={productColumns({ onEdit, onView, onDelete })}
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
minHeight={200}
|
minHeight={200}
|
||||||
|
|||||||
13
src/hooks/useDebounce.ts
Normal file
13
src/hooks/useDebounce.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useDebounce<T>(value: T, delay = 500): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
||||||
|
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
@ -1,18 +1,23 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { DataTableSortStatus } from 'mantine-datatable';
|
import { DataTableSortStatus } from 'mantine-datatable';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
|
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||||
|
|
||||||
export function useSortedPaginatedRecords<T>(data: T[] = [], defaultSort: DataTableSortStatus) {
|
export function useSortedPaginatedRecords<T>(data: T[] = [], defaultSort: DataTableSortStatus) {
|
||||||
|
// ✅ Sort status pakai useState biasa (tidak sync ke URL)
|
||||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>(defaultSort);
|
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>(defaultSort);
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(10);
|
// ✅ Pagination pakai URL
|
||||||
|
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
|
||||||
|
const [pageSize, setPageSize] = useQueryState('pageSize', parseAsInteger.withDefault(10));
|
||||||
|
|
||||||
const [records, setRecords] = useState<T[]>([]);
|
const [records, setRecords] = useState<T[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sorted = sortBy(data, sortStatus.columnAccessor);
|
const sorted = sortBy(data, sortStatus.columnAccessor);
|
||||||
setRecords(sortStatus.direction === 'desc' ? sorted.reverse() : sorted);
|
setRecords(sortStatus.direction === 'desc' ? sorted.reverse() : sorted);
|
||||||
setPage(1);
|
setPage(1); // reset ke page 1 kalau sort berubah
|
||||||
}, [data, sortStatus]);
|
}, [data, sortStatus, setPage]);
|
||||||
|
|
||||||
const paginatedRecords = records.slice((page - 1) * pageSize, page * pageSize);
|
const paginatedRecords = records.slice((page - 1) * pageSize, page * pageSize);
|
||||||
|
|
||||||
|
|||||||
@ -34,8 +34,13 @@ async function loginFn(dataForm: LoginFormValues): Promise<UsersMeResponse> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function logoutFn() {
|
async function logoutFn() {
|
||||||
Cookie.clearTokens();
|
const promise = new Promise((resolve) => {
|
||||||
return null;
|
setTimeout(() => {
|
||||||
|
Cookie.clearTokens();
|
||||||
|
resolve(null);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authConfig = {
|
const authConfig = {
|
||||||
|
|||||||
17
src/main.tsx
17
src/main.tsx
@ -27,20 +27,31 @@ import { queryClient } from './lib/reactQuery';
|
|||||||
import { AuthLoader } from './lib/auth';
|
import { AuthLoader } from './lib/auth';
|
||||||
import IconLoader from './components/Icon/IconLoader';
|
import IconLoader from './components/Icon/IconLoader';
|
||||||
|
|
||||||
|
// Nuqs
|
||||||
|
import { NuqsAdapter } from 'nuqs/adapters/react-router/v6';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Suspense>
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex h-screen w-screen items-center justify-center">
|
||||||
|
<IconLoader className="animate-spin w-16 h-16" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ReactQueryDevtools />
|
<ReactQueryDevtools />
|
||||||
<AuthLoader
|
<AuthLoader
|
||||||
renderLoading={() => (
|
renderLoading={() => (
|
||||||
<div className="flex h-screen w-screen items-center justify-center">
|
<div className="flex h-screen w-screen items-center justify-center">
|
||||||
<IconLoader className="animate-spin" />
|
<IconLoader className="animate-spin w-16 h-16" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RouterProvider router={router} />
|
<NuqsAdapter>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</NuqsAdapter>
|
||||||
</AuthLoader>
|
</AuthLoader>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user