From 4efeaa9dfed49f4e67825d4e07ea261c5e86b734 Mon Sep 17 00:00:00 2001 From: fizipan Date: Sat, 28 Jun 2025 07:59:41 +0700 Subject: [PATCH] first commit --- package-lock.json | 40 + package.json | 1 + src/components/Layouts/DefaultLayout.tsx | 10 +- src/components/Layouts/Header.tsx | 1438 +++++++++-------- src/features/auth/components/LogoutModal.tsx | 65 + .../api/createProductCategory.ts | 2 +- .../api/deleteProductCategort.ts | 2 +- .../api/getProductCategories.ts | 16 +- .../api/updateProductCategory.ts | 2 +- .../components/ProductCategoriesList.tsx | 15 +- .../components/ProductCategoryListHeader.tsx | 4 +- .../api/createProductCollection.ts | 2 +- .../api/deleteProductCollection.ts | 2 +- .../api/getProductCollections.ts | 16 +- .../api/updateProductCollection.ts | 2 +- .../ProductCollectionListHeader.tsx | 4 +- .../components/ProductCollectionsList.tsx | 15 +- .../productColours/api/createProductColour.ts | 2 +- .../productColours/api/deleteProductColour.ts | 2 +- .../productColours/api/getProductColours.ts | 16 +- .../productColours/api/updateProductColour.ts | 2 +- .../components/ProductColourListHeader.tsx | 4 +- .../components/ProductColoursList.tsx | 15 +- .../productSizes/api/createProductSize.ts | 2 +- .../productSizes/api/deleteProductSize.ts | 2 +- .../productSizes/api/getProductSizes.ts | 16 +- .../productSizes/api/updateProductSize.ts | 2 +- .../components/ProductSizeListHeader.tsx | 4 +- .../components/ProductSizesList.tsx | 15 +- src/features/products/api/createProduct.ts | 2 +- src/features/products/api/deleteProduct.ts | 2 +- src/features/products/api/getProducts.ts | 16 +- src/features/products/api/updateProduct.ts | 4 +- .../products/components/ProductListHeader.tsx | 4 +- .../products/components/ProductsList.tsx | 15 +- src/hooks/useDebounce.ts | 13 + src/hooks/useSortedPaginatedRecords.ts | 13 +- src/lib/auth.tsx | 9 +- src/main.tsx | 17 +- 39 files changed, 983 insertions(+), 830 deletions(-) create mode 100644 src/features/auth/components/LogoutModal.tsx create mode 100644 src/hooks/useDebounce.ts diff --git a/package-lock.json b/package-lock.json index da453ce..7a804ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.522.0", "mantine-datatable": "^1.7.17", + "nuqs": "^2.4.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.58.1", @@ -16067,6 +16068,12 @@ "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": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -16241,6 +16248,39 @@ "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": { "version": "2.2.13", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", diff --git a/package.json b/package.json index 4fe6b43..3336150 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.522.0", "mantine-datatable": "^1.7.17", + "nuqs": "^2.4.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.58.1", diff --git a/src/components/Layouts/DefaultLayout.tsx b/src/components/Layouts/DefaultLayout.tsx index e6aa7c6..aa4889c 100644 --- a/src/components/Layouts/DefaultLayout.tsx +++ b/src/components/Layouts/DefaultLayout.tsx @@ -5,9 +5,9 @@ import { IRootState } from '../../store'; import { toggleSidebar } from '../../store/themeConfigSlice'; import Footer from './Footer'; import Header from './Header'; -import Setting from './Setting'; import Sidebar from './Sidebar'; import Portals from '../../components/Portals'; +import IconLoader from '../Icon/IconLoader'; const DefaultLayout = ({ children }: PropsWithChildren) => { const themeConfig = useSelector((state: IRootState) => state.themeConfig); @@ -89,7 +89,13 @@ const DefaultLayout = ({ children }: PropsWithChildren) => { {/* END TOP NAVBAR */} {/* BEGIN CONTENT AREA */} - + + + + } + >
{children}
{/* END CONTENT AREA */} diff --git a/src/components/Layouts/Header.tsx b/src/components/Layouts/Header.tsx index cbeb361..6919a9a 100644 --- a/src/components/Layouts/Header.tsx +++ b/src/components/Layouts/Header.tsx @@ -14,8 +14,6 @@ import IconLaptop from '../Icon/IconLaptop'; import IconInfoCircle from '../Icon/IconInfoCircle'; import IconBellBing from '../Icon/IconBellBing'; import IconUser from '../Icon/IconUser'; -import IconMail from '../Icon/IconMail'; -import IconLockDots from '../Icon/IconLockDots'; import IconLogout from '../Icon/IconLogout'; import IconMenuDashboard from '../Icon/Menu/IconMenuDashboard'; import IconCaretDown from '../Icon/IconCaretDown'; @@ -27,6 +25,7 @@ import IconMenuForms from '../Icon/Menu/IconMenuForms'; import IconMenuPages from '../Icon/Menu/IconMenuPages'; import IconMenuMore from '../Icon/Menu/IconMenuMore'; import { useLogout, useUser } from '../../lib/auth'; +import { LogoutModal } from '../../features/auth/components/LogoutModal'; const Header = () => { const location = useLocation(); @@ -92,735 +91,738 @@ const Header = () => { const [flag, setFlag] = useState(themeConfig.locale); const user = useUser(); - const logout = useLogout(); + const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false); const { t } = useTranslation(); return ( -
-
-
-
- - logo - Mini ERP - - -
- -
-
-
- {themeConfig.theme === 'light' ? ( - - ) : ( - '' - )} - {themeConfig.theme === 'dark' && ( - - )} - {themeConfig.theme === 'system' && ( - - )} -
-
- } + <> +
+
+
+
+ + logo + Mini ERP + + +
+ +
+
+
+ {themeConfig.theme === 'light' ? ( + + ) : ( + '' + )} + {themeConfig.theme === 'dark' && ( + + )} + {themeConfig.theme === 'system' && ( + + )} +
+
+ } + > +
    + {themeConfig.languageList.map((item: any) => { + return ( +
  • + +
  • + ); + })} +
+
+
+
+ + + + + + + + } + > +
    +
  • e.stopPropagation()}> +
    +

    Notification

    + {notifications.length ? {notifications.length}New : ''} +
    +
  • + {notifications.length > 0 ? ( + <> + {notifications.map((notification) => { + return ( +
  • e.stopPropagation()}> +
    +
    +
    + profile + +
    +
    +
    +
    +
    + {notification.time} +
    + +
    +
    +
  • + ); + })} +
  • +
    + +
    +
  • + + ) : ( +
  • e.stopPropagation()}> +
  • - ); - })} -
-
-
-
- - - - - - - - } - > -
    -
  • e.stopPropagation()}> -
    -

    Notification

    - {notifications.length ? {notifications.length}New : ''} -
    -
  • - {notifications.length > 0 ? ( - <> - {notifications.map((notification) => { - return ( -
  • e.stopPropagation()}> -
    -
    -
    - profile - -
    -
    -
    -
    -
    - {notification.time} -
    - -
    -
    -
  • - ); - })} -
  • -
    - + )} +
+
+
+
+ } + > +
    +
  • +
    + userProfile +
    +

    {user?.data?.username}

    +
    -
  • - - ) : ( -
  • e.stopPropagation()}> -
+ +
  • + + + Profile + +
  • +
  • +
  • - )} - - -
    -
    - } - > -
      -
    • -
      - userProfile -
      -

      {user?.data?.username}

      - -
      -
      -
    • -
    • - - - Profile - -
    • -
    • - -
    • -
    -
    + + +
    -
    - {/* horizontal menu */} -
      -
    • - -
        -
      • - {t('sales')} -
      • -
      • - {t('analytics')} -
      • -
      • - {t('finance')} -
      • -
      • - {t('crypto')} -
      • -
      -
    • -
    • - -
        -
      • - {t('chat')} -
      • -
      • - {t('mailbox')} -
      • -
      • - {t('todo_list')} -
      • -
      • - {t('notes')} -
      • -
      • - {t('scrumboard')} -
      • -
      • - {t('contacts')} -
      • -
      • - -
          -
        • - {t('list')} -
        • -
        • - {t('preview')} -
        • -
        • - {t('add')} -
        • -
        • - {t('edit')} -
        • -
        -
      • -
      • - {t('calendar')} -
      • -
      -
    • -
    • - -
        -
      • - {t('tabs')} -
      • -
      • - {t('accordions')} -
      • -
      • - {t('modals')} -
      • -
      • - {t('cards')} -
      • -
      • - {t('carousel')} -
      • -
      • - {t('countdown')} -
      • -
      • - {t('counter')} -
      • -
      • - {t('sweet_alerts')} -
      • -
      • - {t('timeline')} -
      • -
      • - {t('notifications')} -
      • -
      • - {t('media_object')} -
      • -
      • - {t('list_group')} -
      • -
      • - {t('pricing_tables')} -
      • -
      • - {t('lightbox')} -
      • -
      -
    • -
    • - -
        -
      • - {t('alerts')} -
      • -
      • - {t('avatar')} -
      • -
      • - {t('badges')} -
      • -
      • - {t('breadcrumbs')} -
      • -
      • - {t('buttons')} -
      • -
      • - {t('button_groups')} -
      • -
      • - {t('color_library')} -
      • -
      • - {t('dropdown')} -
      • -
      • - {t('infobox')} -
      • -
      • - {t('jumbotron')} -
      • -
      • - {t('loader')} -
      • -
      • - {t('pagination')} -
      • -
      • - {t('popovers')} -
      • -
      • - {t('progress_bar')} -
      • -
      • - {t('search')} -
      • -
      • - {t('tooltips')} -
      • -
      • - {t('treeview')} -
      • -
      • - {t('typography')} -
      • -
      -
    • -
    • - -
        -
      • - {t('tables')} -
      • -
      • - -
          -
        • - {t('basic')} -
        • -
        • - {t('advanced')} -
        • -
        • - {t('skin')} -
        • -
        • - {t('order_sorting')} -
        • -
        • - {t('multi_column')} -
        • -
        • - {t('multiple_tables')} -
        • -
        • - {t('alt_pagination')} -
        • -
        • - {t('checkbox')} -
        • -
        • - {t('range_search')} -
        • -
        • - {t('export')} -
        • -
        • - {t('column_chooser')} -
        • -
        -
      • -
      -
    • -
    • - -
        -
      • - {t('basic')} -
      • -
      • - {t('input_group')} -
      • -
      • - {t('layouts')} -
      • -
      • - {t('validation')} -
      • -
      • - {t('input_mask')} -
      • -
      • - {t('select2')} -
      • -
      • - {t('touchspin')} -
      • -
      • - {t('checkbox_and_radio')} -
      • -
      • - {t('switches')} -
      • -
      • - {t('wizards')} -
      • -
      • - {t('file_upload')} -
      • -
      • - {t('quill_editor')} -
      • -
      • - {t('markdown_editor')} -
      • -
      • - {t('date_and_range_picker')} -
      • -
      • - {t('clipboard')} -
      • -
      -
    • -
    • - -
        -
      • - -
          -
        • - {t('profile')} -
        • -
        • - {t('account_settings')} -
        • -
        -
      • -
      • - {t('knowledge_base')} -
      • -
      • - - {t('contact_us_boxed')} - -
      • -
      • - - {t('contact_us_cover')} - -
      • -
      • - {t('faq')} -
      • -
      • - - {t('coming_soon_boxed')} - -
      • -
      • - - {t('coming_soon_cover')} - -
      • -
      • - - {t('maintenence')} - -
      • -
      • - -
          -
        • - - {t('404')} - -
        • -
        • - - {t('500')} - -
        • -
        • - - {t('503')} - -
        • -
        -
      • -
      • - -
          -
        • - - {t('login_cover')} - -
        • -
        • - - {t('login_boxed')} - -
        • -
        -
      • -
      • - -
          -
        • - - {t('register_cover')} - -
        • -
        • - - {t('register_boxed')} - -
        • -
        -
      • -
      • - -
          -
        • - - {t('recover_id_cover')} - -
        • -
        • - - {t('recover_id_boxed')} - -
        • -
        -
      • -
      • - -
          -
        • - - {t('unlock_cover')} - -
        • -
        • - - {t('unlock_boxed')} - -
        • -
        -
      • -
      -
    • -
    • - -
        -
      • - {t('drag_and_drop')} -
      • -
      • - {t('charts')} -
      • -
      • - {t('font_icons')} -
      • -
      • - {t('widgets')} -
      • -
      • - - {t('documentation')} - -
      • -
      -
    • -
    -
    -
    + {/* horizontal menu */} + + + + setIsLogoutModalOpen(false)} /> + ); }; diff --git a/src/features/auth/components/LogoutModal.tsx b/src/features/auth/components/LogoutModal.tsx new file mode 100644 index 0000000..8790780 --- /dev/null +++ b/src/features/auth/components/LogoutModal.tsx @@ -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 ( + + + {/* BACKDROP */} + +
    + + + {/* PANEL */} +
    + + + {/* CLOSE */} + + + + Confirm Logout + +
    Are you sure you want to logout?
    + +
    + + +
    +
    +
    +
    +
    +
    + ); +}; diff --git a/src/features/productCategories/api/createProductCategory.ts b/src/features/productCategories/api/createProductCategory.ts index 8095f7b..4766496 100644 --- a/src/features/productCategories/api/createProductCategory.ts +++ b/src/features/productCategories/api/createProductCategory.ts @@ -22,7 +22,7 @@ export const useCreateProductCategory = ({ mutationConfig }: UseCreateProductCat mutationFn: createProductCategory, onSuccess: (...args) => { queryClient.invalidateQueries({ - queryKey: getProductCategoriesQueryOptions().queryKey, + queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]], }); onSuccess?.(...args); }, diff --git a/src/features/productCategories/api/deleteProductCategort.ts b/src/features/productCategories/api/deleteProductCategort.ts index 9822470..0e2315c 100644 --- a/src/features/productCategories/api/deleteProductCategort.ts +++ b/src/features/productCategories/api/deleteProductCategort.ts @@ -21,7 +21,7 @@ export const useDeleteProductCategory = ({ mutationConfig }: UseDeleteProductCat mutationFn: deleteProductCategory, onSuccess: (...args) => { queryClient.invalidateQueries({ - queryKey: getProductCategoriesQueryOptions({}).queryKey, + queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]], }); onSuccess?.(...args); }, diff --git a/src/features/productCategories/api/getProductCategories.ts b/src/features/productCategories/api/getProductCategories.ts index ecc391a..eb7ef9c 100644 --- a/src/features/productCategories/api/getProductCategories.ts +++ b/src/features/productCategories/api/getProductCategories.ts @@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient'; import { ProductCategoryResponse } from '../types/api'; type ProductCategoriesQueryParams = { - q?: string; + search?: string | null; page?: number; limit?: number; }; -export const getProductCategories = ({ q, page, limit }: ProductCategoriesQueryParams = {}): Promise => { +export const getProductCategories = ({ search, page, limit }: ProductCategoriesQueryParams = {}): Promise => { 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({ - queryKey: ['productCategories', { q, page, limit }], - queryFn: () => getProductCategories({ q, page, limit }), + queryKey: ['productCategories', { search, page, limit }], + queryFn: () => getProductCategories({ search, page, limit }), }); }; @@ -26,9 +26,9 @@ type UseProductCategoriesOptions = { queryConfig?: QueryConfig; } & ProductCategoriesQueryParams; -export const useProductCategories = ({ q, page, limit, queryConfig }: UseProductCategoriesOptions = {}) => { +export const useProductCategories = ({ search, page, limit, queryConfig }: UseProductCategoriesOptions = {}) => { return useQuery({ - ...getProductCategoriesQueryOptions({ q, page, limit }), + ...getProductCategoriesQueryOptions({ search, page, limit }), ...queryConfig, }); }; diff --git a/src/features/productCategories/api/updateProductCategory.ts b/src/features/productCategories/api/updateProductCategory.ts index 0d21234..b4dd71b 100644 --- a/src/features/productCategories/api/updateProductCategory.ts +++ b/src/features/productCategories/api/updateProductCategory.ts @@ -22,7 +22,7 @@ export const useUpdateProductCategory = ({ mutationConfig }: UseUpdateProductCat mutationFn: updateProductCategory, onSuccess: (data, ...args) => { queryClient.invalidateQueries({ - queryKey: getProductCategoriesQueryOptions().queryKey, + queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]], }); onSuccess?.(data, ...args); }, diff --git a/src/features/productCategories/components/ProductCategoriesList.tsx b/src/features/productCategories/components/ProductCategoriesList.tsx index aa72933..6bb125c 100644 --- a/src/features/productCategories/components/ProductCategoriesList.tsx +++ b/src/features/productCategories/components/ProductCategoriesList.tsx @@ -8,6 +8,8 @@ import { ProductCategory } from '../types/api'; import { EditProductCategoryModal } from './EditProductCategoryModal'; import { DeleteProductCategoryModal } from './DeleteProductCategoryModal'; import { DetailProductCategoryModal } from './DetailProductCategoryModal'; +import { useQueryState } from 'nuqs'; +import { useDebounce } from '../../../hooks/useDebounce'; const PAGE_SIZES = [10, 20, 30, 50]; @@ -17,7 +19,10 @@ export const ProductCategoriesList = () => { const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); const [selectedCategory, setSelectedCategory] = useState(null); - const categoryQuery = useProductCategories(); + const [searchParam, setSearchParam] = useQueryState('search'); + const debouncedSearchParam = useDebounce(searchParam, 500); + + const categoryQuery = useProductCategories({ search: debouncedSearchParam }); const categories = categoryQuery.data ?? []; const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(categories, { @@ -25,10 +30,6 @@ export const ProductCategoriesList = () => { direction: 'desc', }); - const [search, setSearch] = useState(''); - - const filteredRecords = paginatedRecords.filter((category) => category.name.toLowerCase().includes(search.toLowerCase())); - const onDelete = (category: ProductCategory) => { setSelectedCategory(category); setIsDeleteModalOpen(true); @@ -47,12 +48,12 @@ export const ProductCategoriesList = () => { return ( <>
    - +
    void }) => { +export const ProductCategoryListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => { const { t } = useTranslation(); const [isOpenModalCreateCategory, setIsOpenModalCreateCategory] = useState(false); @@ -11,7 +11,7 @@ export const ProductCategoryListHeader = ({ search, setSearch }: { search: strin <>
    - setSearch(e.target.value)} /> + setSearch(e.target.value || null)} />