@goplusvn/core 0.1.0 → 0.1.1
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 +2 -1
- 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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import type {
|
|
5
|
+
ActiveFilter,
|
|
6
|
+
CrudPermissions,
|
|
7
|
+
CrudQueryParams,
|
|
8
|
+
EntityConfig,
|
|
9
|
+
SortingState,
|
|
10
|
+
} from "../../types";
|
|
11
|
+
|
|
12
|
+
// Translations interface for CRUD components
|
|
13
|
+
export interface CrudTranslations {
|
|
14
|
+
edit?: string;
|
|
15
|
+
delete?: string;
|
|
16
|
+
save?: string;
|
|
17
|
+
cancel?: string;
|
|
18
|
+
create?: string;
|
|
19
|
+
update?: string;
|
|
20
|
+
creating?: string;
|
|
21
|
+
updating?: string;
|
|
22
|
+
deleting?: string;
|
|
23
|
+
savedSuccessfully?: string;
|
|
24
|
+
createdSuccessfully?: string;
|
|
25
|
+
updatedSuccessfully?: string;
|
|
26
|
+
confirmDeleteTitle?: string;
|
|
27
|
+
confirmBulkDeleteTitle?: string;
|
|
28
|
+
confirmDeleteDescription?: string;
|
|
29
|
+
confirmBulkDeleteDescription?: string;
|
|
30
|
+
deleteSelected?: string;
|
|
31
|
+
actions?: string;
|
|
32
|
+
add?: string;
|
|
33
|
+
addNew?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 1. Config Context (Static - rarely changes)
|
|
37
|
+
export interface CrudConfigContextValue {
|
|
38
|
+
config: EntityConfig | null;
|
|
39
|
+
setConfig: (config: EntityConfig) => void;
|
|
40
|
+
permissions: CrudPermissions;
|
|
41
|
+
setPermissions: (permissions: CrudPermissions) => void;
|
|
42
|
+
translations: CrudTranslations;
|
|
43
|
+
setTranslations: (translations: CrudTranslations) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const CrudConfigContext = createContext<
|
|
47
|
+
CrudConfigContextValue | undefined
|
|
48
|
+
>(undefined);
|
|
49
|
+
|
|
50
|
+
export function useCrudConfig() {
|
|
51
|
+
const context = useContext(CrudConfigContext);
|
|
52
|
+
if (context === undefined) {
|
|
53
|
+
throw new Error("useCrudConfig must be used within CrudConfigProvider");
|
|
54
|
+
}
|
|
55
|
+
return context;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. State Context (Dynamic - changes often)
|
|
59
|
+
export interface CrudStateContextValue {
|
|
60
|
+
// Search
|
|
61
|
+
search: string;
|
|
62
|
+
setSearch: (search: string) => void;
|
|
63
|
+
|
|
64
|
+
// Filters
|
|
65
|
+
filters: ActiveFilter[];
|
|
66
|
+
setFilters: (filters: ActiveFilter[]) => void;
|
|
67
|
+
addFilter: (filter: ActiveFilter) => void;
|
|
68
|
+
removeFilter: (filterName: string) => void;
|
|
69
|
+
updateFilter: (filterName: string, value: unknown) => void;
|
|
70
|
+
clearFilters: () => void;
|
|
71
|
+
|
|
72
|
+
// Pagination
|
|
73
|
+
pagination: {
|
|
74
|
+
page: number;
|
|
75
|
+
pageSize: number;
|
|
76
|
+
};
|
|
77
|
+
setPagination: (pagination: { page: number; pageSize: number }) => void;
|
|
78
|
+
setPage: (page: number) => void;
|
|
79
|
+
setPageSize: (pageSize: number) => void;
|
|
80
|
+
|
|
81
|
+
// Sorting
|
|
82
|
+
sorting: SortingState | null;
|
|
83
|
+
setSorting: (sorting: SortingState | null) => void;
|
|
84
|
+
|
|
85
|
+
// Query params builder
|
|
86
|
+
getQueryParams: () => CrudQueryParams;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const CrudStateContext = createContext<
|
|
90
|
+
CrudStateContextValue | undefined
|
|
91
|
+
>(undefined);
|
|
92
|
+
|
|
93
|
+
export function useCrudState() {
|
|
94
|
+
const context = useContext(CrudStateContext);
|
|
95
|
+
if (context === undefined) {
|
|
96
|
+
throw new Error("useCrudState must be used within CrudStateProvider");
|
|
97
|
+
}
|
|
98
|
+
return context;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. Selection Context (Very Dynamic - changes frequently)
|
|
102
|
+
export interface CrudSelectionContextValue {
|
|
103
|
+
selectedRows: Set<string>;
|
|
104
|
+
setSelectedRows: (selectedRows: Set<string>) => void;
|
|
105
|
+
toggleRowSelection: (rowId: string) => void;
|
|
106
|
+
selectAllRows: (rowIds: string[]) => void;
|
|
107
|
+
clearSelection: () => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const CrudSelectionContext = createContext<
|
|
111
|
+
CrudSelectionContextValue | undefined
|
|
112
|
+
>(undefined);
|
|
113
|
+
|
|
114
|
+
export function useCrudSelection() {
|
|
115
|
+
const context = useContext(CrudSelectionContext);
|
|
116
|
+
if (context === undefined) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"useCrudSelection must be used within CrudSelectionProvider",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return context;
|
|
122
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import type { EntityConfig } from "../../types";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
AlertDialog,
|
|
9
|
+
AlertDialogAction,
|
|
10
|
+
AlertDialogCancel,
|
|
11
|
+
AlertDialogContent,
|
|
12
|
+
AlertDialogDescription,
|
|
13
|
+
AlertDialogFooter,
|
|
14
|
+
AlertDialogHeader,
|
|
15
|
+
AlertDialogTitle,
|
|
16
|
+
} from "../../ui";
|
|
17
|
+
|
|
18
|
+
interface CrudDeleteDialogProps {
|
|
19
|
+
open: boolean;
|
|
20
|
+
onOpenChange: (open: boolean) => void;
|
|
21
|
+
config: EntityConfig;
|
|
22
|
+
itemData?: Record<string, unknown>;
|
|
23
|
+
itemCount?: number;
|
|
24
|
+
onConfirm: () => void;
|
|
25
|
+
loading?: boolean;
|
|
26
|
+
translations?: {
|
|
27
|
+
confirmDeleteTitle?: string;
|
|
28
|
+
confirmBulkDeleteTitle?: string;
|
|
29
|
+
confirmDeleteDescription?: string;
|
|
30
|
+
confirmBulkDeleteDescription?: string;
|
|
31
|
+
cancel?: string;
|
|
32
|
+
delete?: string;
|
|
33
|
+
deleting?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function CrudDeleteDialog({
|
|
38
|
+
open,
|
|
39
|
+
onOpenChange,
|
|
40
|
+
config,
|
|
41
|
+
itemData,
|
|
42
|
+
itemCount,
|
|
43
|
+
onConfirm,
|
|
44
|
+
loading = false,
|
|
45
|
+
translations,
|
|
46
|
+
}: CrudDeleteDialogProps) {
|
|
47
|
+
const isBulkDelete = itemCount !== undefined && itemCount > 1;
|
|
48
|
+
const displayValue = itemData
|
|
49
|
+
? String(
|
|
50
|
+
itemData[config.displayField] ||
|
|
51
|
+
itemData[config.idField] ||
|
|
52
|
+
"this item",
|
|
53
|
+
)
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
// Translation helpers with fallbacks
|
|
57
|
+
const t = {
|
|
58
|
+
cancel: translations?.cancel || "Cancel",
|
|
59
|
+
delete: translations?.delete || "Delete",
|
|
60
|
+
deleting: translations?.deleting || "Deleting...",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Build title
|
|
64
|
+
const getTitle = () => {
|
|
65
|
+
if (isBulkDelete) {
|
|
66
|
+
if (translations?.confirmBulkDeleteTitle) {
|
|
67
|
+
return translations.confirmBulkDeleteTitle
|
|
68
|
+
.replace("{{count}}", String(itemCount))
|
|
69
|
+
.replace("{{entities}}", config.pluralLabel.toLowerCase());
|
|
70
|
+
}
|
|
71
|
+
return `Delete ${itemCount} ${config.pluralLabel.toLowerCase()}?`;
|
|
72
|
+
} else {
|
|
73
|
+
if (translations?.confirmDeleteTitle) {
|
|
74
|
+
return translations.confirmDeleteTitle.replace(
|
|
75
|
+
"{{entity}}",
|
|
76
|
+
config.label,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return `Delete ${config.label}?`;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Build description
|
|
84
|
+
const getDescription = () => {
|
|
85
|
+
if (isBulkDelete) {
|
|
86
|
+
if (translations?.confirmBulkDeleteDescription) {
|
|
87
|
+
return translations.confirmBulkDeleteDescription
|
|
88
|
+
.replace("{{count}}", String(itemCount))
|
|
89
|
+
.replace("{{entities}}", config.pluralLabel.toLowerCase());
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
Are you sure you want to delete <strong>{itemCount}</strong>{" "}
|
|
94
|
+
{config.pluralLabel.toLowerCase()}? This action cannot be undone.
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
if (translations?.confirmDeleteDescription && displayValue) {
|
|
99
|
+
return translations.confirmDeleteDescription.replace(
|
|
100
|
+
"{{name}}",
|
|
101
|
+
displayValue,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
Are you sure you want to delete{" "}
|
|
107
|
+
{displayValue ? (
|
|
108
|
+
<strong>"{displayValue}"</strong>
|
|
109
|
+
) : (
|
|
110
|
+
`this ${config.label.toLowerCase()}`
|
|
111
|
+
)}
|
|
112
|
+
? This action cannot be undone.
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
120
|
+
<AlertDialogContent>
|
|
121
|
+
<AlertDialogHeader>
|
|
122
|
+
<div className="flex items-center gap-3">
|
|
123
|
+
<div className="rounded-full bg-destructive/10 p-2">
|
|
124
|
+
<AlertTriangle className="h-5 w-5 text-destructive" />
|
|
125
|
+
</div>
|
|
126
|
+
<AlertDialogTitle>{getTitle()}</AlertDialogTitle>
|
|
127
|
+
</div>
|
|
128
|
+
<AlertDialogDescription className="pt-2">
|
|
129
|
+
{getDescription()}
|
|
130
|
+
</AlertDialogDescription>
|
|
131
|
+
</AlertDialogHeader>
|
|
132
|
+
<AlertDialogFooter>
|
|
133
|
+
<AlertDialogCancel disabled={loading}>{t.cancel}</AlertDialogCancel>
|
|
134
|
+
<AlertDialogAction
|
|
135
|
+
onClick={onConfirm}
|
|
136
|
+
disabled={loading}
|
|
137
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
138
|
+
>
|
|
139
|
+
{loading ? t.deleting : t.delete}
|
|
140
|
+
</AlertDialogAction>
|
|
141
|
+
</AlertDialogFooter>
|
|
142
|
+
</AlertDialogContent>
|
|
143
|
+
</AlertDialog>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { CheckCircle2, Loader2 } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import type { EntityConfig } from "../../types";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from "../../ui";
|
|
15
|
+
import { DynamicIcon } from "../../ui";
|
|
16
|
+
import { CrudForm } from "./crud-form";
|
|
17
|
+
import { useCrudConfig } from "./crud-context";
|
|
18
|
+
|
|
19
|
+
interface CrudDialogProps {
|
|
20
|
+
open: boolean;
|
|
21
|
+
onOpenChange: (open: boolean) => void;
|
|
22
|
+
config: EntityConfig;
|
|
23
|
+
initialData?: Record<string, unknown>;
|
|
24
|
+
mode?: "create" | "edit";
|
|
25
|
+
onSubmit: (data: Record<string, unknown>) => Promise<void> | void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function CrudDialog({
|
|
29
|
+
open,
|
|
30
|
+
onOpenChange,
|
|
31
|
+
config,
|
|
32
|
+
initialData,
|
|
33
|
+
mode = "create",
|
|
34
|
+
onSubmit,
|
|
35
|
+
}: CrudDialogProps) {
|
|
36
|
+
const { translations } = useCrudConfig();
|
|
37
|
+
const [showSuccess, setShowSuccess] = useState(false);
|
|
38
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
39
|
+
const [formProgress, setFormProgress] = useState<number | null>(null);
|
|
40
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
41
|
+
const progressSubscriptionRef = useRef<(() => void) | null>(null);
|
|
42
|
+
const successTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
43
|
+
const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
44
|
+
|
|
45
|
+
// Track open state to handle race conditions
|
|
46
|
+
const isOpenRef = useRef(open);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
isOpenRef.current = open;
|
|
49
|
+
}, [open]);
|
|
50
|
+
|
|
51
|
+
const handleSubmit = async (data: Record<string, unknown>) => {
|
|
52
|
+
try {
|
|
53
|
+
setIsSubmitting(true);
|
|
54
|
+
await onSubmit(data);
|
|
55
|
+
|
|
56
|
+
// Check if dialog was closed by parent (CrudPage) during submission
|
|
57
|
+
if (!isOpenRef.current) {
|
|
58
|
+
setIsSubmitting(false);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setIsSubmitting(false);
|
|
63
|
+
setShowSuccess(true);
|
|
64
|
+
|
|
65
|
+
// Clear any existing timeout
|
|
66
|
+
if (successTimeoutRef.current) {
|
|
67
|
+
clearTimeout(successTimeoutRef.current);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
successTimeoutRef.current = setTimeout(() => {
|
|
71
|
+
// Double check open state before closing
|
|
72
|
+
if (isOpenRef.current) {
|
|
73
|
+
setShowSuccess(false);
|
|
74
|
+
onOpenChange(false);
|
|
75
|
+
}
|
|
76
|
+
successTimeoutRef.current = null;
|
|
77
|
+
}, 1500);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (isOpenRef.current) {
|
|
80
|
+
setIsSubmitting(false);
|
|
81
|
+
}
|
|
82
|
+
// Error handling is done in CrudForm
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleCancel = () => {
|
|
87
|
+
if (!isSubmitting) {
|
|
88
|
+
onOpenChange(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Focus management - focus vào field đầu tiên khi mở dialog
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (open) {
|
|
95
|
+
// Clear any existing timeout
|
|
96
|
+
if (focusTimeoutRef.current) {
|
|
97
|
+
clearTimeout(focusTimeoutRef.current);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
focusTimeoutRef.current = setTimeout(() => {
|
|
101
|
+
const firstInput = document.querySelector<HTMLInputElement>(
|
|
102
|
+
'form input:not([type="hidden"]), form textarea, form select',
|
|
103
|
+
);
|
|
104
|
+
firstInput?.focus();
|
|
105
|
+
focusTimeoutRef.current = null;
|
|
106
|
+
}, 100);
|
|
107
|
+
} else {
|
|
108
|
+
// Reset states when dialog closes
|
|
109
|
+
setShowSuccess(false);
|
|
110
|
+
setIsSubmitting(false);
|
|
111
|
+
setFormProgress(null);
|
|
112
|
+
// Cleanup progress subscription
|
|
113
|
+
if (
|
|
114
|
+
progressSubscriptionRef.current &&
|
|
115
|
+
typeof progressSubscriptionRef.current === "function"
|
|
116
|
+
) {
|
|
117
|
+
try {
|
|
118
|
+
progressSubscriptionRef.current();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error("Error cleaning up progress subscription:", error);
|
|
121
|
+
}
|
|
122
|
+
progressSubscriptionRef.current = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Cleanup timeouts when component unmounts or dialog closes
|
|
127
|
+
return () => {
|
|
128
|
+
if (successTimeoutRef.current) {
|
|
129
|
+
clearTimeout(successTimeoutRef.current);
|
|
130
|
+
successTimeoutRef.current = null;
|
|
131
|
+
}
|
|
132
|
+
if (focusTimeoutRef.current) {
|
|
133
|
+
clearTimeout(focusTimeoutRef.current);
|
|
134
|
+
focusTimeoutRef.current = null;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}, [open]);
|
|
138
|
+
|
|
139
|
+
// Keyboard shortcuts
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!open) return;
|
|
142
|
+
|
|
143
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
144
|
+
// Don't handle keyboard events if user is typing in a popover (combobox search)
|
|
145
|
+
const target = e.target as HTMLElement;
|
|
146
|
+
if (
|
|
147
|
+
target.closest('[data-slot="popover-content"]') ||
|
|
148
|
+
target.closest('[data-slot="command-input"]') ||
|
|
149
|
+
target.closest('[data-slot="command-input-wrapper"]')
|
|
150
|
+
) {
|
|
151
|
+
return; // Let popover handle keyboard events
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Esc to close (only if not submitting)
|
|
155
|
+
if (e.key === "Escape" && !showSuccess && !isSubmitting) {
|
|
156
|
+
onOpenChange(false);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Ctrl/Cmd + Enter to submit
|
|
161
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && !isSubmitting) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
const submitButton = formRef.current?.querySelector<HTMLButtonElement>(
|
|
164
|
+
'button[type="submit"]',
|
|
165
|
+
);
|
|
166
|
+
if (submitButton && !submitButton.disabled) {
|
|
167
|
+
submitButton.click();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
173
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
174
|
+
}, [open, showSuccess, isSubmitting, onOpenChange]);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Dialog open={open} onOpenChange={onOpenChange} modal={true}>
|
|
178
|
+
<DialogContent
|
|
179
|
+
className="max-w-2xl max-h-[90vh] sm:max-h-[90vh] h-[100vh] sm:h-auto rounded-none sm:rounded-lg overflow-hidden flex flex-col p-0"
|
|
180
|
+
onInteractOutside={(e) => {
|
|
181
|
+
// Prevent closing dialog when submitting or clicking on Popover
|
|
182
|
+
if (isSubmitting || showSuccess) {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const target = e.target as HTMLElement;
|
|
187
|
+
if (target.closest('[data-slot="popover-content"]')) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
}
|
|
190
|
+
}}
|
|
191
|
+
onEscapeKeyDown={(e) => {
|
|
192
|
+
// Don't close dialog if popover is open or submitting
|
|
193
|
+
if (isSubmitting || showSuccess) {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const popoverOpen = document.querySelector(
|
|
198
|
+
'[data-slot="popover-content"][data-state="open"]',
|
|
199
|
+
);
|
|
200
|
+
if (popoverOpen) {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
onPointerDownOutside={(e) => {
|
|
206
|
+
// Prevent closing dialog when submitting or clicking on Popover
|
|
207
|
+
if (isSubmitting || showSuccess) {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const target = e.target as HTMLElement;
|
|
212
|
+
if (target.closest('[data-slot="popover-content"]')) {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
}
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
{/* Loading Overlay */}
|
|
218
|
+
{isSubmitting && (
|
|
219
|
+
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
|
|
220
|
+
<div className="flex flex-col items-center gap-3 animate-in fade-in zoom-in-95 duration-200">
|
|
221
|
+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
222
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
223
|
+
{mode === "create"
|
|
224
|
+
? translations?.creating || "Creating..."
|
|
225
|
+
: translations?.updating || "Updating..."}
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* Success Animation Overlay */}
|
|
232
|
+
{showSuccess && (
|
|
233
|
+
<div className="absolute inset-0 bg-background/95 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
|
|
234
|
+
<div className="flex flex-col items-center gap-3 animate-in fade-in zoom-in-95 duration-300">
|
|
235
|
+
<CheckCircle2 className="h-12 w-12 text-green-500" />
|
|
236
|
+
<p className="text-lg font-semibold">
|
|
237
|
+
{translations?.savedSuccessfully || "Saved successfully!"}
|
|
238
|
+
</p>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* Sticky Header với Icon */}
|
|
244
|
+
<DialogHeader className="sticky top-0 z-10 bg-background border-b px-4 sm:px-6 pt-4 sm:pt-6 pb-3 sm:pb-4 mb-0">
|
|
245
|
+
<div className="flex items-start justify-between gap-3">
|
|
246
|
+
<div className="flex-1 min-w-0">
|
|
247
|
+
<div className="flex items-center gap-2 sm:gap-3">
|
|
248
|
+
{config.iconName && (
|
|
249
|
+
<div className="p-1.5 rounded-lg bg-primary/10 shrink-0">
|
|
250
|
+
<DynamicIcon
|
|
251
|
+
name={config.iconName as any}
|
|
252
|
+
className="h-4 w-4 sm:h-5 sm:w-5 text-primary"
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
<DialogTitle className="text-lg sm:text-xl font-bold">
|
|
257
|
+
{mode === "create"
|
|
258
|
+
? translations?.add
|
|
259
|
+
? `${translations.add} ${config.label}`
|
|
260
|
+
: `Create ${config.label}`
|
|
261
|
+
: translations?.edit
|
|
262
|
+
? `${translations.edit} ${config.label}`
|
|
263
|
+
: `Edit ${config.label}`}
|
|
264
|
+
</DialogTitle>
|
|
265
|
+
</div>
|
|
266
|
+
<DialogDescription className="mt-1 text-xs text-muted-foreground">
|
|
267
|
+
{mode === "create"
|
|
268
|
+
? translations?.addNew
|
|
269
|
+
? `${translations.addNew} ${config.label.toLowerCase()}`
|
|
270
|
+
: `Add a new ${config.label.toLowerCase()}`
|
|
271
|
+
: translations?.update
|
|
272
|
+
? `${translations.update} ${config.label.toLowerCase()}`
|
|
273
|
+
: `Update ${config.label.toLowerCase()} information`}
|
|
274
|
+
</DialogDescription>
|
|
275
|
+
</div>
|
|
276
|
+
{/* Form Progress in Header */}
|
|
277
|
+
{formProgress !== null && (
|
|
278
|
+
<div className="shrink-0">
|
|
279
|
+
<div className="flex items-center gap-2 min-w-[80px]">
|
|
280
|
+
<div className="flex-1 min-w-[60px]">
|
|
281
|
+
<div className="h-1.5 w-full bg-muted rounded-full overflow-hidden">
|
|
282
|
+
<div
|
|
283
|
+
className="h-full bg-primary transition-all duration-300"
|
|
284
|
+
style={{ width: `${formProgress}%` }}
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<span className="text-xs font-semibold text-muted-foreground tabular-nums whitespace-nowrap">
|
|
289
|
+
{formProgress}%
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
</DialogHeader>
|
|
296
|
+
|
|
297
|
+
{/* Scrollable Form Content */}
|
|
298
|
+
<div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4">
|
|
299
|
+
<CrudForm
|
|
300
|
+
key={`${config.name}-${mode}-${initialData?.id || "new"}`}
|
|
301
|
+
ref={formRef}
|
|
302
|
+
config={config}
|
|
303
|
+
initialData={initialData}
|
|
304
|
+
onSubmit={handleSubmit}
|
|
305
|
+
onCancel={handleCancel}
|
|
306
|
+
submitLabel={
|
|
307
|
+
mode === "create"
|
|
308
|
+
? translations?.create || "Create"
|
|
309
|
+
: translations?.update || "Update"
|
|
310
|
+
}
|
|
311
|
+
submittingLabel={
|
|
312
|
+
mode === "create"
|
|
313
|
+
? translations?.creating || "Creating..."
|
|
314
|
+
: translations?.updating || "Updating..."
|
|
315
|
+
}
|
|
316
|
+
cancelLabel={translations?.cancel || "Cancel"}
|
|
317
|
+
isSubmitting={isSubmitting}
|
|
318
|
+
mode={mode}
|
|
319
|
+
enableAutoSave={true}
|
|
320
|
+
onFormReady={(form) => {
|
|
321
|
+
// Cleanup previous subscription if exists
|
|
322
|
+
if (
|
|
323
|
+
progressSubscriptionRef.current &&
|
|
324
|
+
typeof progressSubscriptionRef.current === "function"
|
|
325
|
+
) {
|
|
326
|
+
try {
|
|
327
|
+
progressSubscriptionRef.current();
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error(
|
|
330
|
+
"Error cleaning up previous subscription:",
|
|
331
|
+
error,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
progressSubscriptionRef.current = null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Watch form values and update progress
|
|
338
|
+
// Filter out: hidden fields and display-only fields (DTO fields)
|
|
339
|
+
const visibleFields = config.fields.filter(
|
|
340
|
+
(field) => !field.hideInForm && !field.isDisplayOnly,
|
|
341
|
+
);
|
|
342
|
+
const totalFields = visibleFields.length;
|
|
343
|
+
|
|
344
|
+
if (totalFields > 5) {
|
|
345
|
+
// Calculate initial progress
|
|
346
|
+
const initialValues = form.getValues();
|
|
347
|
+
const initialFilledFields = visibleFields.filter((field) => {
|
|
348
|
+
const value = initialValues[field.name];
|
|
349
|
+
return (
|
|
350
|
+
value !== undefined &&
|
|
351
|
+
value !== null &&
|
|
352
|
+
value !== "" &&
|
|
353
|
+
!(Array.isArray(value) && value.length === 0)
|
|
354
|
+
);
|
|
355
|
+
}).length;
|
|
356
|
+
setFormProgress(
|
|
357
|
+
Math.round((initialFilledFields / totalFields) * 100),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Watch only visible fields instead of all fields for better performance
|
|
361
|
+
const visibleFieldNames = visibleFields.map(
|
|
362
|
+
(field) => field.name,
|
|
363
|
+
);
|
|
364
|
+
try {
|
|
365
|
+
// form.watch() returns an unsubscribe function directly, not an object with unsubscribe()
|
|
366
|
+
const unsubscribe = form.watch(
|
|
367
|
+
visibleFieldNames,
|
|
368
|
+
(formValues: Record<string, unknown>) => {
|
|
369
|
+
const filledFields = visibleFields.filter((field) => {
|
|
370
|
+
const value = formValues[field.name];
|
|
371
|
+
return (
|
|
372
|
+
value !== undefined &&
|
|
373
|
+
value !== null &&
|
|
374
|
+
value !== "" &&
|
|
375
|
+
!(Array.isArray(value) && value.length === 0)
|
|
376
|
+
);
|
|
377
|
+
}).length;
|
|
378
|
+
|
|
379
|
+
const progress = Math.round(
|
|
380
|
+
(filledFields / totalFields) * 100,
|
|
381
|
+
);
|
|
382
|
+
setFormProgress(progress);
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Store unsubscribe function directly, but only if it's actually a function
|
|
387
|
+
if (typeof unsubscribe === "function") {
|
|
388
|
+
progressSubscriptionRef.current = unsubscribe;
|
|
389
|
+
} else {
|
|
390
|
+
progressSubscriptionRef.current = null;
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error("Error setting up form watch:", error);
|
|
394
|
+
progressSubscriptionRef.current = null;
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
setFormProgress(null);
|
|
398
|
+
progressSubscriptionRef.current = null;
|
|
399
|
+
}
|
|
400
|
+
}}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
</DialogContent>
|
|
404
|
+
</Dialog>
|
|
405
|
+
);
|
|
406
|
+
}
|