@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,290 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parseCSV,
|
|
4
|
+
parseJSON,
|
|
5
|
+
validateImportData,
|
|
6
|
+
exportToCSV,
|
|
7
|
+
exportToJSON,
|
|
8
|
+
exportToXLSX,
|
|
9
|
+
} from "./import-export-service";
|
|
10
|
+
import type { EntityConfig } from "../../types";
|
|
11
|
+
import * as XLSX from "xlsx";
|
|
12
|
+
|
|
13
|
+
// Polyfill File.text() if missing or just mock it on instances
|
|
14
|
+
function createFile(content: string, name: string, type: string) {
|
|
15
|
+
const file = new File([content], name, { type });
|
|
16
|
+
// Mock text method
|
|
17
|
+
Object.defineProperty(file, "text", {
|
|
18
|
+
value: () => Promise.resolve(content),
|
|
19
|
+
writable: true,
|
|
20
|
+
});
|
|
21
|
+
// Mock arrayBuffer
|
|
22
|
+
Object.defineProperty(file, "arrayBuffer", {
|
|
23
|
+
value: () => Promise.resolve(new TextEncoder().encode(content)),
|
|
24
|
+
writable: true,
|
|
25
|
+
});
|
|
26
|
+
return file;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Mock xlsx
|
|
30
|
+
vi.mock("xlsx", () => ({
|
|
31
|
+
read: vi.fn(() => ({
|
|
32
|
+
SheetNames: ["Sheet1"],
|
|
33
|
+
Sheets: {
|
|
34
|
+
Sheet1: {},
|
|
35
|
+
},
|
|
36
|
+
})),
|
|
37
|
+
utils: {
|
|
38
|
+
sheet_to_json: vi.fn(() => [{ name: "Alice", age: 30 }]),
|
|
39
|
+
book_new: vi.fn(() => ({})),
|
|
40
|
+
aoa_to_sheet: vi.fn(),
|
|
41
|
+
book_append_sheet: vi.fn(),
|
|
42
|
+
json_to_sheet: vi.fn(),
|
|
43
|
+
},
|
|
44
|
+
write: vi.fn(() => new ArrayBuffer(8)),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
describe("ImportExportService", () => {
|
|
48
|
+
describe("parseCSV", () => {
|
|
49
|
+
it("should parse valid CSV", async () => {
|
|
50
|
+
const csvContent = "name,age\nAlice,30\nBob,25";
|
|
51
|
+
const file = createFile(csvContent, "test.csv", "text/csv");
|
|
52
|
+
const result = await parseCSV(file);
|
|
53
|
+
expect(result).toHaveLength(2);
|
|
54
|
+
expect(result[0]).toEqual({ name: "Alice", age: "30" });
|
|
55
|
+
expect(result[1]).toEqual({ name: "Bob", age: "25" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should handle empty file", async () => {
|
|
59
|
+
const file = createFile("", "empty.csv", "text/csv");
|
|
60
|
+
const result = await parseCSV(file);
|
|
61
|
+
expect(result).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("parseJSON", () => {
|
|
66
|
+
it("should parse valid JSON array", async () => {
|
|
67
|
+
const jsonContent = JSON.stringify([{ name: "Alice", age: 30 }]);
|
|
68
|
+
const file = createFile(jsonContent, "test.json", "application/json");
|
|
69
|
+
const result = await parseJSON(file);
|
|
70
|
+
expect(result).toHaveLength(1);
|
|
71
|
+
expect(result[0]).toEqual({ name: "Alice", age: 30 });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should parse single JSON object", async () => {
|
|
75
|
+
const jsonContent = JSON.stringify({ name: "Alice", age: 30 });
|
|
76
|
+
const file = createFile(jsonContent, "test.json", "application/json");
|
|
77
|
+
const result = await parseJSON(file);
|
|
78
|
+
expect(result).toHaveLength(1);
|
|
79
|
+
expect(result[0]).toEqual({ name: "Alice", age: 30 });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("validateImportData", () => {
|
|
84
|
+
const mockConfig: EntityConfig = {
|
|
85
|
+
fields: [
|
|
86
|
+
{ name: "name", label: "Name", type: "text", required: true },
|
|
87
|
+
{ name: "age", label: "Age", type: "number" },
|
|
88
|
+
{ name: "email", label: "Email", type: "email" },
|
|
89
|
+
],
|
|
90
|
+
apiEndpoint: "/api/test",
|
|
91
|
+
name: "test",
|
|
92
|
+
label: "Test",
|
|
93
|
+
pluralLabel: "Tests",
|
|
94
|
+
idField: "id",
|
|
95
|
+
displayField: "name",
|
|
96
|
+
} as unknown as EntityConfig; // Partial mock
|
|
97
|
+
|
|
98
|
+
it("should validate valid data", () => {
|
|
99
|
+
const data = [{ name: "Alice", age: "30", email: "alice@example.com" }];
|
|
100
|
+
const result = validateImportData(data, mockConfig);
|
|
101
|
+
expect(result.success).toBe(true);
|
|
102
|
+
expect(result.errors).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should catch required field errors", () => {
|
|
106
|
+
const data = [{ age: "30" }]; // missing name
|
|
107
|
+
const result = validateImportData(data, mockConfig);
|
|
108
|
+
expect(result.success).toBe(false);
|
|
109
|
+
expect(result.errors[0].message).toContain("Name là bắt buộc");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should catch type errors", () => {
|
|
113
|
+
const data = [
|
|
114
|
+
{ name: "Bob", age: "invalid" },
|
|
115
|
+
{ name: "Charlie", email: "not-an-email" },
|
|
116
|
+
];
|
|
117
|
+
const result = validateImportData(data, mockConfig);
|
|
118
|
+
expect(result.success).toBe(false);
|
|
119
|
+
expect(result.errors).toHaveLength(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should validate case-insensitive options", () => {
|
|
123
|
+
const configWithOptions: EntityConfig = {
|
|
124
|
+
...mockConfig,
|
|
125
|
+
fields: [
|
|
126
|
+
...mockConfig.fields,
|
|
127
|
+
{
|
|
128
|
+
name: "status",
|
|
129
|
+
label: "Status",
|
|
130
|
+
type: "select",
|
|
131
|
+
options: [
|
|
132
|
+
{ label: "Active", value: "active" },
|
|
133
|
+
{ label: "Inactive", value: "inactive" },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const data = [
|
|
140
|
+
{ name: "User1", status: "Active" },
|
|
141
|
+
{ name: "User2", status: "ACTIVE" },
|
|
142
|
+
{ name: "User3", status: "active" },
|
|
143
|
+
];
|
|
144
|
+
const result = validateImportData(data, configWithOptions);
|
|
145
|
+
expect(result.success).toBe(true);
|
|
146
|
+
expect(result.errors).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should reject invalid options", () => {
|
|
150
|
+
const configWithOptions: EntityConfig = {
|
|
151
|
+
...mockConfig,
|
|
152
|
+
fields: [
|
|
153
|
+
...mockConfig.fields,
|
|
154
|
+
{
|
|
155
|
+
name: "status",
|
|
156
|
+
label: "Status",
|
|
157
|
+
type: "select",
|
|
158
|
+
options: ["red", "blue"],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const data = [{ name: "User1", status: "green" }];
|
|
164
|
+
const result = validateImportData(data, configWithOptions);
|
|
165
|
+
expect(result.success).toBe(false);
|
|
166
|
+
expect(result.errors[0].message).toContain("Status không hợp lệ");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("exportToCSV", () => {
|
|
171
|
+
it("should export data to CSV", () => {
|
|
172
|
+
const data = [{ name: "Alice", age: 30 }];
|
|
173
|
+
const result = exportToCSV(data);
|
|
174
|
+
expect(result).toContain("name,age");
|
|
175
|
+
expect(result).toContain("Alice,30");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should handle specific fields", () => {
|
|
179
|
+
const data = [{ name: "Alice", age: 30, hidden: "secret" }];
|
|
180
|
+
const result = exportToCSV(data, ["name"]);
|
|
181
|
+
expect(result).toContain("name");
|
|
182
|
+
expect(result).not.toContain("age");
|
|
183
|
+
expect(result).toContain("Alice");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("exportToJSON", () => {
|
|
188
|
+
it("should export data to JSON", () => {
|
|
189
|
+
const data = [{ name: "Alice", age: 30 }];
|
|
190
|
+
const result = exportToJSON(data);
|
|
191
|
+
expect(JSON.parse(result)).toEqual(data);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should handle specific fields", () => {
|
|
195
|
+
const data = [{ name: "Alice", age: 30, hidden: "secret" }];
|
|
196
|
+
const result = exportToJSON(data, ["name"]);
|
|
197
|
+
const parsed = JSON.parse(result);
|
|
198
|
+
expect(parsed[0]).toHaveProperty("name");
|
|
199
|
+
expect(parsed[0]).not.toHaveProperty("age");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Since we mocked xlsx, we can test using those mocks
|
|
204
|
+
describe("XLSX functions", () => {
|
|
205
|
+
it("should parse XLSX", async () => {
|
|
206
|
+
const { parseXLSX } = await import("./import-export-service");
|
|
207
|
+
const file = createFile(
|
|
208
|
+
"dummy",
|
|
209
|
+
"test.xlsx",
|
|
210
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
211
|
+
);
|
|
212
|
+
const result = await parseXLSX(file);
|
|
213
|
+
expect(result).toEqual([{ name: "Alice", age: 30 }]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should generate XLSX template", async () => {
|
|
217
|
+
const { generateXLSXTemplate } = await import("./import-export-service");
|
|
218
|
+
const mockConfig: EntityConfig = {
|
|
219
|
+
fields: [{ name: "name", label: "Name", type: "text" }],
|
|
220
|
+
apiEndpoint: "/api",
|
|
221
|
+
name: "test",
|
|
222
|
+
label: "Test",
|
|
223
|
+
pluralLabel: "Tests",
|
|
224
|
+
idField: "id",
|
|
225
|
+
displayField: "name",
|
|
226
|
+
} as any;
|
|
227
|
+
const blob = await generateXLSXTemplate(mockConfig);
|
|
228
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should export to XLSX", async () => {
|
|
232
|
+
const { exportToXLSX } = await import("./import-export-service");
|
|
233
|
+
const data = [{ name: "Alice" }];
|
|
234
|
+
const blob = await exportToXLSX(data);
|
|
235
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should export to XLSX with specific fields", async () => {
|
|
239
|
+
const { exportToXLSX } = await import("./import-export-service");
|
|
240
|
+
const data = [{ name: "Alice", age: 30 }];
|
|
241
|
+
const blob = await exportToXLSX(data, ["name"]);
|
|
242
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
243
|
+
// Verify filtered data passed to xlsx (via mock)
|
|
244
|
+
const XLSX = await import("xlsx");
|
|
245
|
+
expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith(
|
|
246
|
+
expect.arrayContaining([expect.objectContaining({ name: "Alice" })]),
|
|
247
|
+
);
|
|
248
|
+
// Should NOT check for age absence strictly on the mock call args because json_to_sheet receives the mapped array
|
|
249
|
+
// But we can check expected structure if we spy carefully.
|
|
250
|
+
// In this mock, we just check call happened.
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("downloadFile", () => {
|
|
255
|
+
it("should trigger download", async () => {
|
|
256
|
+
const { downloadFile } = await import("./import-export-service");
|
|
257
|
+
const blob = new Blob(["test"]);
|
|
258
|
+
|
|
259
|
+
// Mock DOM
|
|
260
|
+
const link = {
|
|
261
|
+
href: "",
|
|
262
|
+
download: "",
|
|
263
|
+
click: vi.fn(),
|
|
264
|
+
style: {},
|
|
265
|
+
};
|
|
266
|
+
const createElementSpy = vi
|
|
267
|
+
.spyOn(document, "createElement")
|
|
268
|
+
.mockReturnValue(link as any);
|
|
269
|
+
const appendChildSpy = vi
|
|
270
|
+
.spyOn(document.body, "appendChild")
|
|
271
|
+
.mockImplementation(() => link as any);
|
|
272
|
+
const removeChildSpy = vi
|
|
273
|
+
.spyOn(document.body, "removeChild")
|
|
274
|
+
.mockImplementation(() => link as any);
|
|
275
|
+
|
|
276
|
+
// Mock URL
|
|
277
|
+
global.URL.createObjectURL = vi.fn(() => "blob:url");
|
|
278
|
+
global.URL.revokeObjectURL = vi.fn();
|
|
279
|
+
|
|
280
|
+
downloadFile(blob, "test.csv");
|
|
281
|
+
|
|
282
|
+
expect(createElementSpy).toHaveBeenCalledWith("a");
|
|
283
|
+
expect(link.download).toBe("test.csv");
|
|
284
|
+
expect(link.click).toHaveBeenCalled();
|
|
285
|
+
expect(appendChildSpy).toHaveBeenCalled();
|
|
286
|
+
expect(removeChildSpy).toHaveBeenCalled();
|
|
287
|
+
expect(global.URL.revokeObjectURL).toHaveBeenCalledWith("blob:url");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type { EntityConfig, ImportOptions, ImportResult } from "../../types";
|
|
2
|
+
|
|
3
|
+
function getRowValue(
|
|
4
|
+
row: Record<string, unknown>,
|
|
5
|
+
field: { name: string; label: string },
|
|
6
|
+
): unknown {
|
|
7
|
+
// Prefer machine key (field.name), but support human header (field.label)
|
|
8
|
+
// for backward compatibility with older templates.
|
|
9
|
+
if (Object.prototype.hasOwnProperty.call(row, field.name)) {
|
|
10
|
+
return row[field.name];
|
|
11
|
+
}
|
|
12
|
+
if (Object.prototype.hasOwnProperty.call(row, field.label)) {
|
|
13
|
+
return row[field.label];
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse CSV file
|
|
20
|
+
*/
|
|
21
|
+
export async function parseCSV(file: File): Promise<Record<string, unknown>[]> {
|
|
22
|
+
const text = await file.text();
|
|
23
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
24
|
+
if (lines.length === 0) return [];
|
|
25
|
+
|
|
26
|
+
const headers = lines[0].split(",").map((h) => h.trim());
|
|
27
|
+
const rows: Record<string, unknown>[] = [];
|
|
28
|
+
|
|
29
|
+
for (let i = 1; i < lines.length; i++) {
|
|
30
|
+
const values = lines[i].split(",").map((v) => v.trim());
|
|
31
|
+
const row: Record<string, unknown> = {};
|
|
32
|
+
headers.forEach((header, index) => {
|
|
33
|
+
row[header] = values[index] || "";
|
|
34
|
+
});
|
|
35
|
+
rows.push(row);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse JSON file
|
|
43
|
+
*/
|
|
44
|
+
export async function parseJSON(
|
|
45
|
+
file: File,
|
|
46
|
+
): Promise<Record<string, unknown>[]> {
|
|
47
|
+
const text = await file.text();
|
|
48
|
+
const data = JSON.parse(text);
|
|
49
|
+
return Array.isArray(data) ? data : [data];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse XLSX file (requires xlsx library)
|
|
54
|
+
*/
|
|
55
|
+
export async function parseXLSX(
|
|
56
|
+
file: File,
|
|
57
|
+
): Promise<Record<string, unknown>[]> {
|
|
58
|
+
// Dynamic import to avoid bundling xlsx if not needed
|
|
59
|
+
const XLSX = await import("xlsx");
|
|
60
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
61
|
+
const workbook = XLSX.read(arrayBuffer, { type: "array" });
|
|
62
|
+
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
63
|
+
return XLSX.utils.sheet_to_json(firstSheet);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse file based on format
|
|
68
|
+
*/
|
|
69
|
+
export async function parseFile(
|
|
70
|
+
file: File,
|
|
71
|
+
format: ImportOptions["format"],
|
|
72
|
+
): Promise<Record<string, unknown>[]> {
|
|
73
|
+
switch (format) {
|
|
74
|
+
case "csv":
|
|
75
|
+
return parseCSV(file);
|
|
76
|
+
case "json":
|
|
77
|
+
return parseJSON(file);
|
|
78
|
+
case "xlsx":
|
|
79
|
+
return parseXLSX(file);
|
|
80
|
+
default:
|
|
81
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate import data against field configs
|
|
87
|
+
*/
|
|
88
|
+
export function validateImportData(
|
|
89
|
+
data: Record<string, unknown>[],
|
|
90
|
+
config: EntityConfig,
|
|
91
|
+
options: Partial<ImportOptions> = {},
|
|
92
|
+
): ImportResult {
|
|
93
|
+
const errors: ImportResult["errors"] = [];
|
|
94
|
+
let imported = 0;
|
|
95
|
+
let failed = 0;
|
|
96
|
+
|
|
97
|
+
data.forEach((row, index) => {
|
|
98
|
+
const rowErrors: string[] = [];
|
|
99
|
+
|
|
100
|
+
config.fields.forEach((field) => {
|
|
101
|
+
if (field.hideInForm) return;
|
|
102
|
+
|
|
103
|
+
const value = getRowValue(row, { name: field.name, label: field.label });
|
|
104
|
+
|
|
105
|
+
// Required check
|
|
106
|
+
if (
|
|
107
|
+
field.required &&
|
|
108
|
+
(value === null || value === undefined || value === "")
|
|
109
|
+
) {
|
|
110
|
+
rowErrors.push(`${field.label} là bắt buộc`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// List validation (check if value is in options)
|
|
114
|
+
if (
|
|
115
|
+
value !== null &&
|
|
116
|
+
value !== undefined &&
|
|
117
|
+
value !== "" &&
|
|
118
|
+
field.options &&
|
|
119
|
+
field.options.length > 0
|
|
120
|
+
) {
|
|
121
|
+
const stringValue = String(value).toLowerCase();
|
|
122
|
+
const validOption = field.options.some((opt) => {
|
|
123
|
+
const isObject = typeof opt === "object" && opt !== null;
|
|
124
|
+
const optValue = isObject ? opt.value : opt;
|
|
125
|
+
return String(optValue).toLowerCase() === stringValue;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!validOption) {
|
|
129
|
+
// Collect valid labels for error message
|
|
130
|
+
const validLabels = field.options
|
|
131
|
+
.map((opt) => {
|
|
132
|
+
const isObject = typeof opt === "object" && opt !== null;
|
|
133
|
+
return isObject ? (opt as any).label : String(opt);
|
|
134
|
+
})
|
|
135
|
+
.join(", ");
|
|
136
|
+
rowErrors.push(
|
|
137
|
+
`${field.label} không hợp lệ. Chỉ chấp nhận: ${validLabels}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} else if (value !== null && value !== undefined && value !== "") {
|
|
141
|
+
// Type validation (only if not a list field or validated above)
|
|
142
|
+
switch (field.type) {
|
|
143
|
+
case "email":
|
|
144
|
+
if (
|
|
145
|
+
typeof value === "string" &&
|
|
146
|
+
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
147
|
+
) {
|
|
148
|
+
rowErrors.push(`${field.label} phải là email hợp lệ`);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
case "number":
|
|
152
|
+
case "integer":
|
|
153
|
+
if (isNaN(Number(value))) {
|
|
154
|
+
rowErrors.push(`${field.label} phải là số`);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// End validation
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (rowErrors.length > 0) {
|
|
163
|
+
failed++;
|
|
164
|
+
rowErrors.forEach((error) => {
|
|
165
|
+
errors.push({
|
|
166
|
+
row: index + 1,
|
|
167
|
+
field: "",
|
|
168
|
+
message: error,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
} else {
|
|
172
|
+
imported++;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: failed === 0 || options.skipErrors === true,
|
|
178
|
+
imported,
|
|
179
|
+
failed,
|
|
180
|
+
errors,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate CSV template from config
|
|
186
|
+
*/
|
|
187
|
+
export function generateCSVTemplate(config: EntityConfig): string {
|
|
188
|
+
const headers = config.fields
|
|
189
|
+
.filter((field) => !field.hideInForm || field.showInImport)
|
|
190
|
+
.map((field) => field.label);
|
|
191
|
+
|
|
192
|
+
return headers.join(",") + "\n";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate JSON template from config
|
|
197
|
+
*/
|
|
198
|
+
export function generateJSONTemplate(
|
|
199
|
+
config: EntityConfig,
|
|
200
|
+
): Record<string, unknown> {
|
|
201
|
+
const template: Record<string, unknown> = {};
|
|
202
|
+
|
|
203
|
+
config.fields
|
|
204
|
+
.filter((field) => !field.hideInForm || field.showInImport)
|
|
205
|
+
.forEach((field) => {
|
|
206
|
+
// For JSON, we still use technical names as keys because JSON is developer-centric
|
|
207
|
+
// and usually requires strict structure.
|
|
208
|
+
template[field.name] = field.defaultValue || "";
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return template;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Generate XLSX template (requires xlsx library)
|
|
216
|
+
*/
|
|
217
|
+
export async function generateXLSXTemplate(
|
|
218
|
+
config: EntityConfig,
|
|
219
|
+
): Promise<Blob> {
|
|
220
|
+
const XLSX = await import("xlsx");
|
|
221
|
+
|
|
222
|
+
const importHeaders = config.fields
|
|
223
|
+
.filter((field) => !field.hideInForm || field.showInImport)
|
|
224
|
+
.map((field) => field.label);
|
|
225
|
+
|
|
226
|
+
const workbook = XLSX.utils.book_new();
|
|
227
|
+
|
|
228
|
+
// Sheet 1: actual import template (machine headers)
|
|
229
|
+
const templateSheet = XLSX.utils.aoa_to_sheet([importHeaders]);
|
|
230
|
+
XLSX.utils.book_append_sheet(workbook, templateSheet, "Import");
|
|
231
|
+
|
|
232
|
+
// Sheet 2: guide for humans (doesn't affect parsing because parser reads first sheet)
|
|
233
|
+
const guideRows = [
|
|
234
|
+
["field", "label", "type", "required"],
|
|
235
|
+
...config.fields
|
|
236
|
+
.filter((field) => !field.hideInForm || field.showInImport)
|
|
237
|
+
.map((field) => [
|
|
238
|
+
field.name,
|
|
239
|
+
field.label,
|
|
240
|
+
field.type,
|
|
241
|
+
field.required ? "yes" : "no",
|
|
242
|
+
]),
|
|
243
|
+
];
|
|
244
|
+
const guideSheet = XLSX.utils.aoa_to_sheet(guideRows);
|
|
245
|
+
XLSX.utils.book_append_sheet(workbook, guideSheet, "Guide");
|
|
246
|
+
|
|
247
|
+
const buffer = XLSX.write(workbook, { type: "array", bookType: "xlsx" });
|
|
248
|
+
return new Blob([buffer], {
|
|
249
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Export data to CSV
|
|
255
|
+
*/
|
|
256
|
+
export function exportToCSV(
|
|
257
|
+
data: Record<string, unknown>[],
|
|
258
|
+
fields?: string[],
|
|
259
|
+
): string {
|
|
260
|
+
if (data.length === 0) return "";
|
|
261
|
+
|
|
262
|
+
const keys = fields || Object.keys(data[0]);
|
|
263
|
+
const headers = keys.join(",");
|
|
264
|
+
const rows = data.map((row) =>
|
|
265
|
+
keys.map((key) => String(row[key] || "")).join(","),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
return [headers, ...rows].join("\n");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Export data to JSON
|
|
273
|
+
*/
|
|
274
|
+
export function exportToJSON(
|
|
275
|
+
data: Record<string, unknown>[],
|
|
276
|
+
fields?: string[],
|
|
277
|
+
): string {
|
|
278
|
+
if (fields) {
|
|
279
|
+
const filtered = data.map((row) => {
|
|
280
|
+
const filteredRow: Record<string, unknown> = {};
|
|
281
|
+
fields.forEach((field) => {
|
|
282
|
+
filteredRow[field] = row[field];
|
|
283
|
+
});
|
|
284
|
+
return filteredRow;
|
|
285
|
+
});
|
|
286
|
+
return JSON.stringify(filtered, null, 2);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return JSON.stringify(data, null, 2);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Export data to XLSX (requires xlsx library)
|
|
294
|
+
*/
|
|
295
|
+
export async function exportToXLSX(
|
|
296
|
+
data: Record<string, unknown>[],
|
|
297
|
+
fields?: string[],
|
|
298
|
+
): Promise<Blob> {
|
|
299
|
+
const XLSX = await import("xlsx");
|
|
300
|
+
|
|
301
|
+
let exportData: Record<string, unknown>[];
|
|
302
|
+
if (fields) {
|
|
303
|
+
exportData = data.map((row) => {
|
|
304
|
+
const filteredRow: Record<string, unknown> = {};
|
|
305
|
+
fields.forEach((field) => {
|
|
306
|
+
const value = row[field];
|
|
307
|
+
filteredRow[field] = Array.isArray(value)
|
|
308
|
+
? value.join(", ")
|
|
309
|
+
: typeof value === "object" && value !== null
|
|
310
|
+
? JSON.stringify(value)
|
|
311
|
+
: value;
|
|
312
|
+
});
|
|
313
|
+
return filteredRow;
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
exportData = data.map((row) => {
|
|
317
|
+
const newRow: Record<string, unknown> = {};
|
|
318
|
+
Object.keys(row).forEach((key) => {
|
|
319
|
+
const value = row[key];
|
|
320
|
+
newRow[key] = Array.isArray(value)
|
|
321
|
+
? value.join(", ")
|
|
322
|
+
: typeof value === "object" && value !== null
|
|
323
|
+
? JSON.stringify(value)
|
|
324
|
+
: value;
|
|
325
|
+
});
|
|
326
|
+
return newRow;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
|
331
|
+
const workbook = XLSX.utils.book_new();
|
|
332
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, "Data");
|
|
333
|
+
|
|
334
|
+
const buffer = XLSX.write(workbook, { type: "array", bookType: "xlsx" });
|
|
335
|
+
return new Blob([buffer], {
|
|
336
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Download file
|
|
342
|
+
*/
|
|
343
|
+
export function downloadFile(blob: Blob, filename: string): void {
|
|
344
|
+
const url = URL.createObjectURL(blob);
|
|
345
|
+
const link = document.createElement("a");
|
|
346
|
+
link.href = url;
|
|
347
|
+
link.download = filename;
|
|
348
|
+
document.body.appendChild(link);
|
|
349
|
+
link.click();
|
|
350
|
+
document.body.removeChild(link);
|
|
351
|
+
URL.revokeObjectURL(url);
|
|
352
|
+
}
|