@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,642 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
12
|
+
import { useForm } from "react-hook-form";
|
|
13
|
+
import { AlertCircle, Loader2, Save } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
import type { EntityConfig, FieldConfig } from "../../types";
|
|
16
|
+
|
|
17
|
+
import { filterDisplayOnlyFields, sortFieldsByOrder } from "../lib/crud-utils";
|
|
18
|
+
import {
|
|
19
|
+
buildSchemaFromConfig,
|
|
20
|
+
getDefaultValuesFromConfig,
|
|
21
|
+
} from "../lib/crud-validator";
|
|
22
|
+
import {
|
|
23
|
+
calculateFieldValue,
|
|
24
|
+
getDependentFields,
|
|
25
|
+
} from "../lib/field-calculator";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
Accordion,
|
|
29
|
+
AccordionContent,
|
|
30
|
+
AccordionItem,
|
|
31
|
+
AccordionTrigger,
|
|
32
|
+
} from "../../ui";
|
|
33
|
+
import { Alert, AlertDescription, AlertTitle } from "../../ui";
|
|
34
|
+
import { Badge } from "../../ui";
|
|
35
|
+
import { Button } from "../../ui";
|
|
36
|
+
import {
|
|
37
|
+
Card,
|
|
38
|
+
CardContent,
|
|
39
|
+
CardDescription,
|
|
40
|
+
CardHeader,
|
|
41
|
+
CardTitle,
|
|
42
|
+
} from "../../ui";
|
|
43
|
+
import { Form } from "../../ui";
|
|
44
|
+
import { Progress } from "../../ui";
|
|
45
|
+
import { CrudFieldRenderer } from "./crud-field-renderer";
|
|
46
|
+
|
|
47
|
+
interface CrudFormProps {
|
|
48
|
+
config: EntityConfig;
|
|
49
|
+
initialData?: Record<string, unknown>;
|
|
50
|
+
onSubmit: (data: Record<string, unknown>) => Promise<void> | void;
|
|
51
|
+
onCancel?: () => void;
|
|
52
|
+
submitLabel?: string;
|
|
53
|
+
cancelLabel?: string;
|
|
54
|
+
submittingLabel?: string;
|
|
55
|
+
isSubmitting?: boolean;
|
|
56
|
+
mode?: "create" | "edit";
|
|
57
|
+
enableAutoSave?: boolean;
|
|
58
|
+
onFormReady?: (form: ReturnType<typeof useForm>) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const CrudFormComponent = forwardRef<HTMLFormElement, CrudFormProps>(
|
|
62
|
+
function CrudForm(
|
|
63
|
+
{
|
|
64
|
+
config,
|
|
65
|
+
initialData,
|
|
66
|
+
onSubmit,
|
|
67
|
+
onCancel,
|
|
68
|
+
submitLabel = "Submit",
|
|
69
|
+
cancelLabel = "Cancel",
|
|
70
|
+
submittingLabel = "Submitting...",
|
|
71
|
+
isSubmitting: externalIsSubmitting,
|
|
72
|
+
mode = "create",
|
|
73
|
+
enableAutoSave = true,
|
|
74
|
+
onFormReady,
|
|
75
|
+
},
|
|
76
|
+
ref,
|
|
77
|
+
) {
|
|
78
|
+
const computedFields = useMemo(() => {
|
|
79
|
+
return config.fields.map((field) => {
|
|
80
|
+
const disableForMode =
|
|
81
|
+
(mode === "create" && field.disableOnCreate) ||
|
|
82
|
+
(mode === "edit" && field.disableOnEdit);
|
|
83
|
+
|
|
84
|
+
const shouldDisable = field.disabled || Boolean(disableForMode);
|
|
85
|
+
|
|
86
|
+
if (shouldDisable === field.disabled) {
|
|
87
|
+
return field;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const modifiedField = {
|
|
91
|
+
...field,
|
|
92
|
+
disabled: shouldDisable,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return modifiedField;
|
|
96
|
+
});
|
|
97
|
+
}, [config.fields, mode]);
|
|
98
|
+
|
|
99
|
+
const schema = buildSchemaFromConfig(computedFields);
|
|
100
|
+
const defaultValues = initialData || getDefaultValuesFromConfig(config);
|
|
101
|
+
const draftKey = `draft-${config.name}-${mode}`;
|
|
102
|
+
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
103
|
+
const [hasDraft, setHasDraft] = useState(false);
|
|
104
|
+
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
|
105
|
+
|
|
106
|
+
const form = useForm({
|
|
107
|
+
resolver: zodResolver(schema),
|
|
108
|
+
defaultValues,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Notify parent when form is ready
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (onFormReady) {
|
|
114
|
+
onFormReady(form);
|
|
115
|
+
}
|
|
116
|
+
}, [form, onFormReady]);
|
|
117
|
+
|
|
118
|
+
// Load draft from localStorage when form initializes (only for create mode)
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (
|
|
121
|
+
mode === "create" &&
|
|
122
|
+
enableAutoSave &&
|
|
123
|
+
typeof window !== "undefined" &&
|
|
124
|
+
!initialData
|
|
125
|
+
) {
|
|
126
|
+
try {
|
|
127
|
+
const draft = localStorage.getItem(draftKey);
|
|
128
|
+
if (draft) {
|
|
129
|
+
const draftData = JSON.parse(draft);
|
|
130
|
+
// Only load draft if it's not empty (has at least one non-empty field)
|
|
131
|
+
const hasData = Object.values(draftData).some(
|
|
132
|
+
(value) => value !== undefined && value !== null && value !== "",
|
|
133
|
+
);
|
|
134
|
+
if (hasData) {
|
|
135
|
+
form.reset(draftData);
|
|
136
|
+
setHasDraft(true);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("Error loading draft:", error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}, [mode, enableAutoSave, draftKey, initialData, form]);
|
|
144
|
+
|
|
145
|
+
// Auto-save draft to localStorage
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!enableAutoSave || mode !== "create") return;
|
|
148
|
+
|
|
149
|
+
let unsubscribe: (() => void) | { unsubscribe: () => void } | undefined;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// form.watch() may return an unsubscribe function or a Subscription object
|
|
153
|
+
unsubscribe = form.watch((data: Record<string, unknown>) => {
|
|
154
|
+
// Clear existing timeout
|
|
155
|
+
if (saveTimeoutRef.current) {
|
|
156
|
+
clearTimeout(saveTimeoutRef.current);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Debounce save to localStorage (1 second delay)
|
|
160
|
+
saveTimeoutRef.current = setTimeout(() => {
|
|
161
|
+
try {
|
|
162
|
+
if (typeof window !== "undefined") {
|
|
163
|
+
// Only save if there's actual data
|
|
164
|
+
const hasData = Object.values(data).some(
|
|
165
|
+
(value) =>
|
|
166
|
+
value !== undefined && value !== null && value !== "",
|
|
167
|
+
);
|
|
168
|
+
if (hasData) {
|
|
169
|
+
localStorage.setItem(draftKey, JSON.stringify(data));
|
|
170
|
+
setHasDraft(true);
|
|
171
|
+
setLastSaved(new Date());
|
|
172
|
+
} else {
|
|
173
|
+
// Remove draft if form is empty
|
|
174
|
+
localStorage.removeItem(draftKey);
|
|
175
|
+
setHasDraft(false);
|
|
176
|
+
setLastSaved(null);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error("Error saving draft:", error);
|
|
181
|
+
}
|
|
182
|
+
}, 1000);
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error("Error setting up form watch:", error);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
// Handle unsubscribe - can be a function or a Subscription object
|
|
190
|
+
if (unsubscribe) {
|
|
191
|
+
try {
|
|
192
|
+
if (typeof unsubscribe === "function") {
|
|
193
|
+
unsubscribe();
|
|
194
|
+
} else if (
|
|
195
|
+
unsubscribe &&
|
|
196
|
+
typeof unsubscribe === "object" &&
|
|
197
|
+
"unsubscribe" in unsubscribe
|
|
198
|
+
) {
|
|
199
|
+
unsubscribe.unsubscribe();
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error("Error unsubscribing from form watch:", error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (saveTimeoutRef.current) {
|
|
206
|
+
clearTimeout(saveTimeoutRef.current);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}, [form, enableAutoSave, mode, draftKey]);
|
|
210
|
+
|
|
211
|
+
// Clear draft after successful submit
|
|
212
|
+
const handleSubmit = async (data: Record<string, unknown>) => {
|
|
213
|
+
try {
|
|
214
|
+
// Filter out display-only fields before submitting to API
|
|
215
|
+
const filteredData = filterDisplayOnlyFields(data, config);
|
|
216
|
+
|
|
217
|
+
// Debug log for user-supplier form
|
|
218
|
+
if (config.name === "user-supplier") {
|
|
219
|
+
console.log("Form submit data:", {
|
|
220
|
+
userId: filteredData.userId,
|
|
221
|
+
supplierId: filteredData.supplierId,
|
|
222
|
+
userIdType: typeof filteredData.userId,
|
|
223
|
+
supplierIdType: typeof filteredData.supplierId,
|
|
224
|
+
fullData: filteredData,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await onSubmit(filteredData);
|
|
229
|
+
// Clear draft after successful submit
|
|
230
|
+
if (
|
|
231
|
+
mode === "create" &&
|
|
232
|
+
enableAutoSave &&
|
|
233
|
+
typeof window !== "undefined"
|
|
234
|
+
) {
|
|
235
|
+
localStorage.removeItem(draftKey);
|
|
236
|
+
setHasDraft(false);
|
|
237
|
+
setLastSaved(null);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error("Form submission error:", error);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Clear draft when canceling
|
|
245
|
+
const handleCancel = () => {
|
|
246
|
+
if (
|
|
247
|
+
mode === "create" &&
|
|
248
|
+
enableAutoSave &&
|
|
249
|
+
typeof window !== "undefined"
|
|
250
|
+
) {
|
|
251
|
+
// Optionally clear draft on cancel (or keep it for next time)
|
|
252
|
+
// localStorage.removeItem(draftKey)
|
|
253
|
+
}
|
|
254
|
+
onCancel?.();
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const { isSubmitting: formIsSubmitting, errors } = form.formState;
|
|
258
|
+
const isSubmitting = externalIsSubmitting ?? formIsSubmitting;
|
|
259
|
+
|
|
260
|
+
// ✅ Get all fields that need to be watched for calculations and showWhen
|
|
261
|
+
const fieldsToWatch = useMemo(() => {
|
|
262
|
+
const watchFields = new Set<string>();
|
|
263
|
+
computedFields.forEach((field) => {
|
|
264
|
+
// Watch fields for calculations
|
|
265
|
+
if (field.calculate?.dependsOn) {
|
|
266
|
+
field.calculate.dependsOn.forEach((depField) => {
|
|
267
|
+
watchFields.add(depField);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// Watch fields for cascading
|
|
271
|
+
if (field.cascade?.triggerField) {
|
|
272
|
+
watchFields.add(field.cascade.triggerField);
|
|
273
|
+
}
|
|
274
|
+
// Watch fields for showWhen conditions
|
|
275
|
+
if (field.showWhen?.field) {
|
|
276
|
+
watchFields.add(field.showWhen.field);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return Array.from(watchFields);
|
|
280
|
+
}, [computedFields]);
|
|
281
|
+
|
|
282
|
+
// ✅ Watch form values - watch() without args returns all values as an object
|
|
283
|
+
const formValues = form.watch();
|
|
284
|
+
|
|
285
|
+
// Handle field calculations
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
computedFields.forEach((field) => {
|
|
288
|
+
if (field.calculate) {
|
|
289
|
+
const { dependsOn, mode = "onChange" } = field.calculate;
|
|
290
|
+
|
|
291
|
+
// Check if any dependent field has changed
|
|
292
|
+
const hasDependentValues = dependsOn.every(
|
|
293
|
+
(depField) =>
|
|
294
|
+
formValues[depField] !== undefined &&
|
|
295
|
+
formValues[depField] !== null &&
|
|
296
|
+
formValues[depField] !== "",
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (hasDependentValues) {
|
|
300
|
+
const calculatedValue = calculateFieldValue(field, formValues);
|
|
301
|
+
const currentValue = form.getValues(field.name);
|
|
302
|
+
|
|
303
|
+
// Only update if value changed
|
|
304
|
+
if (
|
|
305
|
+
calculatedValue !== currentValue &&
|
|
306
|
+
calculatedValue !== undefined
|
|
307
|
+
) {
|
|
308
|
+
form.setValue(field.name, calculatedValue, {
|
|
309
|
+
shouldValidate: false,
|
|
310
|
+
shouldDirty: false,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}, [form, formValues, computedFields]);
|
|
317
|
+
|
|
318
|
+
// Handle field cascading
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
computedFields.forEach((field) => {
|
|
321
|
+
if (field.cascade) {
|
|
322
|
+
const { triggerField, action } = field.cascade;
|
|
323
|
+
const triggerValue = formValues[triggerField];
|
|
324
|
+
const previousTriggerValue = form.getValues(
|
|
325
|
+
`_previous_${triggerField}`,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Check if trigger field changed
|
|
329
|
+
if (triggerValue !== previousTriggerValue) {
|
|
330
|
+
if (action === "clear") {
|
|
331
|
+
form.setValue(field.name, undefined, {
|
|
332
|
+
shouldValidate: false,
|
|
333
|
+
shouldDirty: false,
|
|
334
|
+
});
|
|
335
|
+
} else if (action === "reset") {
|
|
336
|
+
const defaultValue = field.defaultValue;
|
|
337
|
+
form.setValue(field.name, defaultValue, {
|
|
338
|
+
shouldValidate: false,
|
|
339
|
+
shouldDirty: false,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Store current trigger value for next comparison
|
|
344
|
+
form.setValue(`_previous_${triggerField}`, triggerValue, {
|
|
345
|
+
shouldValidate: false,
|
|
346
|
+
shouldDirty: false,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}, [form, formValues, computedFields]);
|
|
352
|
+
|
|
353
|
+
// Get field labels for error summary
|
|
354
|
+
const getFieldLabel = (fieldName: string) => {
|
|
355
|
+
const field = computedFields.find((f) => f.name === fieldName);
|
|
356
|
+
return field?.label || fieldName;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Check if field should be visible based on showWhen condition
|
|
360
|
+
const isFieldVisible = useMemo(() => {
|
|
361
|
+
return (field: FieldConfig): boolean => {
|
|
362
|
+
if (!field.showWhen) return true;
|
|
363
|
+
|
|
364
|
+
const {
|
|
365
|
+
field: watchField,
|
|
366
|
+
value,
|
|
367
|
+
operator = "equals",
|
|
368
|
+
} = field.showWhen;
|
|
369
|
+
const watchValue = formValues[watchField];
|
|
370
|
+
|
|
371
|
+
switch (operator) {
|
|
372
|
+
case "equals":
|
|
373
|
+
return watchValue === value;
|
|
374
|
+
case "notEquals":
|
|
375
|
+
return watchValue !== value;
|
|
376
|
+
case "contains":
|
|
377
|
+
return String(watchValue).includes(String(value));
|
|
378
|
+
case "greaterThan":
|
|
379
|
+
return Number(watchValue) > Number(value);
|
|
380
|
+
case "lessThan":
|
|
381
|
+
return Number(watchValue) < Number(value);
|
|
382
|
+
case "isEmpty":
|
|
383
|
+
return (
|
|
384
|
+
watchValue === undefined ||
|
|
385
|
+
watchValue === null ||
|
|
386
|
+
watchValue === ""
|
|
387
|
+
);
|
|
388
|
+
case "isNotEmpty":
|
|
389
|
+
return (
|
|
390
|
+
watchValue !== undefined &&
|
|
391
|
+
watchValue !== null &&
|
|
392
|
+
watchValue !== ""
|
|
393
|
+
);
|
|
394
|
+
default:
|
|
395
|
+
return watchValue === value;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}, [formValues]);
|
|
399
|
+
|
|
400
|
+
// Error summary
|
|
401
|
+
const errorEntries = Object.entries(errors);
|
|
402
|
+
const hasErrors = errorEntries.length > 0;
|
|
403
|
+
|
|
404
|
+
// Group fields by section
|
|
405
|
+
const groupedFields = useMemo(() => {
|
|
406
|
+
// Sort fields by order first, then filter
|
|
407
|
+
const sortedFields = sortFieldsByOrder(computedFields);
|
|
408
|
+
// Filter out: hidden fields, display-only fields (DTO fields), and fields that don't meet visibility conditions
|
|
409
|
+
const visibleFields = sortedFields.filter(
|
|
410
|
+
(field) =>
|
|
411
|
+
!field.hideInForm && !field.isDisplayOnly && isFieldVisible(field),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// If formSections is defined, use it
|
|
415
|
+
if (config.formSections && config.formSections.length > 0) {
|
|
416
|
+
return config.formSections.map((section) => ({
|
|
417
|
+
...section,
|
|
418
|
+
fields: visibleFields.filter((field) =>
|
|
419
|
+
section.fields.includes(field.name),
|
|
420
|
+
),
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Otherwise, group by field.section property
|
|
425
|
+
const sectionsMap = new Map<string, FieldConfig[]>();
|
|
426
|
+
const ungroupedFields: FieldConfig[] = [];
|
|
427
|
+
|
|
428
|
+
visibleFields.forEach((field) => {
|
|
429
|
+
if (field.section) {
|
|
430
|
+
if (!sectionsMap.has(field.section)) {
|
|
431
|
+
sectionsMap.set(field.section, []);
|
|
432
|
+
}
|
|
433
|
+
sectionsMap.get(field.section)!.push(field);
|
|
434
|
+
} else {
|
|
435
|
+
ungroupedFields.push(field);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const sections: Array<{
|
|
440
|
+
title: string;
|
|
441
|
+
description?: string;
|
|
442
|
+
fields: FieldConfig[];
|
|
443
|
+
}> = [];
|
|
444
|
+
|
|
445
|
+
// Add grouped sections
|
|
446
|
+
sectionsMap.forEach((fields, title) => {
|
|
447
|
+
sections.push({ title, fields });
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Add ungrouped fields as a default section if there are any
|
|
451
|
+
if (ungroupedFields.length > 0) {
|
|
452
|
+
sections.push({ title: "", fields: ungroupedFields });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return sections.length > 0
|
|
456
|
+
? sections
|
|
457
|
+
: [{ title: "", fields: visibleFields }];
|
|
458
|
+
}, [computedFields, config.formSections, isFieldVisible]);
|
|
459
|
+
|
|
460
|
+
// ✅ Progress calculation - use form.getValues() to get all values, not just watched ones
|
|
461
|
+
const progress = useMemo(() => {
|
|
462
|
+
const allFormValues = form.getValues(); // Get all form values for progress calculation
|
|
463
|
+
// Filter out: hidden fields, display-only fields (DTO fields), and fields that don't meet visibility conditions
|
|
464
|
+
const visibleFields = computedFields.filter(
|
|
465
|
+
(field) =>
|
|
466
|
+
!field.hideInForm && !field.isDisplayOnly && isFieldVisible(field),
|
|
467
|
+
);
|
|
468
|
+
const totalFields = visibleFields.length;
|
|
469
|
+
if (totalFields === 0) return 100;
|
|
470
|
+
|
|
471
|
+
const filledFields = visibleFields.filter((field) => {
|
|
472
|
+
const value = allFormValues[field.name];
|
|
473
|
+
return (
|
|
474
|
+
value !== undefined &&
|
|
475
|
+
value !== null &&
|
|
476
|
+
value !== "" &&
|
|
477
|
+
!(Array.isArray(value) && value.length === 0)
|
|
478
|
+
);
|
|
479
|
+
}).length;
|
|
480
|
+
|
|
481
|
+
return Math.round((filledFields / totalFields) * 100);
|
|
482
|
+
}, [form, computedFields, isFieldVisible]);
|
|
483
|
+
|
|
484
|
+
// Render field with two-column layout
|
|
485
|
+
const renderField = (field: FieldConfig) => {
|
|
486
|
+
const fullWidth =
|
|
487
|
+
field.fullWidth ||
|
|
488
|
+
field.type === "textarea" ||
|
|
489
|
+
field.type === "multiselect" ||
|
|
490
|
+
(field.type === "text" && field.rows && field.rows > 3);
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<div key={field.name} className={fullWidth ? "md:col-span-2" : ""}>
|
|
494
|
+
<CrudFieldRenderer field={field} mode={mode} />
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// Check if we should use sections (if more than 1 section or sections have titles)
|
|
500
|
+
const useSections =
|
|
501
|
+
groupedFields.length > 1 || groupedFields.some((s) => s.title);
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<Form {...form}>
|
|
505
|
+
<form
|
|
506
|
+
ref={ref}
|
|
507
|
+
onSubmit={form.handleSubmit(handleSubmit)}
|
|
508
|
+
className="space-y-6"
|
|
509
|
+
>
|
|
510
|
+
{/* Draft Indicator */}
|
|
511
|
+
{hasDraft && mode === "create" && enableAutoSave && (
|
|
512
|
+
<Alert className="mb-4 border-blue-200 bg-blue-50/50 dark:bg-blue-950/20">
|
|
513
|
+
<Save className="h-4 w-4 text-blue-600" />
|
|
514
|
+
<AlertTitle className="text-blue-900 dark:text-blue-100">
|
|
515
|
+
Draft saved
|
|
516
|
+
</AlertTitle>
|
|
517
|
+
<AlertDescription className="text-blue-800 dark:text-blue-200">
|
|
518
|
+
{lastSaved
|
|
519
|
+
? `Your changes were saved automatically at ${lastSaved.toLocaleTimeString()}.`
|
|
520
|
+
: "Your changes are being saved automatically."}
|
|
521
|
+
</AlertDescription>
|
|
522
|
+
</Alert>
|
|
523
|
+
)}
|
|
524
|
+
|
|
525
|
+
{/* Error Summary */}
|
|
526
|
+
{hasErrors && (
|
|
527
|
+
<Alert variant="destructive" className="mb-4">
|
|
528
|
+
<AlertCircle className="h-4 w-4" />
|
|
529
|
+
<AlertTitle>Please fix the following errors:</AlertTitle>
|
|
530
|
+
<AlertDescription>
|
|
531
|
+
<ul className="list-disc list-inside space-y-1 mt-2 text-sm">
|
|
532
|
+
{errorEntries.map(([fieldName, error]) => (
|
|
533
|
+
<li key={fieldName}>
|
|
534
|
+
<strong>{getFieldLabel(fieldName)}</strong>:{" "}
|
|
535
|
+
{error?.message as string}
|
|
536
|
+
</li>
|
|
537
|
+
))}
|
|
538
|
+
</ul>
|
|
539
|
+
</AlertDescription>
|
|
540
|
+
</Alert>
|
|
541
|
+
)}
|
|
542
|
+
|
|
543
|
+
{/* Form Fields with Sections */}
|
|
544
|
+
{useSections ? (
|
|
545
|
+
<Accordion
|
|
546
|
+
type="multiple"
|
|
547
|
+
defaultValue={groupedFields
|
|
548
|
+
.map((_, index) => `section-${index}`)
|
|
549
|
+
.filter(
|
|
550
|
+
(_, index) =>
|
|
551
|
+
config.formSections?.[index]?.defaultOpen !== false,
|
|
552
|
+
)}
|
|
553
|
+
className="space-y-4"
|
|
554
|
+
>
|
|
555
|
+
{groupedFields.map((section, sectionIndex) => {
|
|
556
|
+
if (section.fields.length === 0) return null;
|
|
557
|
+
|
|
558
|
+
return (
|
|
559
|
+
<AccordionItem
|
|
560
|
+
key={`section-${sectionIndex}`}
|
|
561
|
+
value={`section-${sectionIndex}`}
|
|
562
|
+
className="border rounded-lg px-4"
|
|
563
|
+
>
|
|
564
|
+
{section.title && (
|
|
565
|
+
<AccordionTrigger className="hover:no-underline">
|
|
566
|
+
<div className="flex flex-col items-start text-left">
|
|
567
|
+
<CardTitle className="text-base font-semibold">
|
|
568
|
+
{section.title}
|
|
569
|
+
</CardTitle>
|
|
570
|
+
{section.description && (
|
|
571
|
+
<CardDescription className="text-xs mt-0.5">
|
|
572
|
+
{section.description}
|
|
573
|
+
</CardDescription>
|
|
574
|
+
)}
|
|
575
|
+
</div>
|
|
576
|
+
</AccordionTrigger>
|
|
577
|
+
)}
|
|
578
|
+
<AccordionContent>
|
|
579
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2">
|
|
580
|
+
{section.fields.map(renderField)}
|
|
581
|
+
</div>
|
|
582
|
+
</AccordionContent>
|
|
583
|
+
</AccordionItem>
|
|
584
|
+
);
|
|
585
|
+
})}
|
|
586
|
+
</Accordion>
|
|
587
|
+
) : (
|
|
588
|
+
/* Simple two-column layout without sections */
|
|
589
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
590
|
+
{groupedFields[0]?.fields.map(renderField)}
|
|
591
|
+
</div>
|
|
592
|
+
)}
|
|
593
|
+
|
|
594
|
+
{/* Form Actions */}
|
|
595
|
+
<div className="flex items-center justify-between gap-2 pt-4 border-t sticky bottom-0 bg-background -mx-4 sm:-mx-6 px-4 sm:px-6 pb-0">
|
|
596
|
+
{/* Draft indicator on left */}
|
|
597
|
+
{hasDraft && mode === "create" && enableAutoSave && (
|
|
598
|
+
<Badge variant="secondary" className="text-xs">
|
|
599
|
+
<Save className="mr-1 h-3 w-3" />
|
|
600
|
+
Draft saved
|
|
601
|
+
</Badge>
|
|
602
|
+
)}
|
|
603
|
+
|
|
604
|
+
{/* Action buttons on right */}
|
|
605
|
+
<div className="flex gap-2 ml-auto">
|
|
606
|
+
{onCancel && (
|
|
607
|
+
<Button
|
|
608
|
+
type="button"
|
|
609
|
+
variant="outline"
|
|
610
|
+
onClick={handleCancel}
|
|
611
|
+
disabled={isSubmitting}
|
|
612
|
+
className="min-w-[80px]"
|
|
613
|
+
>
|
|
614
|
+
{cancelLabel}
|
|
615
|
+
</Button>
|
|
616
|
+
)}
|
|
617
|
+
<Button
|
|
618
|
+
type="submit"
|
|
619
|
+
disabled={isSubmitting}
|
|
620
|
+
className="min-w-[100px]"
|
|
621
|
+
>
|
|
622
|
+
{isSubmitting ? (
|
|
623
|
+
<>
|
|
624
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
625
|
+
<span className="hidden sm:inline">{submittingLabel}</span>
|
|
626
|
+
<span className="sm:hidden">...</span>
|
|
627
|
+
</>
|
|
628
|
+
) : (
|
|
629
|
+
submitLabel
|
|
630
|
+
)}
|
|
631
|
+
</Button>
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
</form>
|
|
635
|
+
</Form>
|
|
636
|
+
);
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
CrudFormComponent.displayName = "CrudForm";
|
|
641
|
+
|
|
642
|
+
export const CrudForm = CrudFormComponent;
|