@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,244 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "../../../../ui/primitives/card";
|
|
11
|
+
import {
|
|
12
|
+
Tabs,
|
|
13
|
+
TabsContent,
|
|
14
|
+
TabsList,
|
|
15
|
+
TabsTrigger,
|
|
16
|
+
} from "../../../../ui/primitives/tabs";
|
|
17
|
+
import { Button } from "../../../../ui/primitives/button";
|
|
18
|
+
import { Input } from "../../../../ui/primitives/input";
|
|
19
|
+
import { Label } from "../../../../ui/primitives/label";
|
|
20
|
+
import { Switch } from "../../../../ui/primitives/switch";
|
|
21
|
+
import { toast } from "sonner";
|
|
22
|
+
import { Loader2 } from "lucide-react";
|
|
23
|
+
|
|
24
|
+
interface AuditSettings {
|
|
25
|
+
retentionDays: number;
|
|
26
|
+
enabledResources: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface NotificationSettings {
|
|
30
|
+
retentionDays: number;
|
|
31
|
+
channels: {
|
|
32
|
+
email: boolean;
|
|
33
|
+
inApp: boolean;
|
|
34
|
+
telegram: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function SystemSettings() {
|
|
39
|
+
const [loading, setLoading] = useState(true);
|
|
40
|
+
const [saving, setSaving] = useState(false);
|
|
41
|
+
const [auditSettings, setAuditSettings] = useState<AuditSettings | null>(
|
|
42
|
+
null,
|
|
43
|
+
);
|
|
44
|
+
const [notifySettings, setNotifySettings] =
|
|
45
|
+
useState<NotificationSettings | null>(null);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
fetch("/api/admin/system/settings")
|
|
49
|
+
.then((res) => res.json())
|
|
50
|
+
.then((data) => {
|
|
51
|
+
if (data.data) {
|
|
52
|
+
setAuditSettings(data.data.audit);
|
|
53
|
+
setNotifySettings(data.data.notification);
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.catch(() => toast.error("Failed to load settings"))
|
|
57
|
+
.finally(() => setLoading(false));
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const saveAuditSettings = async () => {
|
|
61
|
+
if (!auditSettings) return;
|
|
62
|
+
setSaving(true);
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch("/api/admin/system/settings", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify({ type: "audit", settings: auditSettings }),
|
|
67
|
+
});
|
|
68
|
+
if (res.ok) toast.success("Audit settings saved");
|
|
69
|
+
else throw new Error();
|
|
70
|
+
} catch {
|
|
71
|
+
toast.error("Failed to save audit settings");
|
|
72
|
+
} finally {
|
|
73
|
+
setSaving(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const saveNotifySettings = async () => {
|
|
78
|
+
if (!notifySettings) return;
|
|
79
|
+
setSaving(true);
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch("/api/admin/system/settings", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
type: "notification",
|
|
85
|
+
settings: notifySettings,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
if (res.ok) toast.success("Notification settings saved");
|
|
89
|
+
else throw new Error();
|
|
90
|
+
} catch {
|
|
91
|
+
toast.error("Failed to save notification settings");
|
|
92
|
+
} finally {
|
|
93
|
+
setSaving(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (loading)
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex justify-center p-8">
|
|
100
|
+
<Loader2 className="animate-spin h-8 w-8 text-muted-foreground" />
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Tabs defaultValue="audit" className="w-full">
|
|
106
|
+
<TabsList>
|
|
107
|
+
<TabsTrigger value="audit">Audit Logging</TabsTrigger>
|
|
108
|
+
<TabsTrigger value="notification">Notifications</TabsTrigger>
|
|
109
|
+
</TabsList>
|
|
110
|
+
|
|
111
|
+
<TabsContent value="audit" className="space-y-4">
|
|
112
|
+
<Card>
|
|
113
|
+
<CardHeader>
|
|
114
|
+
<CardTitle>Audit Log Configuration</CardTitle>
|
|
115
|
+
<CardDescription>
|
|
116
|
+
Control how long audit logs are kept and what events are recorded.
|
|
117
|
+
</CardDescription>
|
|
118
|
+
</CardHeader>
|
|
119
|
+
<CardContent className="space-y-4">
|
|
120
|
+
<div className="grid gap-2">
|
|
121
|
+
<Label htmlFor="retention">Retention Period (Days)</Label>
|
|
122
|
+
<Input
|
|
123
|
+
id="retention"
|
|
124
|
+
type="number"
|
|
125
|
+
value={auditSettings?.retentionDays || 90}
|
|
126
|
+
onChange={(e) =>
|
|
127
|
+
setAuditSettings((prev) =>
|
|
128
|
+
prev
|
|
129
|
+
? { ...prev, retentionDays: parseInt(e.target.value) }
|
|
130
|
+
: null,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="flex items-center space-x-2">
|
|
137
|
+
{/* Placeholder for resource selection - for now just informative */}
|
|
138
|
+
<span className="text-sm text-muted-foreground">
|
|
139
|
+
Currently logging all resources ('*'). Granular resource
|
|
140
|
+
selection coming soon.
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<Button onClick={saveAuditSettings} disabled={saving}>
|
|
145
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
146
|
+
Save Changes
|
|
147
|
+
</Button>
|
|
148
|
+
</CardContent>
|
|
149
|
+
</Card>
|
|
150
|
+
</TabsContent>
|
|
151
|
+
|
|
152
|
+
<TabsContent value="notification" className="space-y-4">
|
|
153
|
+
<Card>
|
|
154
|
+
<CardHeader>
|
|
155
|
+
<CardTitle>Notification System</CardTitle>
|
|
156
|
+
<CardDescription>
|
|
157
|
+
Configure notification channels and cleanup policies.
|
|
158
|
+
</CardDescription>
|
|
159
|
+
</CardHeader>
|
|
160
|
+
<CardContent className="space-y-4">
|
|
161
|
+
<div className="grid gap-2">
|
|
162
|
+
<Label htmlFor="notify-retention">Retention Period (Days)</Label>
|
|
163
|
+
<Input
|
|
164
|
+
id="notify-retention"
|
|
165
|
+
type="number"
|
|
166
|
+
value={notifySettings?.retentionDays || 30}
|
|
167
|
+
onChange={(e) =>
|
|
168
|
+
setNotifySettings((prev) =>
|
|
169
|
+
prev
|
|
170
|
+
? { ...prev, retentionDays: parseInt(e.target.value) }
|
|
171
|
+
: null,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="space-y-4 border rounded p-4">
|
|
178
|
+
<h4 className="font-medium text-sm">Active Channels</h4>
|
|
179
|
+
|
|
180
|
+
<div className="flex items-center justify-between">
|
|
181
|
+
<Label htmlFor="channel-inapp">In-App Notifications</Label>
|
|
182
|
+
<Switch
|
|
183
|
+
id="channel-inapp"
|
|
184
|
+
checked={notifySettings?.channels.inApp}
|
|
185
|
+
onCheckedChange={(checked) =>
|
|
186
|
+
setNotifySettings((prev) =>
|
|
187
|
+
prev
|
|
188
|
+
? {
|
|
189
|
+
...prev,
|
|
190
|
+
channels: { ...prev.channels, inApp: checked },
|
|
191
|
+
}
|
|
192
|
+
: null,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div className="flex items-center justify-between">
|
|
199
|
+
<Label htmlFor="channel-email">Email Notifications</Label>
|
|
200
|
+
<Switch
|
|
201
|
+
id="channel-email"
|
|
202
|
+
checked={notifySettings?.channels.email}
|
|
203
|
+
onCheckedChange={(checked) =>
|
|
204
|
+
setNotifySettings((prev) =>
|
|
205
|
+
prev
|
|
206
|
+
? {
|
|
207
|
+
...prev,
|
|
208
|
+
channels: { ...prev.channels, email: checked },
|
|
209
|
+
}
|
|
210
|
+
: null,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div className="flex items-center justify-between">
|
|
217
|
+
<Label htmlFor="channel-telegram">Telegram Notifications</Label>
|
|
218
|
+
<Switch
|
|
219
|
+
id="channel-telegram"
|
|
220
|
+
checked={notifySettings?.channels.telegram}
|
|
221
|
+
onCheckedChange={(checked) =>
|
|
222
|
+
setNotifySettings((prev) =>
|
|
223
|
+
prev
|
|
224
|
+
? {
|
|
225
|
+
...prev,
|
|
226
|
+
channels: { ...prev.channels, telegram: checked },
|
|
227
|
+
}
|
|
228
|
+
: null,
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<Button onClick={saveNotifySettings} disabled={saving}>
|
|
236
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
237
|
+
Save Changes
|
|
238
|
+
</Button>
|
|
239
|
+
</CardContent>
|
|
240
|
+
</Card>
|
|
241
|
+
</TabsContent>
|
|
242
|
+
</Tabs>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { CategoryManager } from "./components/categories/category-manager";
|
|
5
|
+
import type { SystemCategoryGroup } from "../types";
|
|
6
|
+
|
|
7
|
+
interface SystemCategoryPageProps {
|
|
8
|
+
initialGroups: SystemCategoryGroup[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SystemCategoryPage({
|
|
12
|
+
initialGroups,
|
|
13
|
+
}: SystemCategoryPageProps) {
|
|
14
|
+
return <CategoryManager initialGroups={initialGroups} />;
|
|
15
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Loader2, RefreshCw, Plus } from "lucide-react";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
import { cn } from "../../utils";
|
|
8
|
+
import { Button } from "../../ui";
|
|
9
|
+
|
|
10
|
+
import { SETTINGS_GROUPS } from "./components/settings/settings-groups";
|
|
11
|
+
import { SettingsSidebar } from "./components/settings/settings-sidebar";
|
|
12
|
+
import { SettingsSearch } from "./components/settings/settings-search";
|
|
13
|
+
import { SettingsSection } from "./components/settings/settings-section";
|
|
14
|
+
import { SettingField } from "./components/settings/setting-field";
|
|
15
|
+
import type { SettingConfig } from "../types";
|
|
16
|
+
import { SettingFormDialog } from "./components/settings/setting-form-dialog";
|
|
17
|
+
import {
|
|
18
|
+
DeleteConfirmDialog,
|
|
19
|
+
SuspendConfirmDialog,
|
|
20
|
+
} from "./components/settings/setting-dialogs";
|
|
21
|
+
|
|
22
|
+
interface SystemSettingsPageProps {
|
|
23
|
+
lang: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SettingsData {
|
|
27
|
+
items: SettingConfig[];
|
|
28
|
+
groupedItems: Record<string, SettingConfig[]>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function SystemSettingsPage({ lang }: SystemSettingsPageProps) {
|
|
32
|
+
const router = useRouter();
|
|
33
|
+
const [loading, setLoading] = React.useState(true);
|
|
34
|
+
const [settings, setSettings] = React.useState<SettingsData | null>(null);
|
|
35
|
+
const [activeGroup, setActiveGroup] = React.useState(
|
|
36
|
+
SETTINGS_GROUPS[0]?.id || "general",
|
|
37
|
+
);
|
|
38
|
+
const [searchQuery, setSearchQuery] = React.useState("");
|
|
39
|
+
const [highlightedKey, setHighlightedKey] = React.useState<string | null>(
|
|
40
|
+
null,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Dialog states
|
|
44
|
+
const [formDialogOpen, setFormDialogOpen] = React.useState(false);
|
|
45
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
|
46
|
+
const [suspendDialogOpen, setSuspendDialogOpen] = React.useState(false);
|
|
47
|
+
const [selectedSetting, setSelectedSetting] =
|
|
48
|
+
React.useState<SettingConfig | null>(null);
|
|
49
|
+
|
|
50
|
+
// Fetch settings
|
|
51
|
+
const fetchSettings = React.useCallback(async () => {
|
|
52
|
+
setLoading(true);
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch("/api/admin/system/settings/all");
|
|
55
|
+
if (!res.ok) throw new Error("Failed to fetch settings");
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
|
|
58
|
+
// Group items by category
|
|
59
|
+
const items: SettingConfig[] = data.data || [];
|
|
60
|
+
const groupedItems: Record<string, SettingConfig[]> = {};
|
|
61
|
+
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const category = item.category || "general";
|
|
64
|
+
if (!groupedItems[category]) {
|
|
65
|
+
groupedItems[category] = [];
|
|
66
|
+
}
|
|
67
|
+
groupedItems[category].push(item);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setSettings({ items, groupedItems });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Error fetching settings:", error);
|
|
73
|
+
toast.error("Không thể tải cấu hình hệ thống");
|
|
74
|
+
} finally {
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
fetchSettings();
|
|
81
|
+
}, [fetchSettings]);
|
|
82
|
+
|
|
83
|
+
// Handle setting update
|
|
84
|
+
const handleUpdate = React.useCallback(async (key: string, value: string) => {
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch("/api/admin/system/settings/update", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ key, value }),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!res.ok) throw new Error("Failed to update setting");
|
|
93
|
+
|
|
94
|
+
// Update local state
|
|
95
|
+
setSettings((prev) => {
|
|
96
|
+
if (!prev) return prev;
|
|
97
|
+
const items = prev.items.map((item) =>
|
|
98
|
+
item.key === key ? { ...item, value } : item,
|
|
99
|
+
);
|
|
100
|
+
const groupedItems: Record<string, SettingConfig[]> = {};
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
const category = item.category || "general";
|
|
103
|
+
if (!groupedItems[category]) {
|
|
104
|
+
groupedItems[category] = [];
|
|
105
|
+
}
|
|
106
|
+
groupedItems[category].push(item);
|
|
107
|
+
}
|
|
108
|
+
return { items, groupedItems };
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error("Error updating setting:", error);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
// Action handlers
|
|
117
|
+
const handleDelete = React.useCallback((setting: SettingConfig) => {
|
|
118
|
+
setSelectedSetting(setting);
|
|
119
|
+
setDeleteDialogOpen(true);
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
const handleToggleStatus = React.useCallback((setting: SettingConfig) => {
|
|
123
|
+
setSelectedSetting(setting);
|
|
124
|
+
setSuspendDialogOpen(true);
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
const handleAddNew = React.useCallback(() => {
|
|
128
|
+
setSelectedSetting(null);
|
|
129
|
+
setFormDialogOpen(true);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const handleDialogSuccess = React.useCallback(() => {
|
|
133
|
+
fetchSettings();
|
|
134
|
+
}, [fetchSettings]);
|
|
135
|
+
|
|
136
|
+
// Filter settings based on search query and active group
|
|
137
|
+
const filteredSettings = React.useMemo(() => {
|
|
138
|
+
if (!settings) return [];
|
|
139
|
+
|
|
140
|
+
let items = settings.items;
|
|
141
|
+
|
|
142
|
+
// Filter by active group
|
|
143
|
+
const group = SETTINGS_GROUPS.find((g) => g.id === activeGroup);
|
|
144
|
+
if (group) {
|
|
145
|
+
items = items.filter((item) => {
|
|
146
|
+
// Special case: phu_quy_* settings should be included in "integrations" group
|
|
147
|
+
const effectiveCategory = item.key.startsWith("phu_quy_") ? "phu_quy" : item.category;
|
|
148
|
+
return group.categories.includes(item.category) || group.categories.includes(effectiveCategory);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Filter by search query
|
|
153
|
+
if (searchQuery.trim()) {
|
|
154
|
+
const query = searchQuery.toLowerCase();
|
|
155
|
+
items = items.filter(
|
|
156
|
+
(item) =>
|
|
157
|
+
item.key.toLowerCase().includes(query) ||
|
|
158
|
+
item.description?.toLowerCase().includes(query) ||
|
|
159
|
+
item.category.toLowerCase().includes(query),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return items;
|
|
164
|
+
}, [settings, activeGroup, searchQuery]);
|
|
165
|
+
|
|
166
|
+
// Group filtered settings by category
|
|
167
|
+
// Special handling: Group settings with key starting with "phu_quy_" into "phu_quy" category
|
|
168
|
+
const groupedFilteredSettings = React.useMemo(() => {
|
|
169
|
+
const grouped: Record<string, SettingConfig[]> = {};
|
|
170
|
+
for (const item of filteredSettings) {
|
|
171
|
+
// Special case: Group phu_quy_* settings together
|
|
172
|
+
let category = item.category || "general";
|
|
173
|
+
if (item.key.startsWith("phu_quy_")) {
|
|
174
|
+
category = "phu_quy";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!grouped[category]) {
|
|
178
|
+
grouped[category] = [];
|
|
179
|
+
}
|
|
180
|
+
grouped[category].push(item);
|
|
181
|
+
}
|
|
182
|
+
return grouped;
|
|
183
|
+
}, [filteredSettings]);
|
|
184
|
+
|
|
185
|
+
// Handle search keyboard navigation
|
|
186
|
+
const handleSearchKeyDown = React.useCallback(
|
|
187
|
+
(e: React.KeyboardEvent) => {
|
|
188
|
+
if (e.key === "Enter" && filteredSettings.length > 0) {
|
|
189
|
+
const firstItem = filteredSettings[0];
|
|
190
|
+
setHighlightedKey(firstItem.key);
|
|
191
|
+
document.getElementById(`setting-${firstItem.key}`)?.scrollIntoView({
|
|
192
|
+
behavior: "smooth",
|
|
193
|
+
block: "center",
|
|
194
|
+
});
|
|
195
|
+
setTimeout(() => setHighlightedKey(null), 3000);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
[filteredSettings],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (loading) {
|
|
202
|
+
return (
|
|
203
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
204
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="h-screen flex flex-col overflow-hidden bg-background">
|
|
211
|
+
{/* Header */}
|
|
212
|
+
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 shrink-0">
|
|
213
|
+
<div className="container flex flex-col md:flex-row items-start md:items-center justify-between py-4 md:h-16 px-4 gap-4 md:gap-0">
|
|
214
|
+
<div>
|
|
215
|
+
<h1 className="text-xl font-semibold">Cài đặt hệ thống</h1>
|
|
216
|
+
<p className="text-sm text-muted-foreground">
|
|
217
|
+
Quản lý cấu hình và tùy chỉnh hệ thống
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="flex items-center gap-2 self-end md:self-auto">
|
|
221
|
+
<Button
|
|
222
|
+
variant="outline"
|
|
223
|
+
size="sm"
|
|
224
|
+
onClick={fetchSettings}
|
|
225
|
+
disabled={loading}
|
|
226
|
+
className="hidden sm:flex"
|
|
227
|
+
>
|
|
228
|
+
<RefreshCw
|
|
229
|
+
className={cn("h-4 w-4 mr-2", loading && "animate-spin")}
|
|
230
|
+
/>
|
|
231
|
+
Làm mới
|
|
232
|
+
</Button>
|
|
233
|
+
<Button
|
|
234
|
+
variant="outline"
|
|
235
|
+
size="icon"
|
|
236
|
+
onClick={fetchSettings}
|
|
237
|
+
disabled={loading}
|
|
238
|
+
className="sm:hidden"
|
|
239
|
+
>
|
|
240
|
+
<RefreshCw
|
|
241
|
+
className={cn("h-4 w-4", loading && "animate-spin")}
|
|
242
|
+
/>
|
|
243
|
+
</Button>
|
|
244
|
+
<Button size="sm" onClick={handleAddNew}>
|
|
245
|
+
<Plus className="h-4 w-4 md:mr-2" />
|
|
246
|
+
<span className="hidden md:inline">Thêm cấu hình</span>
|
|
247
|
+
</Button>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* Main Content */}
|
|
253
|
+
<div className="flex-1 min-h-0 container px-2 md:px-4 py-4 md:py-6 flex flex-col">
|
|
254
|
+
<div className="flex flex-col md:flex-row gap-4 md:gap-8 h-full min-h-0">
|
|
255
|
+
{/* Sidebar */}
|
|
256
|
+
<SettingsSidebar
|
|
257
|
+
groups={SETTINGS_GROUPS}
|
|
258
|
+
activeGroup={activeGroup}
|
|
259
|
+
onGroupChange={setActiveGroup}
|
|
260
|
+
className="w-full md:w-64 shrink-0 md:overflow-y-auto"
|
|
261
|
+
/>
|
|
262
|
+
|
|
263
|
+
{/* Content */}
|
|
264
|
+
<div className="flex-1 min-w-0 flex flex-col h-full min-h-0">
|
|
265
|
+
{/* Search */}
|
|
266
|
+
<SettingsSearch
|
|
267
|
+
value={searchQuery}
|
|
268
|
+
onChange={setSearchQuery}
|
|
269
|
+
onKeyDown={handleSearchKeyDown}
|
|
270
|
+
resultCount={filteredSettings.length}
|
|
271
|
+
className="mb-6 shrink-0"
|
|
272
|
+
/>
|
|
273
|
+
|
|
274
|
+
{/* Settings Groups */}
|
|
275
|
+
<div className="flex-1 overflow-y-auto pr-2 space-y-8">
|
|
276
|
+
{Object.entries(groupedFilteredSettings).length === 0 ? (
|
|
277
|
+
<div className="text-center py-12">
|
|
278
|
+
<p className="text-muted-foreground">
|
|
279
|
+
{searchQuery
|
|
280
|
+
? "Không tìm thấy cấu hình phù hợp"
|
|
281
|
+
: "Chưa có cấu hình nào trong nhóm này"}
|
|
282
|
+
</p>
|
|
283
|
+
<Button
|
|
284
|
+
variant="outline"
|
|
285
|
+
size="sm"
|
|
286
|
+
className="mt-4"
|
|
287
|
+
onClick={handleAddNew}
|
|
288
|
+
>
|
|
289
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
290
|
+
Thêm cấu hình mới
|
|
291
|
+
</Button>
|
|
292
|
+
</div>
|
|
293
|
+
) : (
|
|
294
|
+
Object.entries(groupedFilteredSettings).map(
|
|
295
|
+
([category, items]) => (
|
|
296
|
+
<SettingsSection
|
|
297
|
+
key={category}
|
|
298
|
+
id={category}
|
|
299
|
+
title={formatCategoryName(category)}
|
|
300
|
+
count={items.length}
|
|
301
|
+
>
|
|
302
|
+
{items.map((item) => (
|
|
303
|
+
<SettingField
|
|
304
|
+
key={item.id}
|
|
305
|
+
setting={item}
|
|
306
|
+
onUpdate={handleUpdate}
|
|
307
|
+
onDelete={handleDelete}
|
|
308
|
+
onToggleStatus={handleToggleStatus}
|
|
309
|
+
isHighlighted={highlightedKey === item.key}
|
|
310
|
+
/>
|
|
311
|
+
))}
|
|
312
|
+
</SettingsSection>
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* Dialogs */}
|
|
322
|
+
<SettingFormDialog
|
|
323
|
+
open={formDialogOpen}
|
|
324
|
+
onOpenChange={setFormDialogOpen}
|
|
325
|
+
setting={selectedSetting}
|
|
326
|
+
onSuccess={handleDialogSuccess}
|
|
327
|
+
/>
|
|
328
|
+
<DeleteConfirmDialog
|
|
329
|
+
open={deleteDialogOpen}
|
|
330
|
+
onOpenChange={setDeleteDialogOpen}
|
|
331
|
+
setting={selectedSetting}
|
|
332
|
+
onSuccess={handleDialogSuccess}
|
|
333
|
+
/>
|
|
334
|
+
<SuspendConfirmDialog
|
|
335
|
+
open={suspendDialogOpen}
|
|
336
|
+
onOpenChange={setSuspendDialogOpen}
|
|
337
|
+
setting={selectedSetting}
|
|
338
|
+
onSuccess={handleDialogSuccess}
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Helper function to format category names
|
|
345
|
+
function formatCategoryName(category: string): string {
|
|
346
|
+
const names: Record<string, string> = {
|
|
347
|
+
general: "Cấu hình chung",
|
|
348
|
+
app: "Ứng dụng",
|
|
349
|
+
system: "Hệ thống",
|
|
350
|
+
pricing: "Giá cả",
|
|
351
|
+
sales: "Bán hàng",
|
|
352
|
+
purchase: "Mua hàng",
|
|
353
|
+
inventory: "Kho hàng",
|
|
354
|
+
zalo: "Zalo",
|
|
355
|
+
telegram: "Telegram",
|
|
356
|
+
email: "Email",
|
|
357
|
+
sms: "SMS",
|
|
358
|
+
webhook: "Webhook",
|
|
359
|
+
notifications: "Thông báo",
|
|
360
|
+
alerts: "Cảnh báo",
|
|
361
|
+
security: "Bảo mật",
|
|
362
|
+
auth: "Xác thực",
|
|
363
|
+
audit: "Nhật ký",
|
|
364
|
+
automation: "Tự động hóa",
|
|
365
|
+
cron: "Lịch trình",
|
|
366
|
+
jobs: "Công việc",
|
|
367
|
+
scheduler: "Lập lịch",
|
|
368
|
+
theme: "Giao diện",
|
|
369
|
+
ui: "Hiển thị",
|
|
370
|
+
display: "Màn hình",
|
|
371
|
+
debug: "Debug",
|
|
372
|
+
cache: "Cache",
|
|
373
|
+
performance: "Hiệu năng",
|
|
374
|
+
logs: "Logs",
|
|
375
|
+
phu_quy: "Phú Quý",
|
|
376
|
+
};
|
|
377
|
+
return (
|
|
378
|
+
names[category] || category.charAt(0).toUpperCase() + category.slice(1)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { STATUS_ACTIVE, STATUS_INACTIVE, STATUS_VALUES } from "../constants";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* System Category Group Validation Schema
|
|
6
|
+
*/
|
|
7
|
+
export const systemCategoryGroupSchema = z.object({
|
|
8
|
+
code: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1, "Mã nhóm danh mục là bắt buộc")
|
|
11
|
+
.max(50, "Mã nhóm danh mục tối đa 50 ký tự")
|
|
12
|
+
.trim(),
|
|
13
|
+
|
|
14
|
+
name: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1, "Tên nhóm danh mục là bắt buộc")
|
|
17
|
+
.max(200, "Tên nhóm danh mục tối đa 200 ký tự")
|
|
18
|
+
.trim(),
|
|
19
|
+
|
|
20
|
+
description: z
|
|
21
|
+
.string()
|
|
22
|
+
.max(1000, "Mô tả tối đa 1000 ký tự")
|
|
23
|
+
.optional()
|
|
24
|
+
.or(z.literal(""))
|
|
25
|
+
.transform((val) => val || ""),
|
|
26
|
+
|
|
27
|
+
status: z.preprocess((val) => {
|
|
28
|
+
if (typeof val === "boolean") {
|
|
29
|
+
return val ? STATUS_ACTIVE : STATUS_INACTIVE;
|
|
30
|
+
}
|
|
31
|
+
if (val === "Active" || val === "Inactive") {
|
|
32
|
+
return val === "Active" ? STATUS_ACTIVE : STATUS_INACTIVE;
|
|
33
|
+
}
|
|
34
|
+
return val;
|
|
35
|
+
}, z.enum(STATUS_VALUES).default(STATUS_ACTIVE)),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type SystemCategoryGroupInput = z.infer<
|
|
39
|
+
typeof systemCategoryGroupSchema
|
|
40
|
+
>;
|
|
41
|
+
export const systemCategoryGroupUpdateSchema =
|
|
42
|
+
systemCategoryGroupSchema.partial();
|
|
43
|
+
export const systemCategoryGroupBulkSchema = z.object({
|
|
44
|
+
ids: z.array(z.string()).min(1, "Phải chọn ít nhất 1 nhóm danh mục"),
|
|
45
|
+
action: z.enum(["delete", "activate", "deactivate"]),
|
|
46
|
+
});
|