@goplusvn/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -175
- package/src/assets/erp_wallpaper.png +0 -0
- package/src/assets/goeat_logo.png +0 -0
- package/src/audit/audit-manager.ts +139 -0
- package/src/audit/index.ts +11 -0
- package/src/audit/memory-audit-logger.ts +86 -0
- package/src/audit/types.ts +50 -0
- package/src/auth/auth-service.ts +97 -0
- package/src/auth/index.ts +266 -0
- package/src/code-generation/index.ts +69 -0
- package/src/configs/auth-routes.ts +17 -0
- package/src/configs/crud.ts +136 -0
- package/src/configs/data/navigations.ts +781 -0
- package/src/configs/data/oauth-links.ts +10 -0
- package/src/configs/entities/material-categories.config.ts +125 -0
- package/src/configs/i18n.ts +12 -0
- package/src/configs/index.ts +26 -0
- package/src/configs/status.ts +25 -0
- package/src/configs/themes.ts +100 -0
- package/src/crud/components/crud-bulk-actions.tsx +91 -0
- package/src/crud/components/crud-card-view.tsx +241 -0
- package/src/crud/components/crud-context.tsx +122 -0
- package/src/crud/components/crud-delete-dialog.tsx +145 -0
- package/src/crud/components/crud-dialog.tsx +406 -0
- package/src/crud/components/crud-empty-state.tsx +104 -0
- package/src/crud/components/crud-export-button.tsx +170 -0
- package/src/crud/components/crud-field-renderer.tsx +653 -0
- package/src/crud/components/crud-filter-chips.tsx +102 -0
- package/src/crud/components/crud-filters/checkbox-filter.tsx +97 -0
- package/src/crud/components/crud-filters/datetime-filter.tsx +83 -0
- package/src/crud/components/crud-filters/filter-builder.tsx +66 -0
- package/src/crud/components/crud-filters/index.tsx +76 -0
- package/src/crud/components/crud-filters/radio-filter.tsx +86 -0
- package/src/crud/components/crud-filters/select-filter.tsx +141 -0
- package/src/crud/components/crud-filters/text-filter.tsx +86 -0
- package/src/crud/components/crud-form.tsx +642 -0
- package/src/crud/components/crud-import-dialog.tsx +440 -0
- package/src/crud/components/crud-infinite-scroll.tsx +116 -0
- package/src/crud/components/crud-page.tsx +1017 -0
- package/src/crud/components/crud-provider.tsx +277 -0
- package/src/crud/components/crud-row-actions.tsx +189 -0
- package/src/crud/components/crud-search.tsx +82 -0
- package/src/crud/components/crud-sheet.tsx +336 -0
- package/src/crud/components/crud-table-skeleton.tsx +26 -0
- package/src/crud/components/crud-table-toolbar.tsx +91 -0
- package/src/crud/components/crud-table.tsx +352 -0
- package/src/crud/components/crud-virtual-table.tsx +55 -0
- package/src/crud/components/index.tsx +20 -0
- package/src/crud/crud-filters/checkbox-filter.tsx +87 -0
- package/src/crud/crud-filters/datetime-filter.tsx +82 -0
- package/src/crud/crud-filters/filter-builder.tsx +64 -0
- package/src/crud/crud-filters/index.tsx +78 -0
- package/src/crud/crud-filters/radio-filter.tsx +79 -0
- package/src/crud/crud-filters/select-filter.tsx +148 -0
- package/src/crud/crud-filters/text-filter.tsx +81 -0
- package/src/crud/index.ts +43 -0
- package/src/crud/lib/crud-service.test.ts +334 -0
- package/src/crud/lib/crud-service.ts +358 -0
- package/src/crud/lib/crud-utils.test.ts +354 -0
- package/src/crud/lib/crud-utils.ts +299 -0
- package/src/crud/lib/crud-validator.ts +247 -0
- package/src/crud/lib/data-loader.ts +234 -0
- package/src/crud/lib/field-calculator.ts +241 -0
- package/src/crud/lib/field-formatter.ts +240 -0
- package/src/crud/lib/import-export-service.test.ts +290 -0
- package/src/crud/lib/import-export-service.ts +352 -0
- package/src/crud/lib/import-server-utils.ts +109 -0
- package/src/crud/lib/lazy-loader.ts +241 -0
- package/src/crud/lib/parse-filters.ts +85 -0
- package/src/crud/lib/permissions.ts +52 -0
- package/src/crud/lib/serialize-config.ts +60 -0
- package/src/crud/lib/stream-loader.ts +145 -0
- package/src/crud/lib/translate-config.ts +335 -0
- package/src/crud/lib/types.ts +11 -0
- package/src/crud/pages/entity-crud-page.tsx +144 -0
- package/src/crud/server.ts +8 -0
- package/src/home/constants.tsx +142 -0
- package/src/home/feature-showcase.tsx +171 -0
- package/src/home/home-page.tsx +191 -0
- package/src/home/hooks/index.ts +1 -0
- package/src/home/hooks/useWidgetPreferences.ts +167 -0
- package/src/home/index.ts +33 -0
- package/src/home/quick-access-dialog.tsx +271 -0
- package/src/home/quick-access-menu.tsx +267 -0
- package/src/home/types.ts +140 -0
- package/src/home/welcome-card.tsx +92 -0
- package/src/home/widget-container.tsx +258 -0
- package/src/home/widgets/base-widget.tsx +200 -0
- package/src/home/widgets/customers-widget.tsx +74 -0
- package/src/home/widgets/index.ts +6 -0
- package/src/home/widgets/orders-widget.tsx +87 -0
- package/src/home/widgets/revenue-widget.tsx +71 -0
- package/src/home/widgets/stock-widget.tsx +109 -0
- package/src/hooks/index.tsx +598 -0
- package/src/hooks/use-tenant.test.tsx +30 -0
- package/src/hooks/use-tenant.ts +5 -0
- package/src/index.ts +17 -0
- package/src/infrastructure/__tests__/architecture-verification.spec.ts +103 -0
- package/src/infrastructure/api-service.ts +317 -0
- package/src/infrastructure/cache/cache-manager.ts +107 -0
- package/src/infrastructure/cache/cache.ts +120 -0
- package/src/infrastructure/cache/index.ts +8 -0
- package/src/infrastructure/cache/types.ts +48 -0
- package/src/infrastructure/cron/cron-manager.ts +239 -0
- package/src/infrastructure/cron/index.ts +6 -0
- package/src/infrastructure/cron/types.ts +41 -0
- package/src/infrastructure/event-bus/event-bus.ts +145 -0
- package/src/infrastructure/event-bus/index.ts +2 -0
- package/src/infrastructure/event-bus/types.ts +22 -0
- package/src/infrastructure/index.ts +32 -0
- package/src/infrastructure/lock/decorators.ts +67 -0
- package/src/infrastructure/lock/index.ts +2 -0
- package/src/infrastructure/lock/lock-manager.ts +33 -0
- package/src/infrastructure/logger/index.ts +2 -0
- package/src/infrastructure/logger/logger.ts +96 -0
- package/src/infrastructure/logger/types.ts +25 -0
- package/src/layout/index.tsx +185 -0
- package/src/navigation/index.ts +91 -0
- package/src/notification/index.ts +14 -0
- package/src/notification/notification-service.ts +120 -0
- package/src/notification/storage/in-memory.ts +56 -0
- package/src/notification/storage/index.ts +1 -0
- package/src/notification/types.ts +51 -0
- package/src/organization/branch-service.ts +299 -0
- package/src/organization/branches.config.ts +154 -0
- package/src/organization/index.ts +5 -0
- package/src/plugin/apps-registry.ts +97 -0
- package/src/plugin/index.ts +5 -0
- package/src/plugin/types.ts +41 -0
- package/src/providers/index.tsx +109 -0
- package/src/providers/tenant-provider.tsx +45 -0
- package/src/rbac/components/roles/role-card.tsx +158 -0
- package/src/rbac/components/roles/role-stats-cards.tsx +29 -0
- package/src/rbac/components/roles/role-toolbar.tsx +123 -0
- package/src/rbac/hooks/use-role-operations.ts +159 -0
- package/src/rbac/hooks/use-roles-data.ts +59 -0
- package/src/rbac/index.ts +297 -0
- package/src/rbac/lib/permission-helpers.ts +63 -0
- package/src/rbac/pages/action-list-page.tsx +25 -0
- package/src/rbac/pages/resource-list-page.tsx +25 -0
- package/src/rbac/pages/role-list-page.tsx +378 -0
- package/src/rbac/permission-service.ts +140 -0
- package/src/rbac/permissions.ts +135 -0
- package/src/rbac/resource-service.ts +115 -0
- package/src/rbac/resource-validator.ts +119 -0
- package/src/rbac/role-service.ts +165 -0
- package/src/rbac/server.ts +16 -0
- package/src/rbac/types.ts +38 -0
- package/src/schemas/action.schema.ts +66 -0
- package/src/schemas/branch.schema.ts +52 -0
- package/src/schemas/coming-soon-schema.ts +9 -0
- package/src/schemas/company.schema.ts +44 -0
- package/src/schemas/forgot-passward-schema.ts +9 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/material-category.schema.ts +43 -0
- package/src/schemas/material-pricing.schema.ts +74 -0
- package/src/schemas/material.schema.ts +76 -0
- package/src/schemas/materials.ts +52 -0
- package/src/schemas/new-passward-schema.ts +15 -0
- package/src/schemas/partner-company.schema.ts +149 -0
- package/src/schemas/register-schema.ts +36 -0
- package/src/schemas/resource.schema.ts +133 -0
- package/src/schemas/role.schema.ts +11 -0
- package/src/schemas/sign-in-schema.ts +24 -0
- package/src/schemas/supplier-pricing.schema.ts +15 -0
- package/src/schemas/supplier.schema.ts +120 -0
- package/src/schemas/system-category-group.schema.ts +67 -0
- package/src/schemas/system-category.schema.ts +77 -0
- package/src/schemas/system-config.schema.ts +118 -0
- package/src/schemas/uom.schema.ts +75 -0
- package/src/schemas/user-supplier.schema.ts +179 -0
- package/src/schemas/user.schema.ts +18 -0
- package/src/schemas/verify-email-schema.ts +9 -0
- package/src/schemas/warehouse.schema.ts +49 -0
- package/src/system/components/categories/category-list.tsx +529 -0
- package/src/system/components/categories/category-manager.tsx +89 -0
- package/src/system/components/categories/group-sidebar.tsx +308 -0
- package/src/system/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/components/settings/setting-field.tsx +291 -0
- package/src/system/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/components/settings/settings-groups.ts +80 -0
- package/src/system/components/settings/settings-search.tsx +71 -0
- package/src/system/components/settings/settings-section.tsx +74 -0
- package/src/system/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/constants.ts +3 -0
- package/src/system/index.ts +150 -0
- package/src/system/job-manager.ts +176 -0
- package/src/system/pages/components/categories/category-list.tsx +537 -0
- package/src/system/pages/components/categories/category-manager.tsx +90 -0
- package/src/system/pages/components/categories/group-sidebar.tsx +311 -0
- package/src/system/pages/components/settings/sales-rules-settings.tsx +222 -0
- package/src/system/pages/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/pages/components/settings/setting-field.tsx +292 -0
- package/src/system/pages/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/pages/components/settings/settings-groups.ts +87 -0
- package/src/system/pages/components/settings/settings-page.tsx +372 -0
- package/src/system/pages/components/settings/settings-search.tsx +71 -0
- package/src/system/pages/components/settings/settings-section.tsx +74 -0
- package/src/system/pages/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/pages/components/settings/system-settings.tsx +244 -0
- package/src/system/pages/system-category-page.tsx +15 -0
- package/src/system/pages/system-settings-page.tsx +380 -0
- package/src/system/schemas/system-category-group.schema.ts +46 -0
- package/src/system/schemas/system-category.schema.ts +56 -0
- package/src/system/services/settings-service.ts +127 -0
- package/src/system/services/system-category-service.ts +63 -0
- package/src/system/types.ts +45 -0
- package/src/types/index.ts +703 -0
- package/src/ui/auth/auth-layout.tsx +135 -0
- package/src/ui/auth/forgot-password-form.tsx +98 -0
- package/src/ui/auth/index.tsx +7 -0
- package/src/ui/auth/new-password-form.tsx +107 -0
- package/src/ui/auth/oauth-links.tsx +30 -0
- package/src/ui/auth/register-form.tsx +202 -0
- package/src/ui/auth/sign-in-form.tsx +238 -0
- package/src/ui/auth/verify-email-form.tsx +104 -0
- package/src/ui/crud/index.tsx +10 -0
- package/src/ui/data-display/accordion.tsx +65 -0
- package/src/ui/data-display/aspect-ratio.tsx +11 -0
- package/src/ui/data-display/avatar.tsx +163 -0
- package/src/ui/data-display/bento-grid.tsx +77 -0
- package/src/ui/data-display/carousel.tsx +249 -0
- package/src/ui/data-display/chart.tsx +363 -0
- package/src/ui/data-display/code-block-highlight.tsx +54 -0
- package/src/ui/data-display/collapsible.tsx +42 -0
- package/src/ui/data-display/compact-stat-bar.tsx +149 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +255 -0
- package/src/ui/data-display/data-table/data-table-empty-state.tsx +133 -0
- package/src/ui/data-display/data-table/data-table-skeleton.tsx +145 -0
- package/src/ui/data-display/data-table/data-table-toolbar.tsx +353 -0
- package/src/ui/data-display/data-table/data-table.tsx +597 -0
- package/src/ui/data-display/data-table/index.ts +44 -0
- package/src/ui/data-display/data-table-column-header.tsx +75 -0
- package/src/ui/data-display/data-table-pagination.tsx +130 -0
- package/src/ui/data-display/data-table-view-options.tsx +59 -0
- package/src/ui/data-display/formatted-number-input.tsx +210 -0
- package/src/ui/data-display/highlight.tsx +20 -0
- package/src/ui/data-display/hover-card.tsx +48 -0
- package/src/ui/data-display/index.tsx +50 -0
- package/src/ui/data-display/iphone-15-pro.tsx +114 -0
- package/src/ui/data-display/kanban/index.ts +4 -0
- package/src/ui/data-display/kanban/kanban-board.tsx +192 -0
- package/src/ui/data-display/kanban/kanban-column.tsx +74 -0
- package/src/ui/data-display/kanban/kanban-item.tsx +50 -0
- package/src/ui/data-display/kanban/kanban-types.ts +21 -0
- package/src/ui/data-display/kpi-card.tsx +68 -0
- package/src/ui/data-display/media-grid.tsx +110 -0
- package/src/ui/data-display/safari.tsx +175 -0
- package/src/ui/data-display/show-more-text.tsx +55 -0
- package/src/ui/data-display/tabs.tsx +68 -0
- package/src/ui/data-display/timeline.tsx +256 -0
- package/src/ui/feedback/alert.tsx +60 -0
- package/src/ui/feedback/context-menu.tsx +245 -0
- package/src/ui/feedback/drawer.tsx +132 -0
- package/src/ui/feedback/error-dialog.tsx +273 -0
- package/src/ui/feedback/index.tsx +183 -0
- package/src/ui/feedback/progress.tsx +32 -0
- package/src/ui/feedback/sheet.tsx +148 -0
- package/src/ui/feedback/sonner.tsx +36 -0
- package/src/ui/forms/command.tsx +157 -0
- package/src/ui/forms/date-picker.tsx +73 -0
- package/src/ui/forms/date-range-picker.tsx +76 -0
- package/src/ui/forms/date-time-picker.tsx +109 -0
- package/src/ui/forms/editor/editor-menu-bar.tsx +394 -0
- package/src/ui/forms/editor/index.tsx +130 -0
- package/src/ui/forms/editor/multi-select-example.tsx +1234 -0
- package/src/ui/forms/emoji-picker.tsx +109 -0
- package/src/ui/forms/file-dropzone.tsx +169 -0
- package/src/ui/forms/file-thumbnail.tsx +29 -0
- package/src/ui/forms/index.tsx +201 -0
- package/src/ui/forms/input-file.tsx +99 -0
- package/src/ui/forms/input-group.tsx +46 -0
- package/src/ui/forms/input-otp.tsx +81 -0
- package/src/ui/forms/input-phone.tsx +172 -0
- package/src/ui/forms/input-spin.tsx +116 -0
- package/src/ui/forms/input-tags.tsx +219 -0
- package/src/ui/forms/input-time.tsx +42 -0
- package/src/ui/forms/multi-select.tsx +629 -0
- package/src/ui/forms/multiple-date-picker.tsx +74 -0
- package/src/ui/forms/radio-group.tsx +42 -0
- package/src/ui/forms/rating.tsx +158 -0
- package/src/ui/forms/time-picker.tsx +57 -0
- package/src/ui/index.tsx +17 -0
- package/src/ui/layout/animated-list.tsx +77 -0
- package/src/ui/layout/animated-sidebar.tsx +294 -0
- package/src/ui/layout/command-menu.tsx +355 -0
- package/src/ui/layout/customizer.tsx +324 -0
- package/src/ui/layout/footer.tsx +43 -0
- package/src/ui/layout/full-screen-toggle.tsx +52 -0
- package/src/ui/layout/header-breadcrumb.tsx +77 -0
- package/src/ui/layout/horizontal-layout-header.tsx +83 -0
- package/src/ui/layout/horizontal-layout.tsx +50 -0
- package/src/ui/layout/index.tsx +25 -0
- package/src/ui/layout/language-dropdown.tsx +103 -0
- package/src/ui/layout/logo.tsx +63 -0
- package/src/ui/layout/main-layout.tsx +57 -0
- package/src/ui/layout/mode-dropdown.tsx +58 -0
- package/src/ui/layout/notification-dropdown.tsx +127 -0
- package/src/ui/layout/page-tabs.tsx +306 -0
- package/src/ui/layout/route-cache.tsx +214 -0
- package/src/ui/layout/sidebar-group-icon-menu.tsx +195 -0
- package/src/ui/layout/sidebar.tsx +279 -0
- package/src/ui/layout/tab-content-cache.tsx +201 -0
- package/src/ui/layout/tab-navigation-provider.tsx +536 -0
- package/src/ui/layout/toggle-mobile-sidebar.tsx +33 -0
- package/src/ui/layout/top-bar-header-menubar.tsx +412 -0
- package/src/ui/layout/user-dropdown.tsx +188 -0
- package/src/ui/layout/vertical-layout-header.tsx +65 -0
- package/src/ui/layout/vertical-layout.tsx +47 -0
- package/src/ui/management/audit-log-page.tsx +209 -0
- package/src/ui/management/cache-management.tsx +349 -0
- package/src/ui/management/index.ts +3 -0
- package/src/ui/management/job-management.tsx +308 -0
- package/src/ui/pages/not-found.tsx +30 -0
- package/src/ui/primitives/badge.tsx +66 -0
- package/src/ui/primitives/breadcrumb.tsx +103 -0
- package/src/ui/primitives/button.tsx +129 -0
- package/src/ui/primitives/calendar.tsx +74 -0
- package/src/ui/primitives/card.tsx +86 -0
- package/src/ui/primitives/checkbox.tsx +31 -0
- package/src/ui/primitives/client.ts +30 -0
- package/src/ui/primitives/combobox.tsx +290 -0
- package/src/ui/primitives/dialog.tsx +121 -0
- package/src/ui/primitives/dropdown-menu.tsx +239 -0
- package/src/ui/primitives/dynamic-icon.tsx +24 -0
- package/src/ui/primitives/index.tsx +134 -0
- package/src/ui/primitives/input-number.tsx +131 -0
- package/src/ui/primitives/input.tsx +22 -0
- package/src/ui/primitives/keyboard.tsx +23 -0
- package/src/ui/primitives/label.tsx +24 -0
- package/src/ui/primitives/menubar.tsx +262 -0
- package/src/ui/primitives/navigation-menu.tsx +157 -0
- package/src/ui/primitives/pagination.tsx +118 -0
- package/src/ui/primitives/popover.tsx +56 -0
- package/src/ui/primitives/prefetch-link.tsx +60 -0
- package/src/ui/primitives/resizable.tsx +59 -0
- package/src/ui/primitives/scroll-area.tsx +63 -0
- package/src/ui/primitives/select.tsx +172 -0
- package/src/ui/primitives/separator.tsx +51 -0
- package/src/ui/primitives/sidebar.tsx +844 -0
- package/src/ui/primitives/slider.tsx +27 -0
- package/src/ui/primitives/status-badge.tsx +47 -0
- package/src/ui/primitives/sticky-layout.tsx +50 -0
- package/src/ui/primitives/switch.tsx +29 -0
- package/src/ui/primitives/table.tsx +116 -0
- package/src/ui/primitives/tabs.tsx +55 -0
- package/src/ui/primitives/toggle-group.tsx +70 -0
- package/src/ui/primitives/toggle.tsx +47 -0
- package/src/ui/primitives/tooltip.tsx +59 -0
- package/src/user/components/dangerous-zone.tsx +34 -0
- package/src/user/components/delete-account-form.tsx +40 -0
- package/src/user/components/index.ts +4 -0
- package/src/user/components/profile-info-form.tsx +390 -0
- package/src/user/components/profile-info.tsx +32 -0
- package/src/user/components/unified-profile-dialog.tsx +1019 -0
- package/src/user/components/user-stats.tsx +27 -0
- package/src/user/components/user-toolbar.tsx +137 -0
- package/src/user/components/users-card-view.tsx +253 -0
- package/src/user/index.ts +11 -0
- package/src/user/pages/user-list-page.tsx +234 -0
- package/src/user/pages/users-client-page.tsx +385 -0
- package/src/user/profile-page.tsx +19 -0
- package/src/user/schemas.ts +68 -0
- package/src/user/types.ts +34 -0
- package/src/user/user-service.ts +538 -0
- package/src/utils/index.ts +906 -0
- package/src/workflow/activity-timeline.tsx +412 -0
- package/src/workflow/approval-workflow.tsx +31 -0
- package/src/workflow/index.ts +2 -0
- package/dist/audit/index.d.mts +0 -115
- package/dist/audit/index.d.ts +0 -115
- package/dist/audit/index.js +0 -204
- package/dist/audit/index.js.map +0 -1
- package/dist/audit/index.mjs +0 -200
- package/dist/audit/index.mjs.map +0 -1
- package/dist/auth/index.d.mts +0 -86
- package/dist/auth/index.d.ts +0 -86
- package/dist/auth/index.js +0 -210
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/index.mjs +0 -198
- package/dist/auth/index.mjs.map +0 -1
- package/dist/button-1dWvP9Ib.d.mts +0 -30
- package/dist/button-1dWvP9Ib.d.ts +0 -30
- package/dist/calendar-2QzdEo1z.d.mts +0 -20
- package/dist/calendar-2QzdEo1z.d.ts +0 -20
- package/dist/code-generation/index.d.mts +0 -30
- package/dist/code-generation/index.d.ts +0 -30
- package/dist/code-generation/index.js +0 -31
- package/dist/code-generation/index.js.map +0 -1
- package/dist/code-generation/index.mjs +0 -28
- package/dist/code-generation/index.mjs.map +0 -1
- package/dist/configs/index.d.mts +0 -175
- package/dist/configs/index.d.ts +0 -175
- package/dist/configs/index.js +0 -254
- package/dist/configs/index.js.map +0 -1
- package/dist/configs/index.mjs +0 -233
- package/dist/configs/index.mjs.map +0 -1
- package/dist/crud/index.d.mts +0 -646
- package/dist/crud/index.d.ts +0 -646
- package/dist/crud/index.js +0 -11772
- package/dist/crud/index.js.map +0 -1
- package/dist/crud/index.mjs +0 -11665
- package/dist/crud/index.mjs.map +0 -1
- package/dist/crud/server.d.mts +0 -20
- package/dist/crud/server.d.ts +0 -20
- package/dist/crud/server.js +0 -123
- package/dist/crud/server.js.map +0 -1
- package/dist/crud/server.mjs +0 -120
- package/dist/crud/server.mjs.map +0 -1
- package/dist/data-table-skeleton-12NA8Mjx.d.mts +0 -39
- package/dist/data-table-skeleton-12NA8Mjx.d.ts +0 -39
- package/dist/dialog-bKfjZMTd.d.mts +0 -22
- package/dist/dialog-bKfjZMTd.d.ts +0 -22
- package/dist/dynamic-icon-DrGIiu2N.d.mts +0 -10
- package/dist/dynamic-icon-DrGIiu2N.d.ts +0 -10
- package/dist/home/index.d.mts +0 -269
- package/dist/home/index.d.ts +0 -269
- package/dist/home/index.js +0 -1678
- package/dist/home/index.js.map +0 -1
- package/dist/home/index.mjs +0 -1635
- package/dist/home/index.mjs.map +0 -1
- package/dist/hooks/index.d.mts +0 -7
- package/dist/hooks/index.d.ts +0 -7
- package/dist/hooks/index.js +0 -8316
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/index.mjs +0 -8255
- package/dist/hooks/index.mjs.map +0 -1
- package/dist/index-50hpiPrV.d.ts +0 -116
- package/dist/index-B9zQVEVi.d.mts +0 -116
- package/dist/index.d.mts +0 -5
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -123
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -118
- package/dist/index.mjs.map +0 -1
- package/dist/infrastructure/index.d.mts +0 -423
- package/dist/infrastructure/index.d.ts +0 -423
- package/dist/infrastructure/index.js +0 -633
- package/dist/infrastructure/index.js.map +0 -1
- package/dist/infrastructure/index.mjs +0 -619
- package/dist/infrastructure/index.mjs.map +0 -1
- package/dist/label-DWTEkNPo.d.ts +0 -226
- package/dist/label-LPpdcoBx.d.mts +0 -226
- package/dist/layout/index.d.mts +0 -48
- package/dist/layout/index.d.ts +0 -48
- package/dist/layout/index.js +0 -117
- package/dist/layout/index.js.map +0 -1
- package/dist/layout/index.mjs +0 -90
- package/dist/layout/index.mjs.map +0 -1
- package/dist/navigation/index.d.mts +0 -16
- package/dist/navigation/index.d.ts +0 -16
- package/dist/navigation/index.js +0 -53
- package/dist/navigation/index.js.map +0 -1
- package/dist/navigation/index.mjs +0 -50
- package/dist/navigation/index.mjs.map +0 -1
- package/dist/notification/index.d.mts +0 -105
- package/dist/notification/index.d.ts +0 -105
- package/dist/notification/index.js +0 -278
- package/dist/notification/index.js.map +0 -1
- package/dist/notification/index.mjs +0 -274
- package/dist/notification/index.mjs.map +0 -1
- package/dist/organization/index.d.mts +0 -99
- package/dist/organization/index.d.ts +0 -99
- package/dist/organization/index.js +0 -360
- package/dist/organization/index.js.map +0 -1
- package/dist/organization/index.mjs +0 -352
- package/dist/organization/index.mjs.map +0 -1
- package/dist/plugin/index.d.mts +0 -83
- package/dist/plugin/index.d.ts +0 -83
- package/dist/plugin/index.js +0 -86
- package/dist/plugin/index.js.map +0 -1
- package/dist/plugin/index.mjs +0 -84
- package/dist/plugin/index.mjs.map +0 -1
- package/dist/providers/index.d.mts +0 -25
- package/dist/providers/index.d.ts +0 -25
- package/dist/providers/index.js +0 -84
- package/dist/providers/index.js.map +0 -1
- package/dist/providers/index.mjs +0 -77
- package/dist/providers/index.mjs.map +0 -1
- package/dist/rbac/index.d.mts +0 -226
- package/dist/rbac/index.d.ts +0 -226
- package/dist/rbac/index.js +0 -4784
- package/dist/rbac/index.js.map +0 -1
- package/dist/rbac/index.mjs +0 -4722
- package/dist/rbac/index.mjs.map +0 -1
- package/dist/rbac/permissions.d.mts +0 -26
- package/dist/rbac/permissions.d.ts +0 -26
- package/dist/rbac/permissions.js +0 -94
- package/dist/rbac/permissions.js.map +0 -1
- package/dist/rbac/permissions.mjs +0 -90
- package/dist/rbac/permissions.mjs.map +0 -1
- package/dist/rbac/server.d.mts +0 -1
- package/dist/rbac/server.d.ts +0 -1
- package/dist/rbac/server.js +0 -128
- package/dist/rbac/server.js.map +0 -1
- package/dist/rbac/server.mjs +0 -124
- package/dist/rbac/server.mjs.map +0 -1
- package/dist/schemas/index.d.mts +0 -1257
- package/dist/schemas/index.d.ts +0 -1257
- package/dist/schemas/index.js +0 -572
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/index.mjs +0 -523
- package/dist/schemas/index.mjs.map +0 -1
- package/dist/server-QuYCTa89.d.mts +0 -83
- package/dist/server-QuYCTa89.d.ts +0 -83
- package/dist/sonner-C74GlRDQ.d.mts +0 -71
- package/dist/sonner-C74GlRDQ.d.ts +0 -71
- package/dist/status-BOXZgIqX.d.mts +0 -12
- package/dist/status-BOXZgIqX.d.ts +0 -12
- package/dist/system/index.d.mts +0 -77
- package/dist/system/index.d.ts +0 -77
- package/dist/system/index.js +0 -102
- package/dist/system/index.js.map +0 -1
- package/dist/system/index.mjs +0 -100
- package/dist/system/index.mjs.map +0 -1
- package/dist/tabs-C6FfBwPY.d.mts +0 -18
- package/dist/tabs-C6FfBwPY.d.ts +0 -18
- package/dist/tenant-provider-B8eC_Wpb.d.mts +0 -27
- package/dist/tenant-provider-B8eC_Wpb.d.ts +0 -27
- package/dist/types/index.d.mts +0 -469
- package/dist/types/index.d.ts +0 -469
- package/dist/types/index.js +0 -25
- package/dist/types/index.js.map +0 -1
- package/dist/types/index.mjs +0 -21
- package/dist/types/index.mjs.map +0 -1
- package/dist/ui/auth.d.mts +0 -39
- package/dist/ui/auth.d.ts +0 -39
- package/dist/ui/auth.js +0 -4941
- package/dist/ui/auth.js.map +0 -1
- package/dist/ui/auth.mjs +0 -4896
- package/dist/ui/auth.mjs.map +0 -1
- package/dist/ui/crud.d.mts +0 -2
- package/dist/ui/crud.d.ts +0 -2
- package/dist/ui/crud.js +0 -4
- package/dist/ui/crud.js.map +0 -1
- package/dist/ui/crud.mjs +0 -3
- package/dist/ui/crud.mjs.map +0 -1
- package/dist/ui/data-display.d.mts +0 -596
- package/dist/ui/data-display.d.ts +0 -596
- package/dist/ui/data-display.js +0 -5307
- package/dist/ui/data-display.js.map +0 -1
- package/dist/ui/data-display.mjs +0 -5212
- package/dist/ui/data-display.mjs.map +0 -1
- package/dist/ui/feedback.d.mts +0 -55
- package/dist/ui/feedback.d.ts +0 -55
- package/dist/ui/feedback.js +0 -2608
- package/dist/ui/feedback.js.map +0 -1
- package/dist/ui/feedback.mjs +0 -2526
- package/dist/ui/feedback.mjs.map +0 -1
- package/dist/ui/forms.d.mts +0 -309
- package/dist/ui/forms.d.ts +0 -309
- package/dist/ui/forms.js +0 -4656
- package/dist/ui/forms.js.map +0 -1
- package/dist/ui/forms.mjs +0 -4571
- package/dist/ui/forms.mjs.map +0 -1
- package/dist/ui/index.d.mts +0 -331
- package/dist/ui/index.d.ts +0 -331
- package/dist/ui/index.js +0 -16953
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/index.mjs +0 -16598
- package/dist/ui/index.mjs.map +0 -1
- package/dist/ui/primitives/client.d.mts +0 -61
- package/dist/ui/primitives/client.d.ts +0 -61
- package/dist/ui/primitives/client.js +0 -3408
- package/dist/ui/primitives/client.js.map +0 -1
- package/dist/ui/primitives/client.mjs +0 -3256
- package/dist/ui/primitives/client.mjs.map +0 -1
- package/dist/ui/primitives.d.mts +0 -113
- package/dist/ui/primitives.d.ts +0 -113
- package/dist/ui/primitives.js +0 -3356
- package/dist/ui/primitives.js.map +0 -1
- package/dist/ui/primitives.mjs +0 -3227
- package/dist/ui/primitives.mjs.map +0 -1
- package/dist/user/index.d.mts +0 -228
- package/dist/user/index.d.ts +0 -228
- package/dist/user/index.js +0 -4306
- package/dist/user/index.js.map +0 -1
- package/dist/user/index.mjs +0 -4260
- package/dist/user/index.mjs.map +0 -1
- package/dist/utils/index.d.mts +0 -205
- package/dist/utils/index.d.ts +0 -205
- package/dist/utils/index.js +0 -574
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/index.mjs +0 -514
- package/dist/utils/index.mjs.map +0 -1
- package/dist/workflow/index.d.mts +0 -40
- package/dist/workflow/index.d.ts +0 -40
- package/dist/workflow/index.js +0 -3710
- package/dist/workflow/index.js.map +0 -1
- package/dist/workflow/index.mjs +0 -3677
- package/dist/workflow/index.mjs.map +0 -1
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useCallback,
|
|
9
|
+
Suspense,
|
|
10
|
+
} from "react";
|
|
11
|
+
import dynamic from "next/dynamic";
|
|
12
|
+
import { Card, CardContent, CardHeader } from "../../ui";
|
|
13
|
+
|
|
14
|
+
import { Button } from "../../ui";
|
|
15
|
+
import { globalError } from "../../ui/feedback/error-dialog";
|
|
16
|
+
import { Separator } from "../../ui";
|
|
17
|
+
import { Badge } from "../../ui";
|
|
18
|
+
import { DynamicIcon } from "../../ui";
|
|
19
|
+
import { Plus, Loader2 } from "lucide-react";
|
|
20
|
+
import { toast } from "sonner";
|
|
21
|
+
import type {
|
|
22
|
+
EntityConfig,
|
|
23
|
+
CrudPermissions,
|
|
24
|
+
CrudResponse,
|
|
25
|
+
RowAction,
|
|
26
|
+
} from "../../types";
|
|
27
|
+
import { CrudProvider } from "./crud-provider";
|
|
28
|
+
import { useCrudConfig, useCrudState } from "./crud-context";
|
|
29
|
+
import { CrudTable } from "./crud-table";
|
|
30
|
+
import { CrudTableToolbar } from "./crud-table-toolbar";
|
|
31
|
+
import { useRouter, useParams } from "next/navigation";
|
|
32
|
+
import { CrudBulkActions } from "./crud-bulk-actions";
|
|
33
|
+
import { CrudDeleteDialog } from "./crud-delete-dialog";
|
|
34
|
+
import { crudService } from "../lib/crud-service";
|
|
35
|
+
import { usePrefetch } from "../../hooks";
|
|
36
|
+
import type { DictionaryType } from "../../hooks";
|
|
37
|
+
|
|
38
|
+
// ✅ Defer non-critical features - CardView only loads when needed
|
|
39
|
+
const CrudCardView = dynamic(
|
|
40
|
+
() => import("./crud-card-view").then((m) => ({ default: m.CrudCardView })),
|
|
41
|
+
{
|
|
42
|
+
loading: () => (
|
|
43
|
+
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
|
44
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
45
|
+
<Card key={`skeleton-card-${index}`} className="animate-pulse">
|
|
46
|
+
<CardHeader className="pb-3">
|
|
47
|
+
<div className="flex items-start gap-2">
|
|
48
|
+
<div className="h-4 w-4 bg-muted rounded shrink-0 mt-0.5" />
|
|
49
|
+
<div className="flex-1 space-y-2">
|
|
50
|
+
<div className="h-4 w-3/4 bg-muted rounded" />
|
|
51
|
+
<div className="h-3 w-1/2 bg-muted rounded" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</CardHeader>
|
|
55
|
+
<CardContent className="pt-0">
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<div className="h-4 w-full bg-muted rounded" />
|
|
58
|
+
<div className="h-4 w-2/3 bg-muted rounded" />
|
|
59
|
+
</div>
|
|
60
|
+
</CardContent>
|
|
61
|
+
</Card>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
ssr: false,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// ✅ Dynamic imports for code splitting - only load when needed
|
|
70
|
+
const CrudDialog = dynamic(
|
|
71
|
+
() => import("./crud-dialog").then((m) => ({ default: m.CrudDialog })),
|
|
72
|
+
{
|
|
73
|
+
loading: () => (
|
|
74
|
+
<div className="flex items-center justify-center p-4">
|
|
75
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
ssr: false, // Dialog doesn't need SSR
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const CrudSheet = dynamic(
|
|
83
|
+
() => import("./crud-sheet").then((m) => ({ default: m.CrudSheet })),
|
|
84
|
+
{
|
|
85
|
+
loading: () => (
|
|
86
|
+
<div className="flex items-center justify-center p-4">
|
|
87
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
88
|
+
</div>
|
|
89
|
+
),
|
|
90
|
+
ssr: false, // Sheet doesn't need SSR
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const CrudImportDialog = dynamic(
|
|
95
|
+
() =>
|
|
96
|
+
import("./crud-import-dialog").then((m) => ({
|
|
97
|
+
default: m.CrudImportDialog,
|
|
98
|
+
})),
|
|
99
|
+
{
|
|
100
|
+
ssr: false, // Import dialog doesn't need SSR
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const CrudExportButton = dynamic(
|
|
105
|
+
() =>
|
|
106
|
+
import("./crud-export-button").then((m) => ({
|
|
107
|
+
default: m.CrudExportButton,
|
|
108
|
+
})),
|
|
109
|
+
{
|
|
110
|
+
ssr: false, // Export button doesn't need SSR
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
interface CrudPageContentProps {
|
|
115
|
+
config: EntityConfig;
|
|
116
|
+
permissions: CrudPermissions;
|
|
117
|
+
entityName?: string; // Entity name from URL route (e.g., "suppliers", "warehouses")
|
|
118
|
+
dictionary: DictionaryType;
|
|
119
|
+
customActions?: React.ReactNode;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function CrudPageContent({
|
|
123
|
+
config,
|
|
124
|
+
permissions,
|
|
125
|
+
entityName,
|
|
126
|
+
dictionary,
|
|
127
|
+
customActions,
|
|
128
|
+
}: CrudPageContentProps) {
|
|
129
|
+
|
|
130
|
+
const router = useRouter();
|
|
131
|
+
const params = useParams();
|
|
132
|
+
const lang = (params?.lang as string) || "vi";
|
|
133
|
+
const { prefetchNextPage } = usePrefetch();
|
|
134
|
+
|
|
135
|
+
// Get CRUD translations from dictionary
|
|
136
|
+
const crudDict = (dictionary as Record<string, unknown>).crud as
|
|
137
|
+
| Record<string, unknown>
|
|
138
|
+
| undefined;
|
|
139
|
+
const commonDict = (crudDict?.common as Record<string, string>) || {};
|
|
140
|
+
const messagesDict = (crudDict?.messages as Record<string, string>) || {};
|
|
141
|
+
|
|
142
|
+
// Create translations object for child components
|
|
143
|
+
const translations = {
|
|
144
|
+
edit: commonDict.edit || "Edit",
|
|
145
|
+
delete: commonDict.delete || "Delete",
|
|
146
|
+
save: commonDict.save || "Save",
|
|
147
|
+
cancel: commonDict.cancel || "Cancel",
|
|
148
|
+
create: commonDict.create || "Create",
|
|
149
|
+
update: commonDict.update || "Update",
|
|
150
|
+
creating: commonDict.creating || "Creating...",
|
|
151
|
+
updating: commonDict.updating || "Updating...",
|
|
152
|
+
deleting: commonDict.deleting || "Deleting...",
|
|
153
|
+
savedSuccessfully: commonDict.savedSuccessfully || "Saved successfully!",
|
|
154
|
+
createdSuccessfully:
|
|
155
|
+
commonDict.createdSuccessfully || "Created successfully!",
|
|
156
|
+
updatedSuccessfully:
|
|
157
|
+
commonDict.updatedSuccessfully || "Updated successfully!",
|
|
158
|
+
confirmDeleteTitle: commonDict.confirmDeleteTitle,
|
|
159
|
+
confirmBulkDeleteTitle: commonDict.confirmBulkDeleteTitle,
|
|
160
|
+
confirmDeleteDescription: commonDict.confirmDeleteDescription,
|
|
161
|
+
confirmBulkDeleteDescription: commonDict.confirmBulkDeleteDescription,
|
|
162
|
+
deleteSelected: commonDict.deleteSelected || "Delete selected",
|
|
163
|
+
actions: commonDict.actions || "Actions",
|
|
164
|
+
add: commonDict.add || "Add",
|
|
165
|
+
addNew: commonDict.addNew || "Add New",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Helper function to get translation with fallback
|
|
169
|
+
// Supports dot notation for nested keys (e.g., "crud.common.options.active")
|
|
170
|
+
const t = useCallback(
|
|
171
|
+
(key: string, fallback: string): string => {
|
|
172
|
+
if (!key) return fallback;
|
|
173
|
+
|
|
174
|
+
// 1. Try simple lookup in common dict first (legacy behavior)
|
|
175
|
+
if (commonDict[key]) return commonDict[key];
|
|
176
|
+
|
|
177
|
+
// 2. Try deep lookup in full dictionary
|
|
178
|
+
const keys = key.split(".");
|
|
179
|
+
let current: any = dictionary;
|
|
180
|
+
|
|
181
|
+
for (const k of keys) {
|
|
182
|
+
if (current === undefined || current === null) break;
|
|
183
|
+
current = current[k];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (current && typeof current === "string") {
|
|
187
|
+
return current;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return fallback;
|
|
191
|
+
},
|
|
192
|
+
[commonDict, dictionary],
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Helper function to get message translation with parameters
|
|
196
|
+
const tMessage = useCallback(
|
|
197
|
+
(key: string, params?: Record<string, string | number>): string => {
|
|
198
|
+
let message = messagesDict[key] || key;
|
|
199
|
+
if (params) {
|
|
200
|
+
Object.entries(params).forEach(([param, value]) => {
|
|
201
|
+
message = message.replace(
|
|
202
|
+
new RegExp(`\\{\\{${param}\\}\\}`, "g"),
|
|
203
|
+
String(value),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return message;
|
|
208
|
+
},
|
|
209
|
+
[messagesDict],
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Get form modes from config (default to "dialog")
|
|
213
|
+
const createMode = config.formMode?.create || "dialog";
|
|
214
|
+
const editMode = config.formMode?.edit || "dialog";
|
|
215
|
+
const sheetSide = config.formMode?.sheetSide || "right";
|
|
216
|
+
|
|
217
|
+
// Load view mode from localStorage or default to "table"
|
|
218
|
+
// ✅ Fix hydration mismatch: Initialize with default value, sync with localStorage in useEffect
|
|
219
|
+
const [viewMode, setViewMode] = useState<"table" | "card">("table");
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (typeof window !== "undefined") {
|
|
223
|
+
const saved = localStorage.getItem(`crud-view-mode-${config.name}`);
|
|
224
|
+
if (saved === "table" || saved === "card") {
|
|
225
|
+
setViewMode(saved);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}, [config.name]);
|
|
229
|
+
|
|
230
|
+
const handleViewModeChange = (mode: "table" | "card") => {
|
|
231
|
+
setViewMode(mode);
|
|
232
|
+
if (typeof window !== "undefined") {
|
|
233
|
+
localStorage.setItem(`crud-view-mode-${config.name}`, mode);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const { setConfig, setPermissions } = useCrudConfig();
|
|
238
|
+
|
|
239
|
+
const {
|
|
240
|
+
getQueryParams,
|
|
241
|
+
pagination,
|
|
242
|
+
setPage,
|
|
243
|
+
search,
|
|
244
|
+
sorting,
|
|
245
|
+
filters,
|
|
246
|
+
setSearch,
|
|
247
|
+
clearFilters,
|
|
248
|
+
} = useCrudState();
|
|
249
|
+
const [data, setData] = useState<CrudResponse>({
|
|
250
|
+
data: [],
|
|
251
|
+
total: 0,
|
|
252
|
+
page: 1,
|
|
253
|
+
pageSize: 10,
|
|
254
|
+
});
|
|
255
|
+
const [loading, setLoading] = useState(true);
|
|
256
|
+
const [tableLoading, setTableLoading] = useState(false); // Separate loading state for table only
|
|
257
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
258
|
+
const [editingRowId, setEditingRowId] = useState<string | null>(null);
|
|
259
|
+
const [tableInstance, setTableInstance] = useState<any>(null);
|
|
260
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
261
|
+
const [deletingRowId, setDeletingRowId] = useState<string | null>(null);
|
|
262
|
+
const [bulkDeleteIds, setBulkDeleteIds] = useState<string[]>([]);
|
|
263
|
+
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
264
|
+
const currentRequestRef = useRef<number>(0);
|
|
265
|
+
const hasInitializedRef = useRef<boolean>(false);
|
|
266
|
+
const previousQueryParamsRef = useRef<string>("");
|
|
267
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
268
|
+
|
|
269
|
+
// Initialize config and permissions
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
setConfig(config);
|
|
272
|
+
setPermissions(permissions);
|
|
273
|
+
}, [config, permissions, setConfig, setPermissions]);
|
|
274
|
+
|
|
275
|
+
// Memoize filters string to avoid unnecessary re-renders
|
|
276
|
+
const filtersKey = useMemo(() => {
|
|
277
|
+
// Sort a copy to avoid mutating the original array
|
|
278
|
+
const sortedFilters = [...filters].sort((a, b) =>
|
|
279
|
+
a.name.localeCompare(b.name),
|
|
280
|
+
);
|
|
281
|
+
return JSON.stringify(sortedFilters);
|
|
282
|
+
}, [filters]);
|
|
283
|
+
|
|
284
|
+
// Memoize query params string to detect actual changes
|
|
285
|
+
const queryParamsString = useMemo(() => {
|
|
286
|
+
return JSON.stringify({
|
|
287
|
+
page: pagination.page,
|
|
288
|
+
pageSize: pagination.pageSize,
|
|
289
|
+
search: search || "",
|
|
290
|
+
sortField: sorting?.field || "",
|
|
291
|
+
sortDirection: sorting?.direction || "",
|
|
292
|
+
filters: filtersKey, // Use filtersKey which is already a JSON string
|
|
293
|
+
});
|
|
294
|
+
}, [
|
|
295
|
+
pagination.page,
|
|
296
|
+
pagination.pageSize,
|
|
297
|
+
search,
|
|
298
|
+
sorting?.field,
|
|
299
|
+
sorting?.direction,
|
|
300
|
+
filtersKey,
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
// Fetch data with race condition protection and cancellation
|
|
304
|
+
const fetchData = useCallback(
|
|
305
|
+
async (isInitialLoad = false) => {
|
|
306
|
+
// Increment request ID to track the latest request
|
|
307
|
+
const requestId = ++currentRequestRef.current;
|
|
308
|
+
|
|
309
|
+
// Cancel previous request if exists
|
|
310
|
+
if (abortControllerRef.current) {
|
|
311
|
+
abortControllerRef.current.abort();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Create new abort controller
|
|
315
|
+
const abortController = new AbortController();
|
|
316
|
+
abortControllerRef.current = abortController;
|
|
317
|
+
|
|
318
|
+
// Use full loading for initial load, table loading for pagination
|
|
319
|
+
if (isInitialLoad) {
|
|
320
|
+
setLoading(true);
|
|
321
|
+
} else {
|
|
322
|
+
setTableLoading(true);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const queryParams = getQueryParams();
|
|
327
|
+
const response = await crudService.fetch(
|
|
328
|
+
config.apiEndpoint,
|
|
329
|
+
queryParams,
|
|
330
|
+
abortController.signal,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Only set data if this is still the latest request
|
|
334
|
+
if (requestId === currentRequestRef.current) {
|
|
335
|
+
setData(response);
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
// Ignore abort errors
|
|
339
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Only handle error if this is still the latest request
|
|
344
|
+
if (requestId === currentRequestRef.current) {
|
|
345
|
+
console.error("Error fetching data:", error);
|
|
346
|
+
// Set empty data on error
|
|
347
|
+
setData({
|
|
348
|
+
data: [],
|
|
349
|
+
total: 0,
|
|
350
|
+
page: 1,
|
|
351
|
+
pageSize: 10,
|
|
352
|
+
});
|
|
353
|
+
// Show toast notification to user
|
|
354
|
+
// Show error dialog to user
|
|
355
|
+
globalError.emit({
|
|
356
|
+
title: t("error", "Error"),
|
|
357
|
+
description: tMessage("loadFailed", {}),
|
|
358
|
+
trace: error instanceof Error ? error.stack : undefined,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
} finally {
|
|
362
|
+
// Only update loading state if this is still the latest request
|
|
363
|
+
if (requestId === currentRequestRef.current) {
|
|
364
|
+
if (isInitialLoad) {
|
|
365
|
+
setLoading(false);
|
|
366
|
+
} else {
|
|
367
|
+
setTableLoading(false);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
[config.apiEndpoint, getQueryParams, t, tMessage],
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Initial load - only run once on mount
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
if (!hasInitializedRef.current) {
|
|
378
|
+
hasInitializedRef.current = true;
|
|
379
|
+
// Initialize previousQueryParamsRef with current query params
|
|
380
|
+
previousQueryParamsRef.current = queryParamsString;
|
|
381
|
+
fetchData(true);
|
|
382
|
+
}
|
|
383
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
384
|
+
}, []); // Only run on mount
|
|
385
|
+
|
|
386
|
+
// Listen for external refresh events (e.g., from role dialog)
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
const handleRefresh = () => {
|
|
389
|
+
fetchData(false);
|
|
390
|
+
};
|
|
391
|
+
window.addEventListener("crudRefreshData", handleRefresh);
|
|
392
|
+
return () => {
|
|
393
|
+
window.removeEventListener("crudRefreshData", handleRefresh);
|
|
394
|
+
};
|
|
395
|
+
}, [fetchData]);
|
|
396
|
+
|
|
397
|
+
// ✅ Memoize pageCount to avoid recalculation
|
|
398
|
+
const pageCount = useMemo(() => {
|
|
399
|
+
const total = data.total || 0;
|
|
400
|
+
const pageSize = pagination.pageSize || 10;
|
|
401
|
+
const count = Math.ceil(total / pageSize);
|
|
402
|
+
return count > 0 ? count : 1;
|
|
403
|
+
}, [data.total, pagination.pageSize]);
|
|
404
|
+
|
|
405
|
+
// ✅ Prefetch next page when near the end
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
// Skip prefetch if:
|
|
408
|
+
// - Not initialized yet
|
|
409
|
+
// - Still loading
|
|
410
|
+
// - On last page or beyond
|
|
411
|
+
// - Only one page (no need to prefetch)
|
|
412
|
+
// - On first page (don't prefetch immediately on initial load)
|
|
413
|
+
if (
|
|
414
|
+
!hasInitializedRef.current ||
|
|
415
|
+
loading ||
|
|
416
|
+
tableLoading ||
|
|
417
|
+
pagination.page >= pageCount ||
|
|
418
|
+
pageCount <= 1 ||
|
|
419
|
+
pagination.page === 1 // Don't prefetch on first page load
|
|
420
|
+
) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Prefetch next page when we're within 1 page of the end
|
|
425
|
+
// Only prefetch if there's actually a next page
|
|
426
|
+
if (pagination.page >= pageCount - 1 && pagination.page < pageCount) {
|
|
427
|
+
const queryParams = getQueryParams();
|
|
428
|
+
prefetchNextPage(
|
|
429
|
+
config.apiEndpoint,
|
|
430
|
+
pagination.page,
|
|
431
|
+
pagination.pageSize,
|
|
432
|
+
{
|
|
433
|
+
search: queryParams.search,
|
|
434
|
+
sort: queryParams.sort,
|
|
435
|
+
filters: queryParams.filters,
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}, [
|
|
440
|
+
pagination.page,
|
|
441
|
+
pageCount,
|
|
442
|
+
loading,
|
|
443
|
+
tableLoading,
|
|
444
|
+
config.apiEndpoint,
|
|
445
|
+
pagination.pageSize,
|
|
446
|
+
getQueryParams,
|
|
447
|
+
prefetchNextPage,
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
// Fetch data when query params change (except initial load)
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
// Skip if not initialized yet or still initial loading
|
|
453
|
+
if (!hasInitializedRef.current || loading) return;
|
|
454
|
+
|
|
455
|
+
// Only fetch if query params actually changed
|
|
456
|
+
if (queryParamsString !== previousQueryParamsRef.current) {
|
|
457
|
+
previousQueryParamsRef.current = queryParamsString;
|
|
458
|
+
fetchData(false);
|
|
459
|
+
}
|
|
460
|
+
}, [queryParamsString, loading, fetchData]);
|
|
461
|
+
|
|
462
|
+
const handleCreate = () => {
|
|
463
|
+
if (createMode === "navigate") {
|
|
464
|
+
let path =
|
|
465
|
+
config.formMode?.createPath || `/${entityName || config.name}/new`;
|
|
466
|
+
// Add lang prefix if not already present
|
|
467
|
+
if (!path.startsWith(`/${lang}/`)) {
|
|
468
|
+
path = `/${lang}${path}`;
|
|
469
|
+
}
|
|
470
|
+
router.push(path);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
setEditingRowId(null);
|
|
474
|
+
setDialogOpen(true);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const handleEdit = (rowId: string, rowData?: Record<string, unknown>) => {
|
|
478
|
+
if (editMode === "navigate") {
|
|
479
|
+
const editPath =
|
|
480
|
+
config.formMode?.editPath ||
|
|
481
|
+
`/${entityName || config.name}/${rowId}/edit`;
|
|
482
|
+
// For recipes, use dishId from rowData if available, otherwise use rowId
|
|
483
|
+
let idToUse = rowId;
|
|
484
|
+
if (editPath.includes("[dishId]") && rowData?.dishId) {
|
|
485
|
+
idToUse = String(rowData.dishId);
|
|
486
|
+
}
|
|
487
|
+
// Replace both [id] and [dishId] placeholders
|
|
488
|
+
let path = editPath.replace("[id]", rowId).replace("[dishId]", idToUse);
|
|
489
|
+
// Add lang prefix if not already present
|
|
490
|
+
if (!path.startsWith(`/${lang}/`)) {
|
|
491
|
+
path = `/${lang}${path}`;
|
|
492
|
+
}
|
|
493
|
+
router.push(path);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
setEditingRowId(rowId);
|
|
497
|
+
setDialogOpen(true);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const handleDelete = (rowId: string) => {
|
|
501
|
+
setDeletingRowId(rowId);
|
|
502
|
+
setBulkDeleteIds([]);
|
|
503
|
+
setDeleteDialogOpen(true);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const handleCustomAction = async (
|
|
507
|
+
action: string,
|
|
508
|
+
rowId: string,
|
|
509
|
+
rowData: Record<string, unknown>,
|
|
510
|
+
) => {
|
|
511
|
+
const rowAction = config.rowActions?.find((a) => a.action === action);
|
|
512
|
+
if (!rowAction) return;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
switch (action) {
|
|
516
|
+
case "copy":
|
|
517
|
+
case "duplicate": {
|
|
518
|
+
// Get data to copy
|
|
519
|
+
let copyData = { ...rowData };
|
|
520
|
+
|
|
521
|
+
// Exclude fields specified in config
|
|
522
|
+
if (rowAction.excludeFields) {
|
|
523
|
+
rowAction.excludeFields.forEach((field) => {
|
|
524
|
+
delete copyData[field];
|
|
525
|
+
});
|
|
526
|
+
} else {
|
|
527
|
+
// Default: exclude id and timestamps
|
|
528
|
+
delete copyData[config.idField];
|
|
529
|
+
delete copyData["id"];
|
|
530
|
+
delete copyData["createdAt"];
|
|
531
|
+
delete copyData["createdBy"];
|
|
532
|
+
delete copyData["updatedAt"];
|
|
533
|
+
delete copyData["updatedBy"];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Transform data if specified
|
|
537
|
+
if (rowAction.transformData) {
|
|
538
|
+
copyData = rowAction.transformData(copyData);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Create new record with copied data
|
|
542
|
+
await crudService.create(config.apiEndpoint, copyData);
|
|
543
|
+
await fetchData(false);
|
|
544
|
+
toast.success(t("success", "Success"), {
|
|
545
|
+
description: tMessage("created", { entity: config.label }),
|
|
546
|
+
});
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case "view": {
|
|
551
|
+
// Navigate to view page or open view dialog
|
|
552
|
+
const editPath =
|
|
553
|
+
config.formMode?.editPath ||
|
|
554
|
+
`/${entityName || config.name}/${rowId}`;
|
|
555
|
+
let viewPath = editPath
|
|
556
|
+
.replace("[id]", rowId)
|
|
557
|
+
.replace("[dishId]", rowId);
|
|
558
|
+
// Add lang prefix if not already present
|
|
559
|
+
if (!viewPath.startsWith(`/${lang}/`)) {
|
|
560
|
+
viewPath = `/${lang}${viewPath}`;
|
|
561
|
+
}
|
|
562
|
+
router.push(viewPath);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case "archive": {
|
|
567
|
+
// Archive the record (update status to archived)
|
|
568
|
+
await crudService.update(config.apiEndpoint, rowId, {
|
|
569
|
+
...rowData,
|
|
570
|
+
status: "archived",
|
|
571
|
+
});
|
|
572
|
+
await fetchData(false);
|
|
573
|
+
toast.success(t("success", "Success"), {
|
|
574
|
+
description: tMessage("updated", { entity: config.label }),
|
|
575
|
+
});
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
default: {
|
|
580
|
+
// Custom handler: Get original config to access handler function
|
|
581
|
+
// NOTE: This logic is deprecated because Core package cannot import App configs directly.
|
|
582
|
+
// Custom handlers passed via config functions are lost during serialization.
|
|
583
|
+
// If you need custom actions with handlers, please implement them via `customActions` prop
|
|
584
|
+
// or a registered event handler mechanism.
|
|
585
|
+
|
|
586
|
+
console.warn(
|
|
587
|
+
`Action "${action}" has no built-in handler and custom handlers via config are not supported in this architecture. Please use customActions prop.`,
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
/* Deprecated logic removed to fix build:
|
|
591
|
+
if (entityName) {
|
|
592
|
+
try {
|
|
593
|
+
const originalConfig = getEntityConfig(entityName)
|
|
594
|
+
...
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
*/
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.error(`Error executing ${action} action:`, error);
|
|
602
|
+
globalError.emit({
|
|
603
|
+
title: t("error", "Error"),
|
|
604
|
+
description: tMessage("saveFailed", { entity: rowAction.label }),
|
|
605
|
+
trace: error instanceof Error ? error.stack : undefined,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const confirmDelete = async () => {
|
|
611
|
+
const rowId = deletingRowId;
|
|
612
|
+
const ids = bulkDeleteIds.length > 0 ? bulkDeleteIds : rowId ? [rowId] : [];
|
|
613
|
+
|
|
614
|
+
if (ids.length === 0) return;
|
|
615
|
+
|
|
616
|
+
setDeleteLoading(true);
|
|
617
|
+
try {
|
|
618
|
+
await Promise.all(
|
|
619
|
+
ids.map((id) => crudService.delete(config.apiEndpoint, id)),
|
|
620
|
+
);
|
|
621
|
+
await fetchData(false); // Only reload table data, not entire component
|
|
622
|
+
toast.success(t("success", "Success"), {
|
|
623
|
+
description:
|
|
624
|
+
ids.length === 1
|
|
625
|
+
? tMessage("deleted", { entity: config.label })
|
|
626
|
+
: tMessage("bulkDeleted", {
|
|
627
|
+
count: ids.length,
|
|
628
|
+
entities: config.pluralLabel,
|
|
629
|
+
}),
|
|
630
|
+
});
|
|
631
|
+
setDeleteDialogOpen(false);
|
|
632
|
+
setDeletingRowId(null);
|
|
633
|
+
setBulkDeleteIds([]);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.error("Error deleting:", error);
|
|
636
|
+
|
|
637
|
+
let description = tMessage("deleteFailed", {
|
|
638
|
+
entity: config.pluralLabel,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
if (error instanceof Error) {
|
|
642
|
+
description = error.message;
|
|
643
|
+
|
|
644
|
+
// Enhance FK errors for better user experience
|
|
645
|
+
if (
|
|
646
|
+
description.includes("violates foreign key constraint") ||
|
|
647
|
+
description.includes("violates RESTRICT setting")
|
|
648
|
+
) {
|
|
649
|
+
const match = description.match(/on table "([^"]+)"/);
|
|
650
|
+
if (match) {
|
|
651
|
+
const tableName = match[1];
|
|
652
|
+
// Format table name: role_permissions -> Role Permissions
|
|
653
|
+
const readableTable = tableName
|
|
654
|
+
.split("_")
|
|
655
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
656
|
+
.join(" ");
|
|
657
|
+
description = `Cannot delete because it is used in "${readableTable}".`;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
globalError.emit({
|
|
663
|
+
title: t("error", "Error"),
|
|
664
|
+
description: description,
|
|
665
|
+
trace: error instanceof Error ? error.stack : undefined,
|
|
666
|
+
});
|
|
667
|
+
} finally {
|
|
668
|
+
setDeleteLoading(false);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const handleBulkDelete = (rowIds: string[]) => {
|
|
673
|
+
setDeletingRowId(null);
|
|
674
|
+
setBulkDeleteIds(rowIds);
|
|
675
|
+
setDeleteDialogOpen(true);
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const handleSubmit = async (formData: Record<string, unknown>) => {
|
|
679
|
+
try {
|
|
680
|
+
if (editingRowId) {
|
|
681
|
+
await crudService.update(config.apiEndpoint, editingRowId, formData);
|
|
682
|
+
toast.success(t("success", "Success"), {
|
|
683
|
+
description: tMessage("updated", { entity: config.label }),
|
|
684
|
+
});
|
|
685
|
+
} else {
|
|
686
|
+
await crudService.create(config.apiEndpoint, formData);
|
|
687
|
+
toast.success(t("success", "Success"), {
|
|
688
|
+
description: tMessage("created", { entity: config.label }),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
await fetchData(false); // Only reload table data, not entire component
|
|
692
|
+
} catch (error) {
|
|
693
|
+
console.error("Error saving:", error);
|
|
694
|
+
// Extract error message from error object
|
|
695
|
+
const errorMessage =
|
|
696
|
+
error instanceof Error
|
|
697
|
+
? error.message
|
|
698
|
+
: tMessage("saveFailed", { entity: config.label });
|
|
699
|
+
|
|
700
|
+
// Use detailed error message if available, otherwise fallback to generic message
|
|
701
|
+
const displayMessage =
|
|
702
|
+
errorMessage && errorMessage !== "Failed to POST: Bad Request"
|
|
703
|
+
? errorMessage
|
|
704
|
+
: tMessage("saveFailed", { entity: config.label });
|
|
705
|
+
|
|
706
|
+
globalError.emit({
|
|
707
|
+
title: t("error", "Error"),
|
|
708
|
+
description: displayMessage,
|
|
709
|
+
trace: error instanceof Error ? error.stack : undefined,
|
|
710
|
+
});
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// ✅ Memoize editingData to avoid recalculation
|
|
716
|
+
const editingData = useMemo(() => {
|
|
717
|
+
if (!editingRowId) return undefined;
|
|
718
|
+
return data.data.find(
|
|
719
|
+
(row) =>
|
|
720
|
+
String((row as Record<string, unknown>)[config.idField]) ===
|
|
721
|
+
editingRowId,
|
|
722
|
+
);
|
|
723
|
+
}, [editingRowId, data.data, config.idField]);
|
|
724
|
+
|
|
725
|
+
// ✅ Memoize deletingData to avoid recalculation
|
|
726
|
+
const deletingData = useMemo(() => {
|
|
727
|
+
if (!deletingRowId) return undefined;
|
|
728
|
+
return data.data.find(
|
|
729
|
+
(row) =>
|
|
730
|
+
String((row as Record<string, unknown>)[config.idField]) ===
|
|
731
|
+
deletingRowId,
|
|
732
|
+
);
|
|
733
|
+
}, [deletingRowId, data.data, config.idField]);
|
|
734
|
+
|
|
735
|
+
return (
|
|
736
|
+
<>
|
|
737
|
+
<div className="space-y-3">
|
|
738
|
+
<div className="pb-2">
|
|
739
|
+
{/* Header: Title + Description + Actions */}
|
|
740
|
+
<div className="flex items-start justify-between gap-4 flex-wrap">
|
|
741
|
+
<div className="space-y-1">
|
|
742
|
+
<div className="flex items-center gap-2 sm:gap-3 flex-wrap">
|
|
743
|
+
{config.iconName && (
|
|
744
|
+
<div className="p-1.5 sm:p-2 rounded-lg bg-primary/10 shrink-0">
|
|
745
|
+
<DynamicIcon
|
|
746
|
+
name={config.iconName as any}
|
|
747
|
+
className="h-4 w-4 sm:h-5 sm:w-5 text-primary"
|
|
748
|
+
/>
|
|
749
|
+
</div>
|
|
750
|
+
)}
|
|
751
|
+
<div className="min-w-0 flex-1">
|
|
752
|
+
<h2 className="text-lg sm:text-xl font-bold tracking-tight">
|
|
753
|
+
{config.pluralLabel}
|
|
754
|
+
</h2>
|
|
755
|
+
{config.description && (
|
|
756
|
+
<p className="text-xs sm:text-sm text-muted-foreground mt-0.5 sm:mt-1 hidden sm:block">
|
|
757
|
+
{config.description}
|
|
758
|
+
</p>
|
|
759
|
+
)}
|
|
760
|
+
</div>
|
|
761
|
+
{data.total !== undefined && (
|
|
762
|
+
<Badge
|
|
763
|
+
variant="secondary"
|
|
764
|
+
className="text-xs shrink-0 hidden sm:inline-flex"
|
|
765
|
+
>
|
|
766
|
+
{data.total}{" "}
|
|
767
|
+
{data.total === 1 ? t("item", "item") : t("items", "items")}
|
|
768
|
+
</Badge>
|
|
769
|
+
)}
|
|
770
|
+
</div>
|
|
771
|
+
{/* Mobile: Show total count below title */}
|
|
772
|
+
{data.total !== undefined && (
|
|
773
|
+
<Badge variant="secondary" className="text-xs w-fit sm:hidden">
|
|
774
|
+
{data.total}{" "}
|
|
775
|
+
{data.total === 1 ? t("item", "item") : t("items", "items")}
|
|
776
|
+
</Badge>
|
|
777
|
+
)}
|
|
778
|
+
</div>
|
|
779
|
+
|
|
780
|
+
{/* Action Buttons Grouped */}
|
|
781
|
+
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
|
782
|
+
{/* Primary Actions */}
|
|
783
|
+
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
|
784
|
+
<CrudBulkActions
|
|
785
|
+
config={config}
|
|
786
|
+
permissions={permissions}
|
|
787
|
+
onDelete={handleBulkDelete}
|
|
788
|
+
/>
|
|
789
|
+
{customActions}
|
|
790
|
+
{permissions.create && (
|
|
791
|
+
<Button onClick={handleCreate} size="sm" className="shrink-0">
|
|
792
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
793
|
+
<span className="hidden sm:inline">
|
|
794
|
+
{t("create", "Create")} {config.label}
|
|
795
|
+
</span>
|
|
796
|
+
<span className="sm:hidden">{t("create", "Create")}</span>
|
|
797
|
+
</Button>
|
|
798
|
+
)}
|
|
799
|
+
</div>
|
|
800
|
+
|
|
801
|
+
{/* Secondary Actions - Conditional loading with Suspense */}
|
|
802
|
+
{(permissions.import && config.features?.import) ||
|
|
803
|
+
(permissions.export && config.features?.export) ? (
|
|
804
|
+
<>
|
|
805
|
+
<Separator
|
|
806
|
+
orientation="vertical"
|
|
807
|
+
className="h-6 hidden sm:block"
|
|
808
|
+
/>
|
|
809
|
+
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
|
810
|
+
{permissions.import && config.features?.import && (
|
|
811
|
+
<Suspense fallback={null}>
|
|
812
|
+
<CrudImportDialog
|
|
813
|
+
config={config}
|
|
814
|
+
endpoint={(() => {
|
|
815
|
+
// Handle endpoint with query params correctly
|
|
816
|
+
const baseUrl = config.apiEndpoint.split("?")[0];
|
|
817
|
+
const queryParams = config.apiEndpoint.includes("?")
|
|
818
|
+
? config.apiEndpoint.split("?")[1]
|
|
819
|
+
: "";
|
|
820
|
+
return `${baseUrl}/import${queryParams ? `?${queryParams}` : ""}`;
|
|
821
|
+
})()}
|
|
822
|
+
canImport={permissions.import}
|
|
823
|
+
/>
|
|
824
|
+
</Suspense>
|
|
825
|
+
)}
|
|
826
|
+
{permissions.export && config.features?.export && (
|
|
827
|
+
<Suspense fallback={null}>
|
|
828
|
+
<CrudExportButton
|
|
829
|
+
endpoint={`/api/crud/${entityName || config.name}/export`}
|
|
830
|
+
filters={filters}
|
|
831
|
+
search={search}
|
|
832
|
+
canExport={permissions.export}
|
|
833
|
+
/>
|
|
834
|
+
</Suspense>
|
|
835
|
+
)}
|
|
836
|
+
</div>
|
|
837
|
+
</>
|
|
838
|
+
) : null}
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
<div className="space-y-3">
|
|
844
|
+
{/* Toolbar Section - Sticky when scrolling */}
|
|
845
|
+
<div className="sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 pt-0 border-b border-border/50 transition-all duration-200">
|
|
846
|
+
<CrudTableToolbar
|
|
847
|
+
table={tableInstance}
|
|
848
|
+
config={config}
|
|
849
|
+
permissions={permissions}
|
|
850
|
+
viewMode={viewMode}
|
|
851
|
+
onViewModeChange={handleViewModeChange}
|
|
852
|
+
/>
|
|
853
|
+
</div>
|
|
854
|
+
|
|
855
|
+
{/* Table/Card Section */}
|
|
856
|
+
<div className="overflow-hidden">
|
|
857
|
+
{viewMode === "table" ? (
|
|
858
|
+
<CrudTable
|
|
859
|
+
data={data as CrudResponse<Record<string, unknown>>}
|
|
860
|
+
loading={loading || tableLoading}
|
|
861
|
+
onEdit={handleEdit}
|
|
862
|
+
onDelete={handleDelete}
|
|
863
|
+
onCustomAction={handleCustomAction}
|
|
864
|
+
onTableReady={setTableInstance}
|
|
865
|
+
onEmptyStateAction={{
|
|
866
|
+
onCreate: handleCreate,
|
|
867
|
+
onClearSearch: () => setSearch(""),
|
|
868
|
+
onClearFilters: clearFilters,
|
|
869
|
+
}}
|
|
870
|
+
getTranslation={(key: string) => t(key, key)}
|
|
871
|
+
/>
|
|
872
|
+
) : (
|
|
873
|
+
<div className="p-4">
|
|
874
|
+
<CrudCardView
|
|
875
|
+
config={config}
|
|
876
|
+
data={data as CrudResponse<Record<string, unknown>>}
|
|
877
|
+
permissions={permissions}
|
|
878
|
+
loading={loading || tableLoading}
|
|
879
|
+
onEdit={handleEdit}
|
|
880
|
+
onDelete={handleDelete}
|
|
881
|
+
onCustomAction={handleCustomAction}
|
|
882
|
+
onEmptyStateAction={{
|
|
883
|
+
onCreate: handleCreate,
|
|
884
|
+
onClearSearch: () => setSearch(""),
|
|
885
|
+
onClearFilters: clearFilters,
|
|
886
|
+
}}
|
|
887
|
+
/>
|
|
888
|
+
</div>
|
|
889
|
+
)}
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
893
|
+
|
|
894
|
+
{permissions.create || permissions.update ? (
|
|
895
|
+
<>
|
|
896
|
+
{/* Dialog Mode */}
|
|
897
|
+
{((!editingRowId && createMode === "dialog") ||
|
|
898
|
+
(editingRowId && editMode === "dialog")) && (
|
|
899
|
+
<CrudDialog
|
|
900
|
+
open={dialogOpen}
|
|
901
|
+
onOpenChange={(open) => {
|
|
902
|
+
setDialogOpen(open);
|
|
903
|
+
if (!open) setEditingRowId(null);
|
|
904
|
+
}}
|
|
905
|
+
config={config}
|
|
906
|
+
initialData={editingData as Record<string, unknown> | undefined}
|
|
907
|
+
mode={editingRowId ? "edit" : "create"}
|
|
908
|
+
onSubmit={handleSubmit}
|
|
909
|
+
/>
|
|
910
|
+
)}
|
|
911
|
+
|
|
912
|
+
{/* Sheet Mode */}
|
|
913
|
+
{((!editingRowId && createMode === "sheet") ||
|
|
914
|
+
(editingRowId && editMode === "sheet")) && (
|
|
915
|
+
<CrudSheet
|
|
916
|
+
open={dialogOpen}
|
|
917
|
+
onOpenChange={(open) => {
|
|
918
|
+
setDialogOpen(open);
|
|
919
|
+
if (!open) setEditingRowId(null);
|
|
920
|
+
}}
|
|
921
|
+
config={config}
|
|
922
|
+
initialData={editingData as Record<string, unknown> | undefined}
|
|
923
|
+
mode={editingRowId ? "edit" : "create"}
|
|
924
|
+
onSubmit={handleSubmit}
|
|
925
|
+
side={sheetSide}
|
|
926
|
+
/>
|
|
927
|
+
)}
|
|
928
|
+
</>
|
|
929
|
+
) : null}
|
|
930
|
+
|
|
931
|
+
{permissions.delete && (
|
|
932
|
+
<CrudDeleteDialog
|
|
933
|
+
open={deleteDialogOpen}
|
|
934
|
+
onOpenChange={setDeleteDialogOpen}
|
|
935
|
+
config={config}
|
|
936
|
+
itemData={deletingData as Record<string, unknown> | undefined}
|
|
937
|
+
itemCount={
|
|
938
|
+
bulkDeleteIds.length > 0 ? bulkDeleteIds.length : undefined
|
|
939
|
+
}
|
|
940
|
+
onConfirm={confirmDelete}
|
|
941
|
+
loading={deleteLoading}
|
|
942
|
+
translations={{
|
|
943
|
+
cancel: translations.cancel,
|
|
944
|
+
delete: translations.delete,
|
|
945
|
+
deleting: translations.deleting,
|
|
946
|
+
confirmDeleteTitle: translations.confirmDeleteTitle,
|
|
947
|
+
confirmBulkDeleteTitle: translations.confirmBulkDeleteTitle,
|
|
948
|
+
confirmDeleteDescription: translations.confirmDeleteDescription,
|
|
949
|
+
confirmBulkDeleteDescription:
|
|
950
|
+
translations.confirmBulkDeleteDescription,
|
|
951
|
+
}}
|
|
952
|
+
/>
|
|
953
|
+
)}
|
|
954
|
+
</>
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
interface CrudPageProps {
|
|
959
|
+
config: EntityConfig;
|
|
960
|
+
permissions: CrudPermissions;
|
|
961
|
+
entityName?: string;
|
|
962
|
+
dictionary: DictionaryType;
|
|
963
|
+
customActions?: React.ReactNode;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function CrudPage({
|
|
967
|
+
config,
|
|
968
|
+
permissions,
|
|
969
|
+
entityName,
|
|
970
|
+
dictionary,
|
|
971
|
+
customActions,
|
|
972
|
+
}: CrudPageProps) {
|
|
973
|
+
// Extract translations from dictionary for CrudProvider
|
|
974
|
+
const crudDict = (dictionary as Record<string, unknown>).crud as
|
|
975
|
+
| Record<string, unknown>
|
|
976
|
+
| undefined;
|
|
977
|
+
const commonDict = (crudDict?.common as Record<string, string>) || {};
|
|
978
|
+
|
|
979
|
+
const initialTranslations = {
|
|
980
|
+
edit: commonDict.edit,
|
|
981
|
+
delete: commonDict.delete,
|
|
982
|
+
save: commonDict.save,
|
|
983
|
+
cancel: commonDict.cancel,
|
|
984
|
+
create: commonDict.create,
|
|
985
|
+
update: commonDict.update,
|
|
986
|
+
creating: commonDict.creating,
|
|
987
|
+
updating: commonDict.updating,
|
|
988
|
+
deleting: commonDict.deleting,
|
|
989
|
+
savedSuccessfully: commonDict.savedSuccessfully,
|
|
990
|
+
createdSuccessfully: commonDict.createdSuccessfully,
|
|
991
|
+
updatedSuccessfully: commonDict.updatedSuccessfully,
|
|
992
|
+
confirmDeleteTitle: commonDict.confirmDeleteTitle,
|
|
993
|
+
confirmBulkDeleteTitle: commonDict.confirmBulkDeleteTitle,
|
|
994
|
+
confirmDeleteDescription: commonDict.confirmDeleteDescription,
|
|
995
|
+
confirmBulkDeleteDescription: commonDict.confirmBulkDeleteDescription,
|
|
996
|
+
deleteSelected: commonDict.deleteSelected,
|
|
997
|
+
actions: commonDict.actions,
|
|
998
|
+
add: commonDict.add,
|
|
999
|
+
addNew: commonDict.addNew,
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
return (
|
|
1003
|
+
<CrudProvider
|
|
1004
|
+
initialConfig={config}
|
|
1005
|
+
initialPermissions={permissions}
|
|
1006
|
+
initialTranslations={initialTranslations}
|
|
1007
|
+
>
|
|
1008
|
+
<CrudPageContent
|
|
1009
|
+
config={config}
|
|
1010
|
+
permissions={permissions}
|
|
1011
|
+
entityName={entityName}
|
|
1012
|
+
dictionary={dictionary}
|
|
1013
|
+
customActions={customActions}
|
|
1014
|
+
/>
|
|
1015
|
+
</CrudProvider>
|
|
1016
|
+
);
|
|
1017
|
+
}
|