@mmapp/react-compiler 0.1.0-alpha.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/README.md +107 -0
- package/compile-blueprint-chat.mjs +99 -0
- package/compile-blueprint-glass-console.mjs +98 -0
- package/compile-chat-defs.mjs +92 -0
- package/dist/babel/index.d.mts +3 -0
- package/dist/babel/index.d.ts +3 -0
- package/dist/babel/index.js +4851 -0
- package/dist/babel/index.mjs +7 -0
- package/dist/chunk-26U577GB.mjs +3465 -0
- package/dist/chunk-2FBDFAX6.mjs +2362 -0
- package/dist/chunk-2L4QSMXG.mjs +175 -0
- package/dist/chunk-2REDFOER.mjs +931 -0
- package/dist/chunk-46YKSHQR.mjs +175 -0
- package/dist/chunk-4XHK6FWL.mjs +2058 -0
- package/dist/chunk-5M7DKKBC.mjs +215 -0
- package/dist/chunk-5VNJ7C6N.mjs +154 -0
- package/dist/chunk-6CQOAAMV.mjs +1803 -0
- package/dist/chunk-6SEVAAVT.mjs +3516 -0
- package/dist/chunk-6YLR5ZDA.mjs +2829 -0
- package/dist/chunk-AOGY2GK6.mjs +3292 -0
- package/dist/chunk-AXXUXRNA.mjs +1434 -0
- package/dist/chunk-CHLVKMQW.mjs +175 -0
- package/dist/chunk-CKGOZAB7.mjs +939 -0
- package/dist/chunk-D34RAZUX.mjs +2223 -0
- package/dist/chunk-EQGA6A6D.mjs +121 -0
- package/dist/chunk-EY2CSXYA.mjs +822 -0
- package/dist/chunk-FIQ65CDR.mjs +925 -0
- package/dist/chunk-FOZXJFAR.mjs +186 -0
- package/dist/chunk-FX6URXWN.mjs +186 -0
- package/dist/chunk-G7SMOWOL.mjs +828 -0
- package/dist/chunk-GGB4G5YY.mjs +175 -0
- package/dist/chunk-HLRGCCIL.mjs +4839 -0
- package/dist/chunk-HOIUP6IF.mjs +690 -0
- package/dist/chunk-I3AU7GRD.mjs +120 -0
- package/dist/chunk-ILFGMUVD.mjs +1933 -0
- package/dist/chunk-IPTX5MJU.mjs +3223 -0
- package/dist/chunk-ITGUSH2Z.mjs +2783 -0
- package/dist/chunk-IXHBCAMF.mjs +3306 -0
- package/dist/chunk-J7TWJ3TM.mjs +2784 -0
- package/dist/chunk-JDPLDGVF.mjs +4810 -0
- package/dist/chunk-K53XP2DL.mjs +148 -0
- package/dist/chunk-K5HX2SVL.mjs +1902 -0
- package/dist/chunk-KFGYOOVS.mjs +214 -0
- package/dist/chunk-KFVVOS5N.mjs +925 -0
- package/dist/chunk-L2OZ4CDV.mjs +113 -0
- package/dist/chunk-MIZV3TAN.mjs +3293 -0
- package/dist/chunk-NKKLQE5V.mjs +148 -0
- package/dist/chunk-NOW23XFZ.mjs +186 -0
- package/dist/chunk-NRXQKQ74.mjs +148 -0
- package/dist/chunk-OWI6XWCD.mjs +3375 -0
- package/dist/chunk-PRUMNNDI.mjs +3192 -0
- package/dist/chunk-QTBD5B3F.mjs +148 -0
- package/dist/chunk-SKSDPPNT.mjs +3788 -0
- package/dist/chunk-SP2YUS33.mjs +186 -0
- package/dist/chunk-SU4E6E7B.mjs +3153 -0
- package/dist/chunk-SYUUKW5A.mjs +3379 -0
- package/dist/chunk-UL2XZEMA.mjs +3128 -0
- package/dist/chunk-XMWUHQVV.mjs +939 -0
- package/dist/chunk-XZNEDRGN.mjs +3876 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/chunk-YFS6JMYO.mjs +3342 -0
- package/dist/chunk-Z6AIQ4KL.mjs +113 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +11585 -0
- package/dist/cli/index.mjs +701 -0
- package/dist/codemod/cli.d.mts +1 -0
- package/dist/codemod/cli.d.ts +1 -0
- package/dist/codemod/cli.js +1104 -0
- package/dist/codemod/cli.mjs +157 -0
- package/dist/codemod/index.d.mts +148 -0
- package/dist/codemod/index.d.ts +148 -0
- package/dist/codemod/index.js +981 -0
- package/dist/codemod/index.mjs +25 -0
- package/dist/dev-server-Bs_sz2DG.d.mts +111 -0
- package/dist/dev-server-Bs_sz2DG.d.ts +111 -0
- package/dist/dev-server-CjoufJ-u.d.mts +109 -0
- package/dist/dev-server-CjoufJ-u.d.ts +109 -0
- package/dist/dev-server.d.mts +3 -0
- package/dist/dev-server.d.ts +3 -0
- package/dist/dev-server.js +7603 -0
- package/dist/dev-server.mjs +11 -0
- package/dist/envelope-DD7v0v6E.d.mts +265 -0
- package/dist/envelope-DD7v0v6E.d.ts +265 -0
- package/dist/envelope-vCVjrHlo.d.mts +265 -0
- package/dist/envelope-vCVjrHlo.d.ts +265 -0
- package/dist/envelope.d.mts +2 -0
- package/dist/envelope.d.ts +2 -0
- package/dist/envelope.js +5184 -0
- package/dist/envelope.mjs +9 -0
- package/dist/index-B5gSgvnd.d.mts +44 -0
- package/dist/index-B5gSgvnd.d.ts +44 -0
- package/dist/index-Bs0MnR54.d.mts +103 -0
- package/dist/index-Bs0MnR54.d.ts +103 -0
- package/dist/index-DR0nNc_f.d.mts +101 -0
- package/dist/index-DR0nNc_f.d.ts +101 -0
- package/dist/index-revho_gS.d.mts +104 -0
- package/dist/index-revho_gS.d.ts +104 -0
- package/dist/index.d.mts +1099 -0
- package/dist/index.d.ts +1099 -0
- package/dist/index.js +10162 -0
- package/dist/index.mjs +372 -0
- package/dist/init-IXEE2RCF.mjs +340 -0
- package/dist/project-compiler-EGJUTAJU.mjs +10 -0
- package/dist/project-compiler-VFR6CSDX.mjs +10 -0
- package/dist/project-decompiler-5GY2KSG4.mjs +7 -0
- package/dist/pull-A2QUHW4K.mjs +109 -0
- package/dist/pull-JBEQWVPE.mjs +109 -0
- package/dist/testing/index.d.mts +211 -0
- package/dist/testing/index.d.ts +211 -0
- package/dist/testing/index.js +5106 -0
- package/dist/testing/index.mjs +247 -0
- package/dist/vite/index.d.mts +59 -0
- package/dist/vite/index.d.ts +59 -0
- package/dist/vite/index.js +5023 -0
- package/dist/vite/index.mjs +8 -0
- package/examples/README.md +72 -0
- package/examples/authentication/main.workflow.tsx +139 -0
- package/examples/authentication/mm.config.ts +22 -0
- package/examples/authentication/models/auth.ts +45 -0
- package/examples/authentication/pages/LoginPage.tsx +79 -0
- package/examples/authentication/pages/SignupPage.tsx +87 -0
- package/examples/counter.workflow.tsx +65 -0
- package/examples/dashboard.workflow.tsx +419 -0
- package/examples/invoice-approval/actions/invoice.server.ts +72 -0
- package/examples/invoice-approval/main.workflow.tsx +168 -0
- package/examples/invoice-approval/mm.config.ts +18 -0
- package/examples/invoice-approval/models/invoice.ts +46 -0
- package/examples/invoice-approval/pages/InvoiceDetailPage.tsx +175 -0
- package/examples/invoice-approval/pages/InvoiceFormPage.tsx +198 -0
- package/examples/invoice-approval/pages/InvoiceListPage.tsx +141 -0
- package/examples/todo-app.workflow.tsx +131 -0
- package/examples/uber-app/actions/matching.server.ts +177 -0
- package/examples/uber-app/actions/notifications.server.ts +176 -0
- package/examples/uber-app/actions/payments.server.ts +184 -0
- package/examples/uber-app/actions/pricing.server.ts +176 -0
- package/examples/uber-app/app/admin/analytics.tsx +102 -0
- package/examples/uber-app/app/admin/fleet.tsx +102 -0
- package/examples/uber-app/app/admin/surge-pricing.tsx +95 -0
- package/examples/uber-app/app/driver/dashboard.tsx +87 -0
- package/examples/uber-app/app/driver/earnings.tsx +101 -0
- package/examples/uber-app/app/driver/navigation.tsx +94 -0
- package/examples/uber-app/app/driver/ride-acceptance.tsx +103 -0
- package/examples/uber-app/app/rider/home.tsx +109 -0
- package/examples/uber-app/app/rider/payment-methods.tsx +134 -0
- package/examples/uber-app/app/rider/ride-history.tsx +90 -0
- package/examples/uber-app/app/rider/ride-tracking.tsx +108 -0
- package/examples/uber-app/components/DriverCard.tsx +176 -0
- package/examples/uber-app/components/MapView.tsx +216 -0
- package/examples/uber-app/components/RatingStars.tsx +227 -0
- package/examples/uber-app/components/RideCard.tsx +167 -0
- package/examples/uber-app/mm.config.ts +30 -0
- package/examples/uber-app/models/location.model.ts +70 -0
- package/examples/uber-app/models/payment.model.ts +87 -0
- package/examples/uber-app/models/rating.model.ts +54 -0
- package/examples/uber-app/models/ride.model.ts +118 -0
- package/examples/uber-app/models/user.model.ts +66 -0
- package/examples/uber-app/models/vehicle.model.ts +63 -0
- package/examples/uber-app/tests/payment.test.tsx +129 -0
- package/examples/uber-app/tests/ride-flow.test.tsx +123 -0
- package/examples/uber-app/workflows/dispute-resolution.workflow.tsx +205 -0
- package/examples/uber-app/workflows/driver-onboarding.workflow.tsx +227 -0
- package/examples/uber-app/workflows/payment-processing.workflow.tsx +223 -0
- package/examples/uber-app/workflows/ride-request.workflow.tsx +194 -0
- package/package.json +77 -0
- package/package.json.backup +86 -0
- package/scripts/decompile.ts +226 -0
- package/scripts/seed-auth.ts +267 -0
- package/scripts/seed-uber.ts +248 -0
- package/scripts/validate-uber.ts +119 -0
- package/seed-blueprint-chat.mjs +444 -0
- package/seed-blueprint-glass-console.mjs +445 -0
- package/seed-compiled.mjs +318 -0
- package/src/RoundTripValidator.ts +400 -0
- package/src/__tests__/atom-rendering-coverage.test.ts +680 -0
- package/src/__tests__/auth-module-compilation.test.ts +247 -0
- package/src/__tests__/auth-template-compilation.test.ts +589 -0
- package/src/__tests__/change-extractor.test.ts +142 -0
- package/src/__tests__/cli-pull.test.ts +73 -0
- package/src/__tests__/cli-test.test.ts +72 -0
- package/src/__tests__/component-extractor.test.ts +331 -0
- package/src/__tests__/context-extractor.test.ts +145 -0
- package/src/__tests__/decompiler.test.ts +718 -0
- package/src/__tests__/define-blueprint.test.ts +133 -0
- package/src/__tests__/definition-validator.test.ts +519 -0
- package/src/__tests__/during-extractor.test.ts +152 -0
- package/src/__tests__/effect-extractor.test.ts +107 -0
- package/src/__tests__/event-emission.test.ts +127 -0
- package/src/__tests__/examples.test.ts +236 -0
- package/src/__tests__/full-blueprint-coverage.test.ts +1221 -0
- package/src/__tests__/golden-suite.test.ts +403 -0
- package/src/__tests__/grammar-island-extractor.test.ts +289 -0
- package/src/__tests__/instance-key.test.ts +82 -0
- package/src/__tests__/ir-migration.test.ts +255 -0
- package/src/__tests__/lock-file.test.ts +117 -0
- package/src/__tests__/model-extractor.test.ts +195 -0
- package/src/__tests__/model-field-acl.test.ts +237 -0
- package/src/__tests__/model-hooks.test.ts +130 -0
- package/src/__tests__/model-ref-resolution.test.ts +268 -0
- package/src/__tests__/model-roundtrip.test.ts +502 -0
- package/src/__tests__/model-runtime.test.ts +112 -0
- package/src/__tests__/model-transitions.test.ts +183 -0
- package/src/__tests__/nrt-action-trace.test.ts +391 -0
- package/src/__tests__/pipeline-hardening.test.ts +413 -0
- package/src/__tests__/project-compiler.test.ts +546 -0
- package/src/__tests__/project-decompiler.test.ts +343 -0
- package/src/__tests__/query-compilation.test.ts +145 -0
- package/src/__tests__/round-trip/PLAN.md +158 -0
- package/src/__tests__/round-trip/README.md +52 -0
- package/src/__tests__/round-trip/RESULTS.md +86 -0
- package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +55 -0
- package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +11 -0
- package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +54 -0
- package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +79 -0
- package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +12 -0
- package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +50 -0
- package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +25 -0
- package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +11 -0
- package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +32 -0
- package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +79 -0
- package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +10 -0
- package/src/__tests__/round-trip/round-trip.test.ts +2598 -0
- package/src/__tests__/round-trip-ir.test.ts +300 -0
- package/src/__tests__/round-trip.test.ts +1212 -0
- package/src/__tests__/route-merging.test.ts +372 -0
- package/src/__tests__/router-composition.test.ts +489 -0
- package/src/__tests__/router-extractor.test.ts +176 -0
- package/src/__tests__/server-action-extractor.test.ts +128 -0
- package/src/__tests__/smart-type-inference.test.ts +365 -0
- package/src/__tests__/source-envelope.test.ts +284 -0
- package/src/__tests__/source-fidelity.test.ts +516 -0
- package/src/__tests__/state-extractor.test.ts +115 -0
- package/src/__tests__/strict-mode.test.ts +227 -0
- package/src/__tests__/transition-effect-extractor.test.ts +119 -0
- package/src/__tests__/transition-extractor.test.ts +68 -0
- package/src/__tests__/ts-to-expression.test.ts +462 -0
- package/src/__tests__/type-generator.test.ts +201 -0
- package/src/__tests__/uber-validation.test.ts +502 -0
- package/src/action-compiler.ts +361 -0
- package/src/babel/emitters/experience-transform.ts +199 -0
- package/src/babel/emitters/ir-to-tsx-emitter.ts +110 -0
- package/src/babel/emitters/pure-form-emitter.ts +1023 -0
- package/src/babel/emitters/runtime-glue-emitter.ts +39 -0
- package/src/babel/extractors/change-extractor.ts +199 -0
- package/src/babel/extractors/component-extractor.ts +907 -0
- package/src/babel/extractors/computed-extractor.ts +262 -0
- package/src/babel/extractors/context-extractor.ts +277 -0
- package/src/babel/extractors/during-extractor.ts +295 -0
- package/src/babel/extractors/effect-extractor.ts +340 -0
- package/src/babel/extractors/event-extractor.ts +235 -0
- package/src/babel/extractors/grammar-island-extractor.ts +302 -0
- package/src/babel/extractors/model-extractor.ts +1018 -0
- package/src/babel/extractors/router-extractor.ts +303 -0
- package/src/babel/extractors/server-action-extractor.ts +173 -0
- package/src/babel/extractors/server-action-hook-extractor.ts +72 -0
- package/src/babel/extractors/server-state-extractor.ts +88 -0
- package/src/babel/extractors/state-extractor.ts +214 -0
- package/src/babel/extractors/transition-effect-extractor.ts +176 -0
- package/src/babel/extractors/transition-extractor.ts +143 -0
- package/src/babel/index.ts +24 -0
- package/src/babel/transpilers/ts-to-expression.ts +674 -0
- package/src/babel/visitor.ts +807 -0
- package/src/cli/auth.ts +255 -0
- package/src/cli/build.ts +288 -0
- package/src/cli/deploy.ts +206 -0
- package/src/cli/index.ts +328 -0
- package/src/cli/init.ts +388 -0
- package/src/cli/installer.ts +261 -0
- package/src/cli/lock-file.ts +94 -0
- package/src/cli/mmrc.ts +22 -0
- package/src/cli/pull.ts +172 -0
- package/src/cli/registry-client.ts +175 -0
- package/src/cli/test.ts +397 -0
- package/src/cli/type-generator.ts +243 -0
- package/src/codemod/__tests__/forward.test.ts +239 -0
- package/src/codemod/__tests__/reverse.test.ts +145 -0
- package/src/codemod/__tests__/round-trip.test.ts +137 -0
- package/src/codemod/annotation.ts +97 -0
- package/src/codemod/classify.ts +197 -0
- package/src/codemod/cli.ts +207 -0
- package/src/codemod/control-flow.ts +409 -0
- package/src/codemod/forward.ts +244 -0
- package/src/codemod/import-manager.ts +171 -0
- package/src/codemod/index.ts +120 -0
- package/src/codemod/reverse.ts +197 -0
- package/src/codemod/rules.ts +174 -0
- package/src/codemod/state-transform.ts +126 -0
- package/src/decompiler/ast-builder.ts +538 -0
- package/src/decompiler/config-generator.ts +151 -0
- package/src/decompiler/index.ts +315 -0
- package/src/decompiler/project-decompiler.ts +1776 -0
- package/src/decompiler/project.ts +862 -0
- package/src/decompiler/split-strategy.ts +140 -0
- package/src/decompiler/state-emitter.ts +1053 -0
- package/src/decompiler/sx-emitter.ts +318 -0
- package/src/decompiler/workspace-hydrator.ts +189 -0
- package/src/dev-server.ts +238 -0
- package/src/envelope/fs-tree.ts +217 -0
- package/src/envelope/source-envelope.ts +264 -0
- package/src/envelope.ts +315 -0
- package/src/incremental-compiler.ts +401 -0
- package/src/index.ts +99 -0
- package/src/model-compiler.ts +277 -0
- package/src/project-compiler.ts +1629 -0
- package/src/route-extractor.ts +333 -0
- package/src/testing/index.ts +32 -0
- package/src/testing/snapshot.ts +252 -0
- package/src/testing/test-utils.ts +226 -0
- package/src/types.ts +68 -0
- package/src/vite/index.ts +288 -0
- package/test-compile.mjs +142 -0
- package/tsconfig.json +25 -0
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @workflow slug="dashboard" version="1.0.0" category="blueprint"
|
|
3
|
+
* @description Real-world dashboard blueprint with multi-route layout, role-based access,
|
|
4
|
+
* data source bindings, state management, and conditional rendering based on state.
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
useTransition,
|
|
9
|
+
useOnEnter,
|
|
10
|
+
useOnExit,
|
|
11
|
+
useOnChange,
|
|
12
|
+
useWhileIn,
|
|
13
|
+
useOnEvent,
|
|
14
|
+
useQuery,
|
|
15
|
+
useMutation,
|
|
16
|
+
useRole,
|
|
17
|
+
} from '@mindmatrix/react';
|
|
18
|
+
|
|
19
|
+
export function Dashboard() {
|
|
20
|
+
// --- Fields ---
|
|
21
|
+
const [activeRoute, setActiveRoute] = useState('/dashboard');
|
|
22
|
+
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
23
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
24
|
+
const [notificationCount, setNotificationCount] = useState(0);
|
|
25
|
+
const [selectedRecordId, setSelectedRecordId] = useState('');
|
|
26
|
+
const [theme, setTheme] = useState('light');
|
|
27
|
+
const [refreshInterval, setRefreshInterval] = useState(30000);
|
|
28
|
+
const [lastRefreshed, setLastRefreshed] = useState('');
|
|
29
|
+
|
|
30
|
+
// --- Data Sources ---
|
|
31
|
+
|
|
32
|
+
// Main metrics for the dashboard overview
|
|
33
|
+
const { data: metrics } = useQuery('dashboard-metric', {
|
|
34
|
+
limit: 20,
|
|
35
|
+
orderBy: 'priority',
|
|
36
|
+
filter: { visible: true },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Recent activity feed
|
|
40
|
+
const { data: activities } = useQuery('activity-log', {
|
|
41
|
+
limit: 50,
|
|
42
|
+
orderBy: 'created_at',
|
|
43
|
+
order: 'desc',
|
|
44
|
+
filter: { archived: false },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Workflow definitions for the data browser
|
|
48
|
+
const { data: definitions } = useQuery('workflow-definition', {
|
|
49
|
+
limit: 100,
|
|
50
|
+
filter: { category: 'data' },
|
|
51
|
+
orderBy: 'name',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Notifications
|
|
55
|
+
const { data: notifications } = useQuery('notification', {
|
|
56
|
+
limit: 10,
|
|
57
|
+
filter: { read: false },
|
|
58
|
+
orderBy: 'created_at',
|
|
59
|
+
order: 'desc',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// User settings
|
|
63
|
+
const { data: userSettings } = useQuery('user-setting', {
|
|
64
|
+
limit: 1,
|
|
65
|
+
filter: { scope: 'dashboard' },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const settingsMutation = useMutation('user-setting');
|
|
69
|
+
const activityMutation = useMutation('activity-log');
|
|
70
|
+
|
|
71
|
+
// --- Roles ---
|
|
72
|
+
const isAdmin = useRole('admin');
|
|
73
|
+
const isViewer = useRole('viewer');
|
|
74
|
+
const isEditor = useRole('editor');
|
|
75
|
+
|
|
76
|
+
// --- States & Hooks ---
|
|
77
|
+
|
|
78
|
+
// When the dashboard initializes, clear stale state
|
|
79
|
+
useOnEnter('active', () => {
|
|
80
|
+
setNotificationCount(0);
|
|
81
|
+
setLastRefreshed('');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// When entering settings, record the visit
|
|
85
|
+
useOnEnter('settings', () => {
|
|
86
|
+
setSearchQuery('');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// When leaving active state, persist preferences
|
|
90
|
+
useOnExit('active', () => {
|
|
91
|
+
setSelectedRecordId('');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Auto-refresh metrics while dashboard is active
|
|
95
|
+
useWhileIn('active', 30000, () => {
|
|
96
|
+
setLastRefreshed('now');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Track route changes for analytics
|
|
100
|
+
useOnChange('activeRoute', () => {
|
|
101
|
+
setSearchQuery('');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Track notification count changes
|
|
105
|
+
useOnChange('notificationCount', () => {
|
|
106
|
+
// Could trigger toast or badge update
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Listen for real-time updates from the backend
|
|
110
|
+
useOnEvent('data:updated', () => {
|
|
111
|
+
setLastRefreshed('now');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
useOnEvent('notification:received', () => {
|
|
115
|
+
setNotificationCount(notificationCount + 1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// --- Transitions ---
|
|
119
|
+
|
|
120
|
+
// Lifecycle transitions
|
|
121
|
+
useTransition('initialize', { from: 'draft', to: 'active' });
|
|
122
|
+
useTransition('suspend', { from: 'active', to: 'suspended' });
|
|
123
|
+
useTransition('resume', { from: 'suspended', to: 'active' });
|
|
124
|
+
useTransition('deactivate', { from: 'active', to: 'archived' });
|
|
125
|
+
|
|
126
|
+
// Route transitions (self-transitions that change activeRoute)
|
|
127
|
+
useTransition('navigate', { from: 'active', to: 'active' });
|
|
128
|
+
|
|
129
|
+
// Settings mode
|
|
130
|
+
useTransition('open-settings', { from: 'active', to: 'settings' });
|
|
131
|
+
useTransition('close-settings', { from: 'settings', to: 'active' });
|
|
132
|
+
|
|
133
|
+
// --- Derived state ---
|
|
134
|
+
const isOnDashboard = activeRoute === '/dashboard';
|
|
135
|
+
const isOnSettings = activeRoute === '/settings';
|
|
136
|
+
const isOnData = activeRoute === '/data';
|
|
137
|
+
const hasNotifications = notificationCount > 0;
|
|
138
|
+
|
|
139
|
+
// --- UI ---
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Stack className="dashboard-app" sx={{ height: '100vh', direction: 'row' }}>
|
|
143
|
+
{/* ---- Sidebar Navigation ---- */}
|
|
144
|
+
{sidebarOpen && (
|
|
145
|
+
<Stack sx={{ width: '240px', bg: 'gray.900', color: 'white', p: 4, gap: 2 }}>
|
|
146
|
+
<Heading sx={{ fontSize: 'lg', fontWeight: 'bold', mb: 4 }}>
|
|
147
|
+
MindMatrix
|
|
148
|
+
</Heading>
|
|
149
|
+
|
|
150
|
+
{/* Nav items */}
|
|
151
|
+
<Button
|
|
152
|
+
onClick={() => setActiveRoute('/dashboard')}
|
|
153
|
+
sx={{ variant: isOnDashboard ? 'solid' : 'ghost', colorScheme: 'blue', justifyContent: 'start' }}
|
|
154
|
+
>
|
|
155
|
+
Dashboard
|
|
156
|
+
</Button>
|
|
157
|
+
|
|
158
|
+
<Button
|
|
159
|
+
onClick={() => setActiveRoute('/data')}
|
|
160
|
+
sx={{ variant: isOnData ? 'solid' : 'ghost', colorScheme: 'blue', justifyContent: 'start' }}
|
|
161
|
+
>
|
|
162
|
+
Data Browser
|
|
163
|
+
</Button>
|
|
164
|
+
|
|
165
|
+
<Button
|
|
166
|
+
onClick={() => setActiveRoute('/settings')}
|
|
167
|
+
sx={{ variant: isOnSettings ? 'solid' : 'ghost', colorScheme: 'blue', justifyContent: 'start' }}
|
|
168
|
+
>
|
|
169
|
+
Settings
|
|
170
|
+
</Button>
|
|
171
|
+
|
|
172
|
+
<Spacer />
|
|
173
|
+
|
|
174
|
+
{/* Notification badge */}
|
|
175
|
+
{hasNotifications && (
|
|
176
|
+
<Badge sx={{ colorScheme: 'red' }}>
|
|
177
|
+
{notificationCount} new
|
|
178
|
+
</Badge>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{/* Admin-only: system status */}
|
|
182
|
+
{isAdmin && (
|
|
183
|
+
<Card sx={{ bg: 'gray.800', p: 2 }}>
|
|
184
|
+
<Text sx={{ fontSize: 'xs', color: 'gray.400' }}>System Status</Text>
|
|
185
|
+
<Text sx={{ fontSize: 'sm', color: 'green.400' }}>All services operational</Text>
|
|
186
|
+
</Card>
|
|
187
|
+
)}
|
|
188
|
+
</Stack>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* ---- Main Content Area ---- */}
|
|
192
|
+
<Stack sx={{ flex: 1, overflow: 'auto' }}>
|
|
193
|
+
{/* Top bar */}
|
|
194
|
+
<Stack sx={{ direction: 'row', alignItems: 'center', p: 4, borderBottom: '1px solid', borderColor: 'gray.200', gap: 4 }}>
|
|
195
|
+
<Button onClick={() => setSidebarOpen(!sidebarOpen)} sx={{ variant: 'ghost', size: 'sm' }}>
|
|
196
|
+
Menu
|
|
197
|
+
</Button>
|
|
198
|
+
|
|
199
|
+
<TextInput
|
|
200
|
+
bind="searchQuery"
|
|
201
|
+
placeholder="Search..."
|
|
202
|
+
sx={{ flex: 1, maxWidth: '400px' }}
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
<Spacer />
|
|
206
|
+
|
|
207
|
+
<Text sx={{ fontSize: 'sm', color: 'gray.500' }}>
|
|
208
|
+
Last refreshed: {lastRefreshed}
|
|
209
|
+
</Text>
|
|
210
|
+
|
|
211
|
+
{isAdmin && (
|
|
212
|
+
<Button onClick={() => setRefreshInterval(15000)} sx={{ variant: 'outline', size: 'sm' }}>
|
|
213
|
+
Turbo Refresh
|
|
214
|
+
</Button>
|
|
215
|
+
)}
|
|
216
|
+
</Stack>
|
|
217
|
+
|
|
218
|
+
{/* ---- Route: /dashboard ---- */}
|
|
219
|
+
{isOnDashboard && (
|
|
220
|
+
<Stack sx={{ p: 6, gap: 6 }}>
|
|
221
|
+
<Heading sx={{ fontSize: '2xl', fontWeight: 'bold' }}>
|
|
222
|
+
Dashboard Overview
|
|
223
|
+
</Heading>
|
|
224
|
+
|
|
225
|
+
{/* Metrics grid */}
|
|
226
|
+
<Grid sx={{ columns: 4, gap: 4 }}>
|
|
227
|
+
<Each items={metrics}>
|
|
228
|
+
{(metric: any) => (
|
|
229
|
+
<MetricCard
|
|
230
|
+
label={metric.name}
|
|
231
|
+
value={metric.value}
|
|
232
|
+
format={metric.format}
|
|
233
|
+
trend={metric.trend}
|
|
234
|
+
sparkline={true}
|
|
235
|
+
sx={{ bg: 'white', shadow: 'sm', rounded: 'lg', p: 4 }}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
</Each>
|
|
239
|
+
</Grid>
|
|
240
|
+
|
|
241
|
+
{/* Activity + Notifications side by side */}
|
|
242
|
+
<Stack sx={{ direction: 'row', gap: 6 }}>
|
|
243
|
+
{/* Recent activity */}
|
|
244
|
+
<Stack sx={{ flex: 2, gap: 3 }}>
|
|
245
|
+
<Heading sx={{ fontSize: 'lg', fontWeight: 'semibold' }}>
|
|
246
|
+
Recent Activity
|
|
247
|
+
</Heading>
|
|
248
|
+
<ScrollArea sx={{ maxHeight: '400px' }}>
|
|
249
|
+
<Each items={activities}>
|
|
250
|
+
{(activity: any) => (
|
|
251
|
+
<Stack sx={{ direction: 'row', gap: 3, py: 2, borderBottom: '1px solid', borderColor: 'gray.100' }}>
|
|
252
|
+
<Badge variant={activity.type === 'error' ? 'error' : 'default'}>
|
|
253
|
+
{activity.type}
|
|
254
|
+
</Badge>
|
|
255
|
+
<Stack sx={{ flex: 1 }}>
|
|
256
|
+
<Text sx={{ fontWeight: 'medium' }}>{activity.title}</Text>
|
|
257
|
+
<Text sx={{ fontSize: 'sm', color: 'gray.500' }}>{activity.description}</Text>
|
|
258
|
+
</Stack>
|
|
259
|
+
{isEditor && (
|
|
260
|
+
<Button sx={{ variant: 'ghost', size: 'sm' }}>
|
|
261
|
+
View
|
|
262
|
+
</Button>
|
|
263
|
+
)}
|
|
264
|
+
</Stack>
|
|
265
|
+
)}
|
|
266
|
+
</Each>
|
|
267
|
+
</ScrollArea>
|
|
268
|
+
</Stack>
|
|
269
|
+
|
|
270
|
+
{/* Notifications panel */}
|
|
271
|
+
<Stack sx={{ flex: 1, gap: 3 }}>
|
|
272
|
+
<Heading sx={{ fontSize: 'lg', fontWeight: 'semibold' }}>
|
|
273
|
+
Notifications
|
|
274
|
+
</Heading>
|
|
275
|
+
<Each items={notifications}>
|
|
276
|
+
{(notification: any) => (
|
|
277
|
+
<Card sx={{ p: 3, mb: 2 }}>
|
|
278
|
+
<Text sx={{ fontWeight: 'medium' }}>{notification.title}</Text>
|
|
279
|
+
<Text sx={{ fontSize: 'sm', color: 'gray.500' }}>{notification.message}</Text>
|
|
280
|
+
</Card>
|
|
281
|
+
)}
|
|
282
|
+
</Each>
|
|
283
|
+
{!notifications && (
|
|
284
|
+
<Text sx={{ color: 'gray.400', textAlign: 'center', py: 4 }}>
|
|
285
|
+
No new notifications
|
|
286
|
+
</Text>
|
|
287
|
+
)}
|
|
288
|
+
</Stack>
|
|
289
|
+
</Stack>
|
|
290
|
+
|
|
291
|
+
{/* Admin-only: system charts */}
|
|
292
|
+
{isAdmin && (
|
|
293
|
+
<Stack sx={{ gap: 4 }}>
|
|
294
|
+
<Heading sx={{ fontSize: 'lg', fontWeight: 'semibold' }}>
|
|
295
|
+
System Analytics
|
|
296
|
+
</Heading>
|
|
297
|
+
<Stack sx={{ direction: 'row', gap: 4 }}>
|
|
298
|
+
<Chart type="line" bind="metrics" xField="date" yField="value" sx={{ flex: 1, height: '300px' }} />
|
|
299
|
+
<Chart type="bar" bind="activities" xField="type" yField="count" sx={{ flex: 1, height: '300px' }} />
|
|
300
|
+
</Stack>
|
|
301
|
+
</Stack>
|
|
302
|
+
)}
|
|
303
|
+
</Stack>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
{/* ---- Route: /data ---- */}
|
|
307
|
+
{isOnData && (
|
|
308
|
+
<Stack sx={{ p: 6, gap: 4 }}>
|
|
309
|
+
<Stack sx={{ direction: 'row', alignItems: 'center', gap: 4 }}>
|
|
310
|
+
<Heading sx={{ fontSize: '2xl', fontWeight: 'bold' }}>
|
|
311
|
+
Data Browser
|
|
312
|
+
</Heading>
|
|
313
|
+
{isEditor && (
|
|
314
|
+
<Button sx={{ colorScheme: 'blue' }}>
|
|
315
|
+
New Definition
|
|
316
|
+
</Button>
|
|
317
|
+
)}
|
|
318
|
+
</Stack>
|
|
319
|
+
|
|
320
|
+
{/* Definition list with server grid */}
|
|
321
|
+
<ServerGrid
|
|
322
|
+
bind="definitions"
|
|
323
|
+
endpoint="/api/v1/workflow/definitions"
|
|
324
|
+
columns={[
|
|
325
|
+
{ field: 'name', header: 'Name', sortable: true },
|
|
326
|
+
{ field: 'category', header: 'Category', sortable: true },
|
|
327
|
+
{ field: 'version', header: 'Version', sortable: false },
|
|
328
|
+
{ field: 'updated_at', header: 'Last Updated', sortable: true },
|
|
329
|
+
]}
|
|
330
|
+
pageSize={25}
|
|
331
|
+
/>
|
|
332
|
+
|
|
333
|
+
{/* Viewer restriction notice */}
|
|
334
|
+
{isViewer && !isEditor && (
|
|
335
|
+
<Card sx={{ bg: 'yellow.50', p: 4 }}>
|
|
336
|
+
<Text sx={{ color: 'yellow.800' }}>
|
|
337
|
+
You have read-only access. Contact an admin to request edit permissions.
|
|
338
|
+
</Text>
|
|
339
|
+
</Card>
|
|
340
|
+
)}
|
|
341
|
+
</Stack>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
{/* ---- Route: /settings ---- */}
|
|
345
|
+
{isOnSettings && (
|
|
346
|
+
<Stack sx={{ p: 6, gap: 6, maxWidth: '800px' }}>
|
|
347
|
+
<Heading sx={{ fontSize: '2xl', fontWeight: 'bold' }}>
|
|
348
|
+
Settings
|
|
349
|
+
</Heading>
|
|
350
|
+
|
|
351
|
+
{/* General settings */}
|
|
352
|
+
<Section title="General" description="Configure your dashboard preferences">
|
|
353
|
+
<Stack sx={{ gap: 4 }}>
|
|
354
|
+
<Stack sx={{ direction: 'row', alignItems: 'center', gap: 4 }}>
|
|
355
|
+
<Text sx={{ width: '150px', fontWeight: 'medium' }}>Theme</Text>
|
|
356
|
+
<Select
|
|
357
|
+
bind="theme"
|
|
358
|
+
options={['light', 'dark', 'system']}
|
|
359
|
+
/>
|
|
360
|
+
</Stack>
|
|
361
|
+
|
|
362
|
+
<Stack sx={{ direction: 'row', alignItems: 'center', gap: 4 }}>
|
|
363
|
+
<Text sx={{ width: '150px', fontWeight: 'medium' }}>Refresh Rate</Text>
|
|
364
|
+
<Select
|
|
365
|
+
bind="refreshInterval"
|
|
366
|
+
options={['15000', '30000', '60000', '300000']}
|
|
367
|
+
/>
|
|
368
|
+
</Stack>
|
|
369
|
+
</Stack>
|
|
370
|
+
</Section>
|
|
371
|
+
|
|
372
|
+
{/* Admin-only settings */}
|
|
373
|
+
{isAdmin && (
|
|
374
|
+
<Section title="Administration" description="System-level configuration (admin only)">
|
|
375
|
+
<Stack sx={{ gap: 4 }}>
|
|
376
|
+
<Card sx={{ p: 4 }}>
|
|
377
|
+
<Stack sx={{ gap: 3 }}>
|
|
378
|
+
<Text sx={{ fontWeight: 'medium' }}>User Management</Text>
|
|
379
|
+
<Text sx={{ fontSize: 'sm', color: 'gray.500' }}>
|
|
380
|
+
Manage users, roles, and permissions for this workspace.
|
|
381
|
+
</Text>
|
|
382
|
+
<Button sx={{ colorScheme: 'blue', variant: 'outline' }}>
|
|
383
|
+
Manage Users
|
|
384
|
+
</Button>
|
|
385
|
+
</Stack>
|
|
386
|
+
</Card>
|
|
387
|
+
|
|
388
|
+
<Card sx={{ p: 4 }}>
|
|
389
|
+
<Stack sx={{ gap: 3 }}>
|
|
390
|
+
<Text sx={{ fontWeight: 'medium' }}>Data Retention</Text>
|
|
391
|
+
<Text sx={{ fontSize: 'sm', color: 'gray.500' }}>
|
|
392
|
+
Configure how long activity logs and archived records are retained.
|
|
393
|
+
</Text>
|
|
394
|
+
<Button sx={{ colorScheme: 'red', variant: 'outline' }}>
|
|
395
|
+
Configure Retention
|
|
396
|
+
</Button>
|
|
397
|
+
</Stack>
|
|
398
|
+
</Card>
|
|
399
|
+
</Stack>
|
|
400
|
+
</Section>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{/* Save button */}
|
|
404
|
+
{isEditor && (
|
|
405
|
+
<Stack sx={{ direction: 'row', justifyContent: 'end', gap: 2 }}>
|
|
406
|
+
<Button onClick={() => setActiveRoute('/dashboard')} sx={{ variant: 'outline' }}>
|
|
407
|
+
Cancel
|
|
408
|
+
</Button>
|
|
409
|
+
<Button sx={{ colorScheme: 'blue' }}>
|
|
410
|
+
Save Settings
|
|
411
|
+
</Button>
|
|
412
|
+
</Stack>
|
|
413
|
+
)}
|
|
414
|
+
</Stack>
|
|
415
|
+
)}
|
|
416
|
+
</Stack>
|
|
417
|
+
</Stack>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server actions — backend handlers for invoice approval transitions.
|
|
3
|
+
*
|
|
4
|
+
* These functions run server-side during state transitions.
|
|
5
|
+
* Each receives a TransitionContext with instance data and utilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TransitionContext } from '@mindmatrix/react';
|
|
9
|
+
|
|
10
|
+
/** Send approval notification to the invoice submitter. */
|
|
11
|
+
export async function sendApprovalNotification(ctx: TransitionContext): Promise<void> {
|
|
12
|
+
const { instance, env } = ctx;
|
|
13
|
+
await env.notify({
|
|
14
|
+
message: `Invoice #${instance.id} has been approved`,
|
|
15
|
+
instanceId: instance.id,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Also emit an event for audit trail
|
|
19
|
+
await env.emit('invoice:approved', {
|
|
20
|
+
instanceId: instance.id,
|
|
21
|
+
approvedBy: ctx.actor?.id,
|
|
22
|
+
approvedAt: new Date().toISOString(),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Send rejection notification with reason. */
|
|
27
|
+
export async function sendRejectionNotification(ctx: TransitionContext): Promise<void> {
|
|
28
|
+
const { instance, env } = ctx;
|
|
29
|
+
const reason = instance.state_data?.rejectionReason ?? 'No reason provided';
|
|
30
|
+
|
|
31
|
+
await env.notify({
|
|
32
|
+
message: `Invoice #${instance.id} was rejected: ${reason}`,
|
|
33
|
+
instanceId: instance.id,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await env.emit('invoice:rejected', {
|
|
37
|
+
instanceId: instance.id,
|
|
38
|
+
rejectedBy: ctx.actor?.id,
|
|
39
|
+
reason,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Validate invoice data before submission. */
|
|
44
|
+
export async function validateInvoice(ctx: TransitionContext): Promise<void> {
|
|
45
|
+
const { instance } = ctx;
|
|
46
|
+
const data = instance.state_data;
|
|
47
|
+
|
|
48
|
+
if (!data?.amount || data.amount <= 0) {
|
|
49
|
+
throw new Error('Invoice amount must be greater than zero');
|
|
50
|
+
}
|
|
51
|
+
if (!data?.vendor) {
|
|
52
|
+
throw new Error('Vendor name is required');
|
|
53
|
+
}
|
|
54
|
+
if (!data?.date) {
|
|
55
|
+
throw new Error('Invoice date is required');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Send webhook to accounting system on approval. */
|
|
60
|
+
export async function syncToAccounting(ctx: TransitionContext): Promise<void> {
|
|
61
|
+
const { instance } = ctx;
|
|
62
|
+
await fetch('https://api.example.com/accounting/invoices', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
externalId: instance.id,
|
|
67
|
+
amount: instance.state_data?.amount,
|
|
68
|
+
vendor: instance.state_data?.vendor,
|
|
69
|
+
approvedAt: new Date().toISOString(),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @workflow slug="invoice-approval" version="1.0.0" category="blueprint"
|
|
3
|
+
* @description Invoice approval workflow with multi-page views, data model, and server actions.
|
|
4
|
+
*
|
|
5
|
+
* States: draft → submitted → approved | rejected
|
|
6
|
+
* Pages: InvoiceList, InvoiceDetail, InvoiceForm
|
|
7
|
+
* Model: Invoice (amount, vendor, date, status, notes)
|
|
8
|
+
* Server Action: sendNotification (on approval/rejection)
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
Stack,
|
|
12
|
+
Row,
|
|
13
|
+
Heading,
|
|
14
|
+
Text,
|
|
15
|
+
Button,
|
|
16
|
+
Show,
|
|
17
|
+
useOnEnter,
|
|
18
|
+
useOnExit,
|
|
19
|
+
useTransition,
|
|
20
|
+
useRole,
|
|
21
|
+
} from '@mindmatrix/react';
|
|
22
|
+
import { useState } from 'react';
|
|
23
|
+
import type { Invoice } from './models/invoice';
|
|
24
|
+
import { sendApprovalNotification, sendRejectionNotification } from './actions/invoice.server';
|
|
25
|
+
import { InvoiceListPage } from './pages/InvoiceListPage';
|
|
26
|
+
import { InvoiceDetailPage } from './pages/InvoiceDetailPage';
|
|
27
|
+
import { InvoiceFormPage } from './pages/InvoiceFormPage';
|
|
28
|
+
|
|
29
|
+
export default function InvoiceApproval() {
|
|
30
|
+
// --- Fields ---
|
|
31
|
+
const [activeRoute, setActiveRoute] = useState('/invoices');
|
|
32
|
+
const [selectedInvoiceId, setSelectedInvoiceId] = useState('');
|
|
33
|
+
const [filterStatus, setFilterStatus] = useState('all');
|
|
34
|
+
const [totalPending, setTotalPending] = useState(0);
|
|
35
|
+
const [lastAction, setLastAction] = useState('');
|
|
36
|
+
|
|
37
|
+
// --- Roles ---
|
|
38
|
+
const isApprover = useRole('approver');
|
|
39
|
+
const isSubmitter = useRole('submitter');
|
|
40
|
+
|
|
41
|
+
// --- State Effects ---
|
|
42
|
+
useOnEnter('draft', () => {
|
|
43
|
+
setLastAction('created');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
useOnEnter('submitted', () => {
|
|
47
|
+
setLastAction('submitted');
|
|
48
|
+
setTotalPending(totalPending + 1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
useOnEnter('approved', () => {
|
|
52
|
+
setLastAction('approved');
|
|
53
|
+
setTotalPending(Math.max(0, totalPending - 1));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
useOnEnter('rejected', () => {
|
|
57
|
+
setLastAction('rejected');
|
|
58
|
+
setTotalPending(Math.max(0, totalPending - 1));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
useOnExit('draft', () => {
|
|
62
|
+
// Clear form state when leaving draft
|
|
63
|
+
setSelectedInvoiceId('');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// --- Transitions ---
|
|
67
|
+
const submit = useTransition('submit', {
|
|
68
|
+
from: 'draft',
|
|
69
|
+
to: 'submitted',
|
|
70
|
+
requiredFields: ['amount', 'vendor', 'date'],
|
|
71
|
+
roles: ['submitter'],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const approve = useTransition('approve', {
|
|
75
|
+
from: 'submitted',
|
|
76
|
+
to: 'approved',
|
|
77
|
+
roles: ['approver'],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const reject = useTransition('reject', {
|
|
81
|
+
from: 'submitted',
|
|
82
|
+
to: 'rejected',
|
|
83
|
+
roles: ['approver'],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const revise = useTransition('revise', {
|
|
87
|
+
from: 'rejected',
|
|
88
|
+
to: 'draft',
|
|
89
|
+
roles: ['submitter'],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// --- Navigation ---
|
|
93
|
+
const isOnList = activeRoute === '/invoices';
|
|
94
|
+
const isOnDetail = activeRoute === '/invoices/detail';
|
|
95
|
+
const isOnForm = activeRoute === '/invoices/new';
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Stack sx={{ minHeight: '100vh', bg: 'gray.50' }}>
|
|
99
|
+
{/* Header */}
|
|
100
|
+
<Row sx={{ alignItems: 'center', justifyContent: 'space-between', p: 16, bg: 'white', borderBottom: '1px solid', borderColor: 'gray.200' }}>
|
|
101
|
+
<Row sx={{ alignItems: 'center', gap: 12 }}>
|
|
102
|
+
<Heading sx={{ fontSize: 20, fontWeight: 'bold' }}>Invoice Approval</Heading>
|
|
103
|
+
<Show when={totalPending > 0}>
|
|
104
|
+
<Text sx={{ bg: 'orange.100', color: 'orange.800', px: 8, py: 2, rounded: 'full', fontSize: 12 }}>
|
|
105
|
+
{totalPending} pending
|
|
106
|
+
</Text>
|
|
107
|
+
</Show>
|
|
108
|
+
</Row>
|
|
109
|
+
<Row sx={{ gap: 8 }}>
|
|
110
|
+
<Button
|
|
111
|
+
onClick={() => setActiveRoute('/invoices')}
|
|
112
|
+
sx={{ variant: isOnList ? 'solid' : 'ghost' }}
|
|
113
|
+
>
|
|
114
|
+
All Invoices
|
|
115
|
+
</Button>
|
|
116
|
+
<Show when={isSubmitter}>
|
|
117
|
+
<Button
|
|
118
|
+
onClick={() => setActiveRoute('/invoices/new')}
|
|
119
|
+
sx={{ variant: isOnForm ? 'solid' : 'ghost', colorScheme: 'blue' }}
|
|
120
|
+
>
|
|
121
|
+
New Invoice
|
|
122
|
+
</Button>
|
|
123
|
+
</Show>
|
|
124
|
+
</Row>
|
|
125
|
+
</Row>
|
|
126
|
+
|
|
127
|
+
{/* Route Pages */}
|
|
128
|
+
<Show when={isOnList}>
|
|
129
|
+
<InvoiceListPage
|
|
130
|
+
filterStatus={filterStatus}
|
|
131
|
+
onFilterChange={setFilterStatus}
|
|
132
|
+
onSelect={(id: string) => {
|
|
133
|
+
setSelectedInvoiceId(id);
|
|
134
|
+
setActiveRoute('/invoices/detail');
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
</Show>
|
|
138
|
+
|
|
139
|
+
<Show when={isOnDetail}>
|
|
140
|
+
<InvoiceDetailPage
|
|
141
|
+
invoiceId={selectedInvoiceId}
|
|
142
|
+
isApprover={isApprover}
|
|
143
|
+
onApprove={() => approve.fire()}
|
|
144
|
+
onReject={() => reject.fire()}
|
|
145
|
+
onRevise={() => revise.fire()}
|
|
146
|
+
onBack={() => setActiveRoute('/invoices')}
|
|
147
|
+
/>
|
|
148
|
+
</Show>
|
|
149
|
+
|
|
150
|
+
<Show when={isOnForm}>
|
|
151
|
+
<InvoiceFormPage
|
|
152
|
+
onSubmit={() => {
|
|
153
|
+
submit.fire();
|
|
154
|
+
setActiveRoute('/invoices');
|
|
155
|
+
}}
|
|
156
|
+
onCancel={() => setActiveRoute('/invoices')}
|
|
157
|
+
/>
|
|
158
|
+
</Show>
|
|
159
|
+
|
|
160
|
+
{/* Status bar */}
|
|
161
|
+
<Row sx={{ p: 8, bg: 'gray.100', justifyContent: 'end', gap: 8, mt: 'auto' }}>
|
|
162
|
+
<Text sx={{ fontSize: 11, color: 'gray.500' }}>
|
|
163
|
+
Last action: {lastAction || 'none'}
|
|
164
|
+
</Text>
|
|
165
|
+
</Row>
|
|
166
|
+
</Stack>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint configuration for "Invoice Approval".
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineBlueprint } from '@mindmatrix/react';
|
|
6
|
+
|
|
7
|
+
export default defineBlueprint({
|
|
8
|
+
slug: 'invoice-approval',
|
|
9
|
+
name: 'Invoice Approval',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
category: 'blueprint',
|
|
12
|
+
description: 'Invoice approval workflow with multi-page views, data model, and server actions.',
|
|
13
|
+
roles: [
|
|
14
|
+
{ name: 'submitter', permissions: ['view', 'edit', 'transition'] },
|
|
15
|
+
{ name: 'approver', permissions: ['view', 'transition'] },
|
|
16
|
+
{ name: 'admin', permissions: ['view', 'edit', 'transition', 'delete'] },
|
|
17
|
+
],
|
|
18
|
+
});
|