@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,271 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { useForm } from "react-hook-form";
|
|
4
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
|
|
7
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
8
|
+
import { X } from "lucide-react";
|
|
9
|
+
import { cn } from "../utils";
|
|
10
|
+
import type { QuickAccessItem } from "./types";
|
|
11
|
+
import { iconMap } from "./constants";
|
|
12
|
+
|
|
13
|
+
const AVAILABLE_ICONS = Object.keys(iconMap);
|
|
14
|
+
const AVAILABLE_COLORS = [
|
|
15
|
+
"blue",
|
|
16
|
+
"green",
|
|
17
|
+
"purple",
|
|
18
|
+
"orange",
|
|
19
|
+
"pink",
|
|
20
|
+
"indigo",
|
|
21
|
+
"teal",
|
|
22
|
+
"red",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const formSchema = z.object({
|
|
26
|
+
label: z.string().min(1, "Vui lòng nhập tên lối tắt"),
|
|
27
|
+
href: z.string().min(1, "Vui lòng nhập đường dẫn"),
|
|
28
|
+
icon: z.string(),
|
|
29
|
+
color: z.string(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
type FormValues = z.infer<typeof formSchema>;
|
|
33
|
+
|
|
34
|
+
interface QuickAccessDialogProps {
|
|
35
|
+
open: boolean;
|
|
36
|
+
onOpenChange: (open: boolean) => void;
|
|
37
|
+
onSubmit: (data: Omit<QuickAccessItem, "id">) => void;
|
|
38
|
+
initialData?: QuickAccessItem;
|
|
39
|
+
availableFeatures?: Array<{ label: string; href: string; icon?: string }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function QuickAccessDialog({
|
|
43
|
+
open,
|
|
44
|
+
onOpenChange,
|
|
45
|
+
onSubmit,
|
|
46
|
+
initialData,
|
|
47
|
+
availableFeatures,
|
|
48
|
+
}: QuickAccessDialogProps) {
|
|
49
|
+
const form = useForm<FormValues>({
|
|
50
|
+
resolver: zodResolver(formSchema),
|
|
51
|
+
defaultValues: {
|
|
52
|
+
label: "",
|
|
53
|
+
href: "",
|
|
54
|
+
icon: "document-text",
|
|
55
|
+
color: "blue",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Reset form when dialog opens/closes or initialData changes
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (open) {
|
|
62
|
+
form.reset(
|
|
63
|
+
initialData
|
|
64
|
+
? {
|
|
65
|
+
label: initialData.label,
|
|
66
|
+
href: initialData.href,
|
|
67
|
+
icon: initialData.icon,
|
|
68
|
+
color: initialData.color,
|
|
69
|
+
}
|
|
70
|
+
: {
|
|
71
|
+
label: "",
|
|
72
|
+
href: "",
|
|
73
|
+
icon: "document-text",
|
|
74
|
+
color: "blue",
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}, [open, initialData, form]);
|
|
79
|
+
|
|
80
|
+
const handleSubmit = (values: FormValues) => {
|
|
81
|
+
onSubmit(values);
|
|
82
|
+
onOpenChange(false);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleFeatureChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
86
|
+
const selectedHref = e.target.value;
|
|
87
|
+
const feature = availableFeatures?.find((f) => f.href === selectedHref);
|
|
88
|
+
|
|
89
|
+
if (feature) {
|
|
90
|
+
form.setValue("href", feature.href);
|
|
91
|
+
form.setValue("label", feature.label);
|
|
92
|
+
if (feature.icon && iconMap[feature.icon]) {
|
|
93
|
+
form.setValue("icon", feature.icon);
|
|
94
|
+
} else if (feature.icon && iconMap[feature.icon.replace(/-/g, "")]) {
|
|
95
|
+
// Attempt fuzzy match or mapping if needed, theoretically icon names should match
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
|
102
|
+
<DialogPrimitive.Portal>
|
|
103
|
+
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
|
104
|
+
<DialogPrimitive.Content className="fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg">
|
|
105
|
+
<div className="flex flex-col space-y-1.5 text-center sm:text-left">
|
|
106
|
+
<DialogPrimitive.Title className="text-lg font-semibold leading-none tracking-tight">
|
|
107
|
+
{initialData ? "Sửa lối tắt" : "Thêm lối tắt mới"}
|
|
108
|
+
</DialogPrimitive.Title>
|
|
109
|
+
<DialogPrimitive.Description className="text-sm text-muted-foreground">
|
|
110
|
+
Tạo lối tắt để truy cập nhanh các tính năng thường dùng
|
|
111
|
+
</DialogPrimitive.Description>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<form
|
|
115
|
+
onSubmit={form.handleSubmit(handleSubmit)}
|
|
116
|
+
className="space-y-4"
|
|
117
|
+
>
|
|
118
|
+
<div className="grid gap-4 py-4">
|
|
119
|
+
{/* Feature Selection if available */}
|
|
120
|
+
{availableFeatures && availableFeatures.length > 0 && (
|
|
121
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
122
|
+
<label
|
|
123
|
+
htmlFor="feature-select"
|
|
124
|
+
className="text-right text-sm font-medium"
|
|
125
|
+
>
|
|
126
|
+
Chức năng
|
|
127
|
+
</label>
|
|
128
|
+
<div className="col-span-3">
|
|
129
|
+
<select
|
|
130
|
+
id="feature-select"
|
|
131
|
+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
132
|
+
onChange={handleFeatureChange}
|
|
133
|
+
defaultValue={initialData?.href || ""}
|
|
134
|
+
>
|
|
135
|
+
<option value="">Chọn chức năng...</option>
|
|
136
|
+
{availableFeatures.map((f) => (
|
|
137
|
+
<option key={f.href} value={f.href}>
|
|
138
|
+
{f.label}
|
|
139
|
+
</option>
|
|
140
|
+
))}
|
|
141
|
+
</select>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
147
|
+
<label
|
|
148
|
+
htmlFor="label"
|
|
149
|
+
className="text-right text-sm font-medium"
|
|
150
|
+
>
|
|
151
|
+
Tên lối tắt
|
|
152
|
+
</label>
|
|
153
|
+
<div className="col-span-3">
|
|
154
|
+
<input
|
|
155
|
+
id="label"
|
|
156
|
+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
157
|
+
{...form.register("label")}
|
|
158
|
+
/>
|
|
159
|
+
{form.formState.errors.label && (
|
|
160
|
+
<p className="text-xs text-red-500 mt-1">
|
|
161
|
+
{form.formState.errors.label.message}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Show HREF input only if NO features available (fallback) or if we want to show it as readonly?
|
|
168
|
+
The requirement says "only allow ... in the list".
|
|
169
|
+
So if list is present, HIDE the href input or make it readOnly hidden.
|
|
170
|
+
Let's hide it but keep it registered in form so validation passes.
|
|
171
|
+
*/}
|
|
172
|
+
<div
|
|
173
|
+
className={cn(
|
|
174
|
+
"grid grid-cols-4 items-center gap-4",
|
|
175
|
+
availableFeatures?.length ? "hidden" : "",
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
<label
|
|
179
|
+
htmlFor="href"
|
|
180
|
+
className="text-right text-sm font-medium"
|
|
181
|
+
>
|
|
182
|
+
Đường dẫn
|
|
183
|
+
</label>
|
|
184
|
+
<div className="col-span-3">
|
|
185
|
+
<input
|
|
186
|
+
id="href"
|
|
187
|
+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
188
|
+
{...form.register("href")}
|
|
189
|
+
readOnly={!!availableFeatures?.length}
|
|
190
|
+
/>
|
|
191
|
+
{form.formState.errors.href && (
|
|
192
|
+
<p className="text-xs text-red-500 mt-1">
|
|
193
|
+
{form.formState.errors.href.message}
|
|
194
|
+
</p>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div className="grid grid-cols-4 items-start gap-4">
|
|
200
|
+
<label className="text-right text-sm font-medium pt-2">
|
|
201
|
+
Icon
|
|
202
|
+
</label>
|
|
203
|
+
<div className="col-span-3 flex flex-wrap gap-2">
|
|
204
|
+
{AVAILABLE_ICONS.map((icon) => (
|
|
205
|
+
<button
|
|
206
|
+
key={icon}
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={() => form.setValue("icon", icon)}
|
|
209
|
+
className={cn(
|
|
210
|
+
"flex h-8 w-8 items-center justify-center rounded-md border transition-colors",
|
|
211
|
+
form.watch("icon") === icon
|
|
212
|
+
? "border-primary bg-primary/10 text-primary"
|
|
213
|
+
: "border-input hover:bg-muted",
|
|
214
|
+
)}
|
|
215
|
+
>
|
|
216
|
+
{iconMap[icon]}
|
|
217
|
+
</button>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className="grid grid-cols-4 items-start gap-4">
|
|
223
|
+
<label className="text-right text-sm font-medium pt-2">
|
|
224
|
+
Màu sắc
|
|
225
|
+
</label>
|
|
226
|
+
<div className="col-span-3 flex flex-wrap gap-2">
|
|
227
|
+
{AVAILABLE_COLORS.map((color) => (
|
|
228
|
+
<button
|
|
229
|
+
key={color}
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={() => form.setValue("color", color)}
|
|
232
|
+
className={cn(
|
|
233
|
+
"h-8 w-8 rounded-full border-2 transition-all",
|
|
234
|
+
`bg-${color}-500`,
|
|
235
|
+
form.watch("color") === color
|
|
236
|
+
? "border-primary scale-110 shadow-sm"
|
|
237
|
+
: "border-transparent hover:scale-105",
|
|
238
|
+
)}
|
|
239
|
+
aria-label={color}
|
|
240
|
+
/>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<div className="flex justify-end gap-2">
|
|
247
|
+
<DialogPrimitive.Close asChild>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2"
|
|
251
|
+
>
|
|
252
|
+
Hủy
|
|
253
|
+
</button>
|
|
254
|
+
</DialogPrimitive.Close>
|
|
255
|
+
<button
|
|
256
|
+
type="submit"
|
|
257
|
+
className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2"
|
|
258
|
+
>
|
|
259
|
+
{initialData ? "Lưu thay đổi" : "Thêm mới"}
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
</form>
|
|
263
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
264
|
+
<X className="h-4 w-4" />
|
|
265
|
+
<span className="sr-only">Close</span>
|
|
266
|
+
</DialogPrimitive.Close>
|
|
267
|
+
</DialogPrimitive.Content>
|
|
268
|
+
</DialogPrimitive.Portal>
|
|
269
|
+
</DialogPrimitive.Root>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import { Plus, X, Pencil, GripVertical } from "lucide-react";
|
|
6
|
+
import { cn } from "../utils";
|
|
7
|
+
import type { QuickAccessItem } from "./types";
|
|
8
|
+
import { QuickAccessDialog } from "./quick-access-dialog";
|
|
9
|
+
|
|
10
|
+
interface QuickAccessMenuProps {
|
|
11
|
+
items: QuickAccessItem[];
|
|
12
|
+
onItemClick?: (item: QuickAccessItem) => void;
|
|
13
|
+
onItemsChange?: (items: QuickAccessItem[]) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
editable?: boolean;
|
|
16
|
+
availableFeatures?: Array<{ label: string; href: string; icon?: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const containerVariants = {
|
|
20
|
+
hidden: { opacity: 0 },
|
|
21
|
+
visible: {
|
|
22
|
+
opacity: 1,
|
|
23
|
+
transition: {
|
|
24
|
+
staggerChildren: 0.05,
|
|
25
|
+
delayChildren: 0.1,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const itemVariants = {
|
|
31
|
+
hidden: { opacity: 0, scale: 0.9 },
|
|
32
|
+
visible: {
|
|
33
|
+
opacity: 1,
|
|
34
|
+
scale: 1,
|
|
35
|
+
transition: {
|
|
36
|
+
duration: 0.2,
|
|
37
|
+
ease: "easeOut",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
import { iconMap, colorClasses } from "./constants";
|
|
43
|
+
|
|
44
|
+
export function QuickAccessMenu({
|
|
45
|
+
items,
|
|
46
|
+
onItemClick,
|
|
47
|
+
onItemsChange,
|
|
48
|
+
className,
|
|
49
|
+
editable = false,
|
|
50
|
+
availableFeatures,
|
|
51
|
+
}: QuickAccessMenuProps) {
|
|
52
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
53
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
54
|
+
const [editingItem, setEditingItem] = useState<QuickAccessItem | undefined>(
|
|
55
|
+
undefined,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const handleAddStart = () => {
|
|
59
|
+
setEditingItem(undefined);
|
|
60
|
+
setDialogOpen(true);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleEditStart = (item: QuickAccessItem, e: React.MouseEvent) => {
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
setEditingItem(item);
|
|
66
|
+
setDialogOpen(true);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleDelete = (itemId: string, e: React.MouseEvent) => {
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
if (confirm("Bạn có chắc muốn xóa lối tắt này?")) {
|
|
72
|
+
onItemsChange?.(items.filter((item) => item.id !== itemId));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleSave = (data: Omit<QuickAccessItem, "id">) => {
|
|
77
|
+
// Check for duplicates
|
|
78
|
+
const isDuplicate = items.some(
|
|
79
|
+
(item) => item.href === data.href && item.id !== editingItem?.id,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (isDuplicate) {
|
|
83
|
+
alert("Lối tắt này đã tồn tại!");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (editingItem) {
|
|
88
|
+
// Edit existing
|
|
89
|
+
onItemsChange?.(
|
|
90
|
+
items.map((item) =>
|
|
91
|
+
item.id === editingItem.id ? { ...item, ...data } : item,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
// Add new
|
|
96
|
+
const newItem: QuickAccessItem = {
|
|
97
|
+
...data,
|
|
98
|
+
id: `custom-${Date.now()}`,
|
|
99
|
+
};
|
|
100
|
+
onItemsChange?.([...items, newItem]);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (items.length === 0 && !editable) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className={cn("space-y-4", className)}>
|
|
110
|
+
<div className="flex items-center justify-between">
|
|
111
|
+
<h3 className="text-slate-900 dark:text-white text-lg font-bold">
|
|
112
|
+
Quick Shortcuts
|
|
113
|
+
</h3>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<motion.div
|
|
117
|
+
variants={containerVariants}
|
|
118
|
+
initial="hidden"
|
|
119
|
+
animate="visible"
|
|
120
|
+
// @ts-ignore className is valid for motion.div
|
|
121
|
+
className="grid grid-cols-2 gap-4 sm:grid-cols-4"
|
|
122
|
+
>
|
|
123
|
+
<AnimatePresence mode="popLayout">
|
|
124
|
+
{items.map((item) => (
|
|
125
|
+
<motion.button
|
|
126
|
+
key={item.id}
|
|
127
|
+
variants={itemVariants}
|
|
128
|
+
whileHover={{ scale: 1.02 }}
|
|
129
|
+
whileTap={{ scale: 0.98 }}
|
|
130
|
+
// @ts-ignore onClick and className are valid for motion.button
|
|
131
|
+
onClick={() => {
|
|
132
|
+
if (!isEditing) {
|
|
133
|
+
onItemClick?.(item);
|
|
134
|
+
}
|
|
135
|
+
}}
|
|
136
|
+
className={cn(
|
|
137
|
+
"group relative flex flex-col items-center justify-center gap-3 bg-white dark:bg-slate-800 p-5 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm hover:shadow-md hover:border-primary/50 transition-all text-left",
|
|
138
|
+
isEditing && "cursor-default opacity-90 hover:opacity-100",
|
|
139
|
+
)}
|
|
140
|
+
>
|
|
141
|
+
{isEditing && (
|
|
142
|
+
<>
|
|
143
|
+
<div
|
|
144
|
+
onClick={(e) => handleDelete(item.id, e)}
|
|
145
|
+
className="absolute -right-2 -top-2 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full bg-destructive text-white shadow-sm ring-2 ring-white hover:bg-destructive/90"
|
|
146
|
+
>
|
|
147
|
+
<X className="h-3 w-3" />
|
|
148
|
+
</div>
|
|
149
|
+
<div
|
|
150
|
+
onClick={(e) => handleEditStart(item, e)}
|
|
151
|
+
className="absolute -left-2 -top-2 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full bg-background text-foreground shadow-sm ring-2 ring-white hover:bg-muted"
|
|
152
|
+
>
|
|
153
|
+
<Pencil className="h-3 w-3" />
|
|
154
|
+
</div>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{editable && (
|
|
159
|
+
<span
|
|
160
|
+
onClick={() => setIsEditing(!isEditing)}
|
|
161
|
+
className="absolute top-2 right-2 text-slate-300 dark:text-slate-600 cursor-move opacity-100 hover:text-primary transition-colors"
|
|
162
|
+
>
|
|
163
|
+
<GripVertical size={18} />
|
|
164
|
+
</span>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
<div
|
|
168
|
+
className={cn(
|
|
169
|
+
"size-12 rounded-full flex items-center justify-center group-hover:scale-110 transition-transform",
|
|
170
|
+
colorClasses[item.color || "blue"] || colorClasses.blue,
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
{iconMap[item.icon] || iconMap["document-text"]}
|
|
174
|
+
</div>
|
|
175
|
+
<span className="text-slate-700 dark:text-slate-200 font-semibold text-sm">
|
|
176
|
+
{item.label}
|
|
177
|
+
</span>
|
|
178
|
+
</motion.button>
|
|
179
|
+
))}
|
|
180
|
+
|
|
181
|
+
{editable && (
|
|
182
|
+
<motion.button
|
|
183
|
+
variants={itemVariants}
|
|
184
|
+
// @ts-ignore
|
|
185
|
+
onClick={handleAddStart}
|
|
186
|
+
whileHover={{ scale: 1.02 }}
|
|
187
|
+
whileTap={{ scale: 0.98 }}
|
|
188
|
+
// @ts-ignore
|
|
189
|
+
className="flex flex-col items-center justify-center gap-2 bg-slate-50 dark:bg-slate-800/50 p-5 rounded-xl border-2 border-dashed border-slate-300 dark:border-slate-600 hover:border-primary hover:bg-slate-100 dark:hover:bg-slate-800 transition-all text-slate-500 hover:text-primary cursor-pointer group h-full min-h-[140px]"
|
|
190
|
+
>
|
|
191
|
+
<Plus className="h-8 w-8 group-hover:scale-110 transition-transform" />
|
|
192
|
+
<span className="text-sm font-medium">Add Shortcut</span>
|
|
193
|
+
</motion.button>
|
|
194
|
+
)}
|
|
195
|
+
</AnimatePresence>
|
|
196
|
+
</motion.div>
|
|
197
|
+
|
|
198
|
+
<QuickAccessDialog
|
|
199
|
+
open={dialogOpen}
|
|
200
|
+
onOpenChange={setDialogOpen}
|
|
201
|
+
onSubmit={handleSave}
|
|
202
|
+
initialData={editingItem}
|
|
203
|
+
availableFeatures={availableFeatures}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Default quick access items
|
|
210
|
+
export const DEFAULT_QUICK_ACCESS: QuickAccessItem[] = [
|
|
211
|
+
{
|
|
212
|
+
id: "pos",
|
|
213
|
+
label: "Bán hàng",
|
|
214
|
+
href: "/sales/pos",
|
|
215
|
+
icon: "shopping-cart",
|
|
216
|
+
color: "green",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "orders",
|
|
220
|
+
label: "Đơn hàng",
|
|
221
|
+
href: "/sales-orders",
|
|
222
|
+
icon: "clipboard-list",
|
|
223
|
+
color: "blue",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "customers",
|
|
227
|
+
label: "Khách hàng",
|
|
228
|
+
href: "/customers",
|
|
229
|
+
icon: "users",
|
|
230
|
+
color: "purple",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "inventory",
|
|
234
|
+
label: "Tồn kho",
|
|
235
|
+
href: "/inventory",
|
|
236
|
+
icon: "package",
|
|
237
|
+
color: "orange",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "reports",
|
|
241
|
+
label: "Báo cáo",
|
|
242
|
+
href: "/reports",
|
|
243
|
+
icon: "chart-bar",
|
|
244
|
+
color: "teal",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "finance",
|
|
248
|
+
label: "Thu chi",
|
|
249
|
+
href: "/finance",
|
|
250
|
+
icon: "currency-dollar",
|
|
251
|
+
color: "pink",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: "products",
|
|
255
|
+
label: "Sản phẩm",
|
|
256
|
+
href: "/crud/products",
|
|
257
|
+
icon: "package",
|
|
258
|
+
color: "indigo",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "settings",
|
|
262
|
+
label: "Cài đặt",
|
|
263
|
+
href: "/admin",
|
|
264
|
+
icon: "cog",
|
|
265
|
+
color: "red",
|
|
266
|
+
},
|
|
267
|
+
];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Widget System Types for Home Page
|
|
2
|
+
|
|
3
|
+
export type WidgetType = "revenue" | "orders" | "customers" | "stock";
|
|
4
|
+
|
|
5
|
+
export type WidgetSize = "small" | "medium" | "large";
|
|
6
|
+
|
|
7
|
+
export interface WidgetConfig {
|
|
8
|
+
id: string;
|
|
9
|
+
type: WidgetType;
|
|
10
|
+
position: number;
|
|
11
|
+
visible: boolean;
|
|
12
|
+
size: WidgetSize;
|
|
13
|
+
settings?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WidgetProps {
|
|
17
|
+
config: WidgetConfig;
|
|
18
|
+
onRemove?: () => void;
|
|
19
|
+
onSettings?: () => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WidgetData {
|
|
24
|
+
value: number | string;
|
|
25
|
+
label: string;
|
|
26
|
+
change?: {
|
|
27
|
+
value: number;
|
|
28
|
+
type: "increase" | "decrease";
|
|
29
|
+
period: string;
|
|
30
|
+
};
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface QuickAccessItem {
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
href: string;
|
|
39
|
+
icon: string;
|
|
40
|
+
color?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UserHomePreferences {
|
|
44
|
+
widgets: WidgetConfig[];
|
|
45
|
+
quickAccess: QuickAccessItem[]; // List of quick access items
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_WIDGETS: WidgetConfig[] = [
|
|
49
|
+
{
|
|
50
|
+
id: "revenue",
|
|
51
|
+
type: "revenue",
|
|
52
|
+
position: 0,
|
|
53
|
+
visible: true,
|
|
54
|
+
size: "medium",
|
|
55
|
+
},
|
|
56
|
+
{ id: "orders", type: "orders", position: 1, visible: true, size: "medium" },
|
|
57
|
+
{
|
|
58
|
+
id: "customers",
|
|
59
|
+
type: "customers",
|
|
60
|
+
position: 2,
|
|
61
|
+
visible: true,
|
|
62
|
+
size: "medium",
|
|
63
|
+
},
|
|
64
|
+
{ id: "stock", type: "stock", position: 3, visible: true, size: "medium" },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export const WIDGET_LABELS: Record<WidgetType, string> = {
|
|
68
|
+
revenue: "Doanh thu",
|
|
69
|
+
orders: "Đơn hàng",
|
|
70
|
+
customers: "Khách hàng",
|
|
71
|
+
stock: "Tồn kho",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface ERPFeature {
|
|
75
|
+
id: string;
|
|
76
|
+
icon: string;
|
|
77
|
+
title: string;
|
|
78
|
+
description: string;
|
|
79
|
+
subFeatures: string[];
|
|
80
|
+
color: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const ERP_FEATURES: ERPFeature[] = [
|
|
84
|
+
{
|
|
85
|
+
id: "inventory",
|
|
86
|
+
icon: "📦",
|
|
87
|
+
title: "Quản lý Kho",
|
|
88
|
+
description: "Quản lý toàn diện hàng hóa và tồn kho",
|
|
89
|
+
subFeatures: ["Nhập/Xuất kho", "Kiểm kê", "Điều chuyển", "Cảnh báo tồn"],
|
|
90
|
+
color: "from-blue-500 to-cyan-500",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "sales",
|
|
94
|
+
icon: "💰",
|
|
95
|
+
title: "Quản lý Bán hàng",
|
|
96
|
+
description: "Xử lý đơn hàng và bán hàng hiệu quả",
|
|
97
|
+
subFeatures: ["POS", "Đơn hàng", "Hợp đồng", "Báo giá"],
|
|
98
|
+
color: "from-green-500 to-emerald-500",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "purchasing",
|
|
102
|
+
icon: "🛒",
|
|
103
|
+
title: "Quản lý Mua hàng",
|
|
104
|
+
description: "Quản lý nhà cung cấp và đặt hàng",
|
|
105
|
+
subFeatures: ["Đặt hàng NCC", "Nhập hàng", "Công nợ phải trả"],
|
|
106
|
+
color: "from-orange-500 to-amber-500",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "finance",
|
|
110
|
+
icon: "💵",
|
|
111
|
+
title: "Tài chính",
|
|
112
|
+
description: "Quản lý dòng tiền và công nợ",
|
|
113
|
+
subFeatures: ["Thu/Chi", "Sổ quỹ", "Công nợ", "Báo cáo tài chính"],
|
|
114
|
+
color: "from-purple-500 to-violet-500",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "crm",
|
|
118
|
+
icon: "👥",
|
|
119
|
+
title: "CRM",
|
|
120
|
+
description: "Chăm sóc và quản lý khách hàng",
|
|
121
|
+
subFeatures: ["Khách hàng", "Lịch sử giao dịch", "Phân loại"],
|
|
122
|
+
color: "from-pink-500 to-rose-500",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "hr",
|
|
126
|
+
icon: "👨💼",
|
|
127
|
+
title: "Nhân sự",
|
|
128
|
+
description: "Quản lý người dùng và phân quyền",
|
|
129
|
+
subFeatures: ["Users", "Phân quyền", "Roles"],
|
|
130
|
+
color: "from-indigo-500 to-blue-500",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "reports",
|
|
134
|
+
icon: "📊",
|
|
135
|
+
title: "Báo cáo",
|
|
136
|
+
description: "Thống kê và phân tích dữ liệu",
|
|
137
|
+
subFeatures: ["Dashboard", "Thống kê", "Export"],
|
|
138
|
+
color: "from-teal-500 to-green-500",
|
|
139
|
+
},
|
|
140
|
+
];
|