@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,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field Calculator - Handles field calculations based on dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FieldConfig } from "../../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Safe expression evaluator - replaces eval() to prevent code injection
|
|
9
|
+
* Supports: +, -, *, /, (), numbers
|
|
10
|
+
* Uses a simple recursive descent parser - NO eval() or Function()
|
|
11
|
+
*/
|
|
12
|
+
function safeEvaluate(expression: string): number {
|
|
13
|
+
// Remove all whitespace
|
|
14
|
+
expression = expression.replace(/\s+/g, "");
|
|
15
|
+
|
|
16
|
+
// Validate expression only contains safe characters
|
|
17
|
+
// Allowed: numbers, operators (+, -, *, /), parentheses, and decimal points
|
|
18
|
+
if (!/^[0-9+\-*/().]+$/.test(expression)) {
|
|
19
|
+
throw new Error("Invalid characters in expression");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let index = 0;
|
|
23
|
+
|
|
24
|
+
// Parse a number
|
|
25
|
+
function parseNumber(): number {
|
|
26
|
+
let numStr = "";
|
|
27
|
+
while (index < expression.length && /[0-9.]/.test(expression[index])) {
|
|
28
|
+
numStr += expression[index];
|
|
29
|
+
index++;
|
|
30
|
+
}
|
|
31
|
+
const num = parseFloat(numStr);
|
|
32
|
+
if (isNaN(num)) {
|
|
33
|
+
throw new Error(`Invalid number: ${numStr}`);
|
|
34
|
+
}
|
|
35
|
+
return num;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse expression with addition and subtraction
|
|
39
|
+
function parseExpression(): number {
|
|
40
|
+
let result = parseTerm();
|
|
41
|
+
while (index < expression.length) {
|
|
42
|
+
const op = expression[index];
|
|
43
|
+
if (op === "+") {
|
|
44
|
+
index++;
|
|
45
|
+
result += parseTerm();
|
|
46
|
+
} else if (op === "-") {
|
|
47
|
+
index++;
|
|
48
|
+
result -= parseTerm();
|
|
49
|
+
} else {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse term with multiplication and division
|
|
57
|
+
function parseTerm(): number {
|
|
58
|
+
let result = parseFactor();
|
|
59
|
+
while (index < expression.length) {
|
|
60
|
+
const op = expression[index];
|
|
61
|
+
if (op === "*") {
|
|
62
|
+
index++;
|
|
63
|
+
result *= parseFactor();
|
|
64
|
+
} else if (op === "/") {
|
|
65
|
+
index++;
|
|
66
|
+
const divisor = parseFactor();
|
|
67
|
+
if (divisor === 0) {
|
|
68
|
+
throw new Error("Division by zero");
|
|
69
|
+
}
|
|
70
|
+
result /= divisor;
|
|
71
|
+
} else {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Parse factor (number or parenthesized expression)
|
|
79
|
+
function parseFactor(): number {
|
|
80
|
+
if (index >= expression.length) {
|
|
81
|
+
throw new Error("Unexpected end of expression");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (expression[index] === "(") {
|
|
85
|
+
index++; // skip '('
|
|
86
|
+
const result = parseExpression();
|
|
87
|
+
if (index >= expression.length || expression[index] !== ")") {
|
|
88
|
+
throw new Error("Missing closing parenthesis");
|
|
89
|
+
}
|
|
90
|
+
index++; // skip ')'
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (expression[index] === "-") {
|
|
95
|
+
index++; // unary minus
|
|
96
|
+
return -parseFactor();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (expression[index] === "+") {
|
|
100
|
+
index++; // unary plus
|
|
101
|
+
return parseFactor();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parseNumber();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = parseExpression();
|
|
109
|
+
if (index < expression.length) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Unexpected character: ${expression[index]} at position ${index}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Invalid expression: ${error instanceof Error ? error.message : String(error)}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Sanitize and validate field names
|
|
124
|
+
*/
|
|
125
|
+
function isValidFieldName(name: string): boolean {
|
|
126
|
+
// Only allow alphanumeric and underscore, must start with letter or underscore
|
|
127
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Evaluate a formula string or function
|
|
132
|
+
* SECURITY: No longer uses eval() - uses safe evaluator instead
|
|
133
|
+
*/
|
|
134
|
+
export function evaluateFormula(
|
|
135
|
+
formula: string | ((values: Record<string, unknown>) => unknown),
|
|
136
|
+
values: Record<string, unknown>,
|
|
137
|
+
): unknown {
|
|
138
|
+
if (typeof formula === "function") {
|
|
139
|
+
return formula(values);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Simple formula parser for common operations
|
|
143
|
+
// Supports: +, -, *, /, ()
|
|
144
|
+
try {
|
|
145
|
+
// Replace field names with their values
|
|
146
|
+
let expression = formula.trim();
|
|
147
|
+
const fieldNames = Object.keys(values);
|
|
148
|
+
|
|
149
|
+
// Validate all field names first
|
|
150
|
+
fieldNames.forEach((fieldName) => {
|
|
151
|
+
if (!isValidFieldName(fieldName)) {
|
|
152
|
+
throw new Error(`Invalid field name: ${fieldName}`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Replace field names with their numeric values
|
|
157
|
+
fieldNames.forEach((fieldName) => {
|
|
158
|
+
const value = values[fieldName];
|
|
159
|
+
const numValue = Number(value);
|
|
160
|
+
if (!isNaN(numValue) && isFinite(numValue)) {
|
|
161
|
+
// Replace field name with its numeric value
|
|
162
|
+
// Use word boundary to avoid partial matches
|
|
163
|
+
expression = expression.replace(
|
|
164
|
+
new RegExp(`\\b${fieldName}\\b`, "g"),
|
|
165
|
+
String(numValue),
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
// If field value is not a valid number, throw error
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Field "${fieldName}" must be a valid number for calculation`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Validate expression after replacement
|
|
176
|
+
// Check that all field names have been replaced
|
|
177
|
+
const remainingFieldNames = fieldNames.filter((fieldName) => {
|
|
178
|
+
return new RegExp(`\\b${fieldName}\\b`).test(expression);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (remainingFieldNames.length > 0) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Fields not found in values: ${remainingFieldNames.join(", ")}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Evaluate the expression safely (no eval!)
|
|
188
|
+
return safeEvaluate(expression);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error("Error evaluating formula:", error);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Calculate field value based on dependencies
|
|
197
|
+
*/
|
|
198
|
+
export function calculateFieldValue(
|
|
199
|
+
field: FieldConfig,
|
|
200
|
+
formValues: Record<string, unknown>,
|
|
201
|
+
): unknown {
|
|
202
|
+
if (!field.calculate) return undefined;
|
|
203
|
+
|
|
204
|
+
const { dependsOn, formula } = field.calculate;
|
|
205
|
+
|
|
206
|
+
// Get values of dependent fields
|
|
207
|
+
const dependentValues: Record<string, unknown> = {};
|
|
208
|
+
dependsOn.forEach((depField) => {
|
|
209
|
+
dependentValues[depField] = formValues[depField];
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Check if all required fields have values
|
|
213
|
+
const hasAllValues = dependsOn.every(
|
|
214
|
+
(depField) =>
|
|
215
|
+
formValues[depField] !== undefined &&
|
|
216
|
+
formValues[depField] !== null &&
|
|
217
|
+
formValues[depField] !== "",
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (!hasAllValues) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Evaluate formula
|
|
225
|
+
return evaluateFormula(formula, dependentValues);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get all fields that depend on a given field
|
|
230
|
+
*/
|
|
231
|
+
export function getDependentFields(
|
|
232
|
+
fieldName: string,
|
|
233
|
+
allFields: FieldConfig[],
|
|
234
|
+
): FieldConfig[] {
|
|
235
|
+
return allFields.filter(
|
|
236
|
+
(field) =>
|
|
237
|
+
field.calculate?.dependsOn?.includes(fieldName) ||
|
|
238
|
+
field.dependencies?.fields?.includes(fieldName) ||
|
|
239
|
+
field.cascade?.triggerField === fieldName,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field Formatter - Handles field value formatting for display
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FieldConfig } from "../../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format a value based on field configuration
|
|
9
|
+
*/
|
|
10
|
+
export function formatFieldValue(value: unknown, field: FieldConfig): string {
|
|
11
|
+
if (value === undefined || value === null || value === "") {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Apply output transformation if exists
|
|
16
|
+
if (field.transform?.output) {
|
|
17
|
+
value = field.transform.output(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Apply formatting if exists
|
|
21
|
+
// Priority: Explicit format config
|
|
22
|
+
if (field.format) {
|
|
23
|
+
const { type, options = {} } = field.format;
|
|
24
|
+
|
|
25
|
+
switch (type) {
|
|
26
|
+
case "currency":
|
|
27
|
+
return formatCurrency(value, options);
|
|
28
|
+
|
|
29
|
+
case "percentage":
|
|
30
|
+
return formatPercentage(value, options);
|
|
31
|
+
|
|
32
|
+
case "number":
|
|
33
|
+
return formatNumber(value, options);
|
|
34
|
+
|
|
35
|
+
case "date":
|
|
36
|
+
return formatDate(value, options);
|
|
37
|
+
|
|
38
|
+
case "datetime":
|
|
39
|
+
return formatDateTime(value, options);
|
|
40
|
+
|
|
41
|
+
case "custom":
|
|
42
|
+
if (options.custom) {
|
|
43
|
+
return options.custom(value);
|
|
44
|
+
}
|
|
45
|
+
return String(value);
|
|
46
|
+
|
|
47
|
+
default:
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: Default formatting based on field type
|
|
53
|
+
switch (field.type) {
|
|
54
|
+
case "integer":
|
|
55
|
+
return formatNumber(value, { useGrouping: true, decimals: 0 });
|
|
56
|
+
|
|
57
|
+
case "number":
|
|
58
|
+
return formatNumber(value, { useGrouping: true });
|
|
59
|
+
|
|
60
|
+
case "date":
|
|
61
|
+
return formatDate(value, {});
|
|
62
|
+
|
|
63
|
+
case "datetime":
|
|
64
|
+
return formatDateTime(value, {});
|
|
65
|
+
|
|
66
|
+
case "boolean":
|
|
67
|
+
return value ? "Yes" : "No";
|
|
68
|
+
|
|
69
|
+
case "select":
|
|
70
|
+
case "multiselect":
|
|
71
|
+
if (field.options && Array.isArray(field.options)) {
|
|
72
|
+
// Handle multiselect
|
|
73
|
+
if (field.type === "multiselect" && Array.isArray(value)) {
|
|
74
|
+
return value
|
|
75
|
+
.map((val) => {
|
|
76
|
+
const option = field.options?.find((opt) => {
|
|
77
|
+
const optValue = typeof opt === "object" ? opt.value : opt;
|
|
78
|
+
return String(optValue) === String(val);
|
|
79
|
+
});
|
|
80
|
+
return option
|
|
81
|
+
? typeof option === "object"
|
|
82
|
+
? option.label
|
|
83
|
+
: String(option)
|
|
84
|
+
: String(val);
|
|
85
|
+
})
|
|
86
|
+
.join(", ");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle single select
|
|
90
|
+
const option = field.options.find((opt) => {
|
|
91
|
+
const optValue = typeof opt === "object" ? opt.value : opt;
|
|
92
|
+
const valToCheck =
|
|
93
|
+
typeof value === "object" && value !== null
|
|
94
|
+
? (value as Record<string, unknown>).value || value
|
|
95
|
+
: value;
|
|
96
|
+
return String(optValue) === String(valToCheck);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (option) {
|
|
100
|
+
return typeof option === "object" ? option.label : String(option);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return String(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return String(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format as currency
|
|
111
|
+
*/
|
|
112
|
+
function formatCurrency(
|
|
113
|
+
value: unknown,
|
|
114
|
+
options: {
|
|
115
|
+
currency?: string;
|
|
116
|
+
locale?: string;
|
|
117
|
+
decimals?: number;
|
|
118
|
+
},
|
|
119
|
+
): string {
|
|
120
|
+
const numValue = Number(value);
|
|
121
|
+
if (isNaN(numValue)) return String(value);
|
|
122
|
+
|
|
123
|
+
const { currency = "USD", locale = "en-US", decimals = 2 } = options;
|
|
124
|
+
|
|
125
|
+
return new Intl.NumberFormat(locale, {
|
|
126
|
+
style: "currency",
|
|
127
|
+
currency,
|
|
128
|
+
minimumFractionDigits: decimals,
|
|
129
|
+
maximumFractionDigits: decimals,
|
|
130
|
+
}).format(numValue);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format as percentage
|
|
135
|
+
*/
|
|
136
|
+
function formatPercentage(
|
|
137
|
+
value: unknown,
|
|
138
|
+
options: {
|
|
139
|
+
decimals?: number;
|
|
140
|
+
locale?: string;
|
|
141
|
+
},
|
|
142
|
+
): string {
|
|
143
|
+
const numValue = Number(value);
|
|
144
|
+
if (isNaN(numValue)) return String(value);
|
|
145
|
+
|
|
146
|
+
const { decimals = 2, locale = "en-US" } = options;
|
|
147
|
+
|
|
148
|
+
return new Intl.NumberFormat(locale, {
|
|
149
|
+
style: "percent",
|
|
150
|
+
minimumFractionDigits: decimals,
|
|
151
|
+
maximumFractionDigits: decimals,
|
|
152
|
+
}).format(numValue / 100);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Format as number
|
|
157
|
+
*/
|
|
158
|
+
function formatNumber(
|
|
159
|
+
value: unknown,
|
|
160
|
+
options: {
|
|
161
|
+
decimals?: number;
|
|
162
|
+
locale?: string;
|
|
163
|
+
useGrouping?: boolean;
|
|
164
|
+
},
|
|
165
|
+
): string {
|
|
166
|
+
const numValue = Number(value);
|
|
167
|
+
if (isNaN(numValue)) return String(value);
|
|
168
|
+
|
|
169
|
+
const { decimals = 2, locale = "en-US", useGrouping = true } = options;
|
|
170
|
+
|
|
171
|
+
return new Intl.NumberFormat(locale, {
|
|
172
|
+
minimumFractionDigits: decimals,
|
|
173
|
+
maximumFractionDigits: decimals,
|
|
174
|
+
useGrouping,
|
|
175
|
+
}).format(numValue);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Format as date (Asia/Ho_Chi_Minh timezone)
|
|
180
|
+
*/
|
|
181
|
+
function formatDate(
|
|
182
|
+
value: unknown,
|
|
183
|
+
options: {
|
|
184
|
+
dateFormat?: string;
|
|
185
|
+
locale?: string;
|
|
186
|
+
},
|
|
187
|
+
): string {
|
|
188
|
+
if (!value) return "";
|
|
189
|
+
|
|
190
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
191
|
+
if (isNaN(date.getTime())) return String(value);
|
|
192
|
+
|
|
193
|
+
const { dateFormat, locale = "en-US" } = options;
|
|
194
|
+
|
|
195
|
+
if (dateFormat) {
|
|
196
|
+
// Custom format using date-fns or similar
|
|
197
|
+
// For now, use Intl.DateTimeFormat
|
|
198
|
+
return new Intl.DateTimeFormat(locale, {
|
|
199
|
+
year: "numeric",
|
|
200
|
+
month: "2-digit",
|
|
201
|
+
day: "2-digit",
|
|
202
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
203
|
+
}).format(date);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return new Intl.DateTimeFormat(locale, {
|
|
207
|
+
year: "numeric",
|
|
208
|
+
month: "2-digit",
|
|
209
|
+
day: "2-digit",
|
|
210
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
211
|
+
}).format(date);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Format as datetime (Asia/Ho_Chi_Minh timezone)
|
|
216
|
+
*/
|
|
217
|
+
function formatDateTime(
|
|
218
|
+
value: unknown,
|
|
219
|
+
options: {
|
|
220
|
+
dateFormat?: string;
|
|
221
|
+
locale?: string;
|
|
222
|
+
},
|
|
223
|
+
): string {
|
|
224
|
+
if (!value) return "";
|
|
225
|
+
|
|
226
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
227
|
+
if (isNaN(date.getTime())) return String(value);
|
|
228
|
+
|
|
229
|
+
const { dateFormat, locale = "en-US" } = options;
|
|
230
|
+
|
|
231
|
+
return new Intl.DateTimeFormat(locale, {
|
|
232
|
+
year: "numeric",
|
|
233
|
+
month: "2-digit",
|
|
234
|
+
day: "2-digit",
|
|
235
|
+
hour: "2-digit",
|
|
236
|
+
minute: "2-digit",
|
|
237
|
+
second: "2-digit",
|
|
238
|
+
timeZone: "Asia/Ho_Chi_Minh",
|
|
239
|
+
}).format(date);
|
|
240
|
+
}
|