@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,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invoice — data model interface.
|
|
3
|
+
*
|
|
4
|
+
* Auto-generated from workflow definition "invoice-approval".
|
|
5
|
+
* Fields: 8
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface Invoice {
|
|
9
|
+
/** Invoice amount in dollars */
|
|
10
|
+
amount: number;
|
|
11
|
+
/** Vendor or supplier name */
|
|
12
|
+
vendor: string;
|
|
13
|
+
/** Invoice date */
|
|
14
|
+
date: Date;
|
|
15
|
+
/** Current approval status */
|
|
16
|
+
status: string;
|
|
17
|
+
/** Optional notes or comments */
|
|
18
|
+
notes?: string;
|
|
19
|
+
/** Department the invoice belongs to */
|
|
20
|
+
department?: string;
|
|
21
|
+
/** Purchase order reference */
|
|
22
|
+
poNumber?: string;
|
|
23
|
+
/** Line items breakdown */
|
|
24
|
+
lineItems?: InvoiceLineItem[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface InvoiceLineItem {
|
|
28
|
+
description: string;
|
|
29
|
+
quantity: number;
|
|
30
|
+
unitPrice: number;
|
|
31
|
+
total: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export enum InvoiceState {
|
|
35
|
+
Draft = 'draft',
|
|
36
|
+
Submitted = 'submitted',
|
|
37
|
+
Approved = 'approved',
|
|
38
|
+
Rejected = 'rejected',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export enum InvoicePriority {
|
|
42
|
+
Low = 'low',
|
|
43
|
+
Normal = 'normal',
|
|
44
|
+
High = 'high',
|
|
45
|
+
Urgent = 'urgent',
|
|
46
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InvoiceDetailPage — detail view for a single invoice.
|
|
3
|
+
*
|
|
4
|
+
* Shows invoice data, approval history, and action buttons
|
|
5
|
+
* for approvers (approve/reject) and submitters (revise).
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
Stack,
|
|
9
|
+
Row,
|
|
10
|
+
Heading,
|
|
11
|
+
Text,
|
|
12
|
+
Button,
|
|
13
|
+
Badge,
|
|
14
|
+
Card,
|
|
15
|
+
Section,
|
|
16
|
+
Show,
|
|
17
|
+
Each,
|
|
18
|
+
Divider,
|
|
19
|
+
useQuery,
|
|
20
|
+
} from '@mindmatrix/react';
|
|
21
|
+
import type { Invoice, InvoiceLineItem } from '../models/invoice';
|
|
22
|
+
|
|
23
|
+
interface InvoiceDetailPageProps {
|
|
24
|
+
invoiceId: string;
|
|
25
|
+
isApprover: boolean;
|
|
26
|
+
onApprove: () => void;
|
|
27
|
+
onReject: () => void;
|
|
28
|
+
onRevise: () => void;
|
|
29
|
+
onBack: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function InvoiceDetailPage({
|
|
33
|
+
invoiceId,
|
|
34
|
+
isApprover,
|
|
35
|
+
onApprove,
|
|
36
|
+
onReject,
|
|
37
|
+
onRevise,
|
|
38
|
+
onBack,
|
|
39
|
+
}: InvoiceDetailPageProps) {
|
|
40
|
+
// Fetch the specific invoice
|
|
41
|
+
const { data: invoice, loading } = useQuery('invoice-approval', {
|
|
42
|
+
filter: { id: invoiceId },
|
|
43
|
+
limit: 1,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const inv = invoice?.[0] as (Invoice & { id: string }) | undefined;
|
|
47
|
+
|
|
48
|
+
const statusColor = (status: string) => {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 'draft': return 'gray';
|
|
51
|
+
case 'submitted': return 'blue';
|
|
52
|
+
case 'approved': return 'green';
|
|
53
|
+
case 'rejected': return 'red';
|
|
54
|
+
default: return 'gray';
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Stack sx={{ p: 24, gap: 16, flex: 1, maxWidth: 800, mx: 'auto' }}>
|
|
60
|
+
{/* Back button */}
|
|
61
|
+
<Button onClick={onBack} sx={{ variant: 'ghost', alignSelf: 'start' }}>
|
|
62
|
+
← Back to Invoices
|
|
63
|
+
</Button>
|
|
64
|
+
|
|
65
|
+
<Show when={loading}>
|
|
66
|
+
<Text sx={{ color: 'gray.400', textAlign: 'center', py: 16 }}>Loading invoice...</Text>
|
|
67
|
+
</Show>
|
|
68
|
+
|
|
69
|
+
<Show when={!loading && !!inv}>
|
|
70
|
+
{/* Invoice header */}
|
|
71
|
+
<Card sx={{ p: 20 }}>
|
|
72
|
+
<Row sx={{ alignItems: 'start', justifyContent: 'space-between', mb: 16 }}>
|
|
73
|
+
<Stack sx={{ gap: 4 }}>
|
|
74
|
+
<Heading sx={{ fontSize: 22, fontWeight: 'bold' }}>
|
|
75
|
+
Invoice #{invoiceId.slice(0, 8)}
|
|
76
|
+
</Heading>
|
|
77
|
+
<Text sx={{ color: 'gray.500', fontSize: 13 }}>
|
|
78
|
+
{inv?.vendor}
|
|
79
|
+
</Text>
|
|
80
|
+
</Stack>
|
|
81
|
+
<Badge variant={statusColor(inv?.status ?? 'draft')} sx={{ fontSize: 14, px: 12, py: 4 }}>
|
|
82
|
+
{inv?.status}
|
|
83
|
+
</Badge>
|
|
84
|
+
</Row>
|
|
85
|
+
|
|
86
|
+
<Divider sx={{ my: 16 }} />
|
|
87
|
+
|
|
88
|
+
{/* Invoice details grid */}
|
|
89
|
+
<Section title="Details">
|
|
90
|
+
<Row sx={{ gap: 24, flexWrap: 'wrap' }}>
|
|
91
|
+
<Stack sx={{ gap: 2, minWidth: 150 }}>
|
|
92
|
+
<Text sx={{ fontSize: 11, color: 'gray.500', textTransform: 'uppercase' }}>Amount</Text>
|
|
93
|
+
<Text sx={{ fontSize: 28, fontWeight: 'bold', color: 'gray.900' }}>
|
|
94
|
+
${inv?.amount?.toLocaleString() ?? '0'}
|
|
95
|
+
</Text>
|
|
96
|
+
</Stack>
|
|
97
|
+
|
|
98
|
+
<Stack sx={{ gap: 2, minWidth: 150 }}>
|
|
99
|
+
<Text sx={{ fontSize: 11, color: 'gray.500', textTransform: 'uppercase' }}>Date</Text>
|
|
100
|
+
<Text sx={{ fontSize: 16 }}>
|
|
101
|
+
{inv?.date ? new Date(inv.date).toLocaleDateString() : '—'}
|
|
102
|
+
</Text>
|
|
103
|
+
</Stack>
|
|
104
|
+
|
|
105
|
+
<Stack sx={{ gap: 2, minWidth: 150 }}>
|
|
106
|
+
<Text sx={{ fontSize: 11, color: 'gray.500', textTransform: 'uppercase' }}>Department</Text>
|
|
107
|
+
<Text sx={{ fontSize: 16 }}>{inv?.department ?? '—'}</Text>
|
|
108
|
+
</Stack>
|
|
109
|
+
|
|
110
|
+
<Stack sx={{ gap: 2, minWidth: 150 }}>
|
|
111
|
+
<Text sx={{ fontSize: 11, color: 'gray.500', textTransform: 'uppercase' }}>PO Number</Text>
|
|
112
|
+
<Text sx={{ fontSize: 16 }}>{inv?.poNumber ?? '—'}</Text>
|
|
113
|
+
</Stack>
|
|
114
|
+
</Row>
|
|
115
|
+
</Section>
|
|
116
|
+
|
|
117
|
+
{/* Line items */}
|
|
118
|
+
<Show when={inv?.lineItems && inv.lineItems.length > 0}>
|
|
119
|
+
<Section title="Line Items">
|
|
120
|
+
<Stack sx={{ gap: 4 }}>
|
|
121
|
+
<Row sx={{ fontWeight: 'semibold', fontSize: 12, color: 'gray.500', p: 8 }}>
|
|
122
|
+
<Text sx={{ flex: 3 }}>Description</Text>
|
|
123
|
+
<Text sx={{ flex: 1, textAlign: 'right' }}>Qty</Text>
|
|
124
|
+
<Text sx={{ flex: 1, textAlign: 'right' }}>Unit Price</Text>
|
|
125
|
+
<Text sx={{ flex: 1, textAlign: 'right' }}>Total</Text>
|
|
126
|
+
</Row>
|
|
127
|
+
<Each items={inv?.lineItems ?? []}>
|
|
128
|
+
{(item: InvoiceLineItem) => (
|
|
129
|
+
<Row sx={{ p: 8, borderBottom: '1px solid', borderColor: 'gray.50' }}>
|
|
130
|
+
<Text sx={{ flex: 3 }}>{item.description}</Text>
|
|
131
|
+
<Text sx={{ flex: 1, textAlign: 'right' }}>{item.quantity}</Text>
|
|
132
|
+
<Text sx={{ flex: 1, textAlign: 'right' }}>${item.unitPrice}</Text>
|
|
133
|
+
<Text sx={{ flex: 1, textAlign: 'right', fontWeight: 'medium' }}>
|
|
134
|
+
${item.total}
|
|
135
|
+
</Text>
|
|
136
|
+
</Row>
|
|
137
|
+
)}
|
|
138
|
+
</Each>
|
|
139
|
+
</Stack>
|
|
140
|
+
</Section>
|
|
141
|
+
</Show>
|
|
142
|
+
|
|
143
|
+
{/* Notes */}
|
|
144
|
+
<Show when={!!inv?.notes}>
|
|
145
|
+
<Section title="Notes">
|
|
146
|
+
<Text sx={{ color: 'gray.700', lineHeight: 1.6 }}>{inv?.notes}</Text>
|
|
147
|
+
</Section>
|
|
148
|
+
</Show>
|
|
149
|
+
</Card>
|
|
150
|
+
|
|
151
|
+
{/* Action buttons */}
|
|
152
|
+
<Card sx={{ p: 16 }}>
|
|
153
|
+
<Row sx={{ justifyContent: 'end', gap: 8 }}>
|
|
154
|
+
{/* Approver actions */}
|
|
155
|
+
<Show when={isApprover && inv?.status === 'submitted'}>
|
|
156
|
+
<Button onClick={onReject} sx={{ variant: 'outline', colorScheme: 'red' }}>
|
|
157
|
+
Reject
|
|
158
|
+
</Button>
|
|
159
|
+
<Button onClick={onApprove} sx={{ colorScheme: 'green' }}>
|
|
160
|
+
Approve
|
|
161
|
+
</Button>
|
|
162
|
+
</Show>
|
|
163
|
+
|
|
164
|
+
{/* Submitter: revise rejected invoice */}
|
|
165
|
+
<Show when={inv?.status === 'rejected'}>
|
|
166
|
+
<Button onClick={onRevise} sx={{ colorScheme: 'blue' }}>
|
|
167
|
+
Revise & Resubmit
|
|
168
|
+
</Button>
|
|
169
|
+
</Show>
|
|
170
|
+
</Row>
|
|
171
|
+
</Card>
|
|
172
|
+
</Show>
|
|
173
|
+
</Stack>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InvoiceFormPage — form view for creating/editing an invoice.
|
|
3
|
+
*
|
|
4
|
+
* Provides validated input fields for all invoice data,
|
|
5
|
+
* line item management, and submit/cancel actions.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
Stack,
|
|
9
|
+
Row,
|
|
10
|
+
Heading,
|
|
11
|
+
Text,
|
|
12
|
+
Button,
|
|
13
|
+
TextInput,
|
|
14
|
+
Select,
|
|
15
|
+
Card,
|
|
16
|
+
Section,
|
|
17
|
+
Show,
|
|
18
|
+
Each,
|
|
19
|
+
Divider,
|
|
20
|
+
} from '@mindmatrix/react';
|
|
21
|
+
import { useState } from 'react';
|
|
22
|
+
import type { InvoiceLineItem } from '../models/invoice';
|
|
23
|
+
|
|
24
|
+
interface InvoiceFormPageProps {
|
|
25
|
+
onSubmit: () => void;
|
|
26
|
+
onCancel: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function InvoiceFormPage({ onSubmit, onCancel }: InvoiceFormPageProps) {
|
|
30
|
+
// Form state
|
|
31
|
+
const [vendor, setVendor] = useState('');
|
|
32
|
+
const [amount, setAmount] = useState(0);
|
|
33
|
+
const [date, setDate] = useState('');
|
|
34
|
+
const [department, setDepartment] = useState('');
|
|
35
|
+
const [poNumber, setPoNumber] = useState('');
|
|
36
|
+
const [notes, setNotes] = useState('');
|
|
37
|
+
const [lineItems, setLineItems] = useState<InvoiceLineItem[]>([]);
|
|
38
|
+
const [errors, setErrors] = useState<string[]>([]);
|
|
39
|
+
|
|
40
|
+
// Computed total from line items
|
|
41
|
+
const lineItemTotal = lineItems.reduce((sum, item) => sum + item.total, 0);
|
|
42
|
+
const displayAmount = lineItems.length > 0 ? lineItemTotal : amount;
|
|
43
|
+
|
|
44
|
+
// Validation
|
|
45
|
+
const validate = (): boolean => {
|
|
46
|
+
const errs: string[] = [];
|
|
47
|
+
if (!vendor.trim()) errs.push('Vendor name is required');
|
|
48
|
+
if (displayAmount <= 0) errs.push('Amount must be greater than zero');
|
|
49
|
+
if (!date) errs.push('Invoice date is required');
|
|
50
|
+
setErrors(errs);
|
|
51
|
+
return errs.length === 0;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleSubmit = () => {
|
|
55
|
+
if (validate()) {
|
|
56
|
+
onSubmit();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Line item management
|
|
61
|
+
const addLineItem = () => {
|
|
62
|
+
setLineItems([...lineItems, { description: '', quantity: 1, unitPrice: 0, total: 0 }]);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const removeLineItem = (index: number) => {
|
|
66
|
+
setLineItems(lineItems.filter((_, i) => i !== index));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Stack sx={{ p: 24, gap: 16, flex: 1, maxWidth: 700, mx: 'auto' }}>
|
|
71
|
+
<Heading sx={{ fontSize: 22, fontWeight: 'bold' }}>New Invoice</Heading>
|
|
72
|
+
|
|
73
|
+
{/* Validation errors */}
|
|
74
|
+
<Show when={errors.length > 0}>
|
|
75
|
+
<Card sx={{ p: 12, bg: 'red.50', borderColor: 'red.200' }}>
|
|
76
|
+
<Each items={errors}>
|
|
77
|
+
{(error: string) => (
|
|
78
|
+
<Text sx={{ color: 'red.700', fontSize: 13 }}>• {error}</Text>
|
|
79
|
+
)}
|
|
80
|
+
</Each>
|
|
81
|
+
</Card>
|
|
82
|
+
</Show>
|
|
83
|
+
|
|
84
|
+
{/* Basic Info */}
|
|
85
|
+
<Card sx={{ p: 20 }}>
|
|
86
|
+
<Section title="Invoice Details">
|
|
87
|
+
<Stack sx={{ gap: 16 }}>
|
|
88
|
+
<Stack sx={{ gap: 4 }}>
|
|
89
|
+
<Text sx={{ fontSize: 13, fontWeight: 'medium' }}>Vendor *</Text>
|
|
90
|
+
<TextInput
|
|
91
|
+
bind="vendor"
|
|
92
|
+
placeholder="Enter vendor name"
|
|
93
|
+
sx={{ width: '100%' }}
|
|
94
|
+
/>
|
|
95
|
+
</Stack>
|
|
96
|
+
|
|
97
|
+
<Row sx={{ gap: 16 }}>
|
|
98
|
+
<Stack sx={{ gap: 4, flex: 1 }}>
|
|
99
|
+
<Text sx={{ fontSize: 13, fontWeight: 'medium' }}>Amount *</Text>
|
|
100
|
+
<TextInput
|
|
101
|
+
bind="amount"
|
|
102
|
+
placeholder="0.00"
|
|
103
|
+
sx={{ width: '100%' }}
|
|
104
|
+
/>
|
|
105
|
+
</Stack>
|
|
106
|
+
<Stack sx={{ gap: 4, flex: 1 }}>
|
|
107
|
+
<Text sx={{ fontSize: 13, fontWeight: 'medium' }}>Date *</Text>
|
|
108
|
+
<TextInput
|
|
109
|
+
bind="date"
|
|
110
|
+
placeholder="YYYY-MM-DD"
|
|
111
|
+
sx={{ width: '100%' }}
|
|
112
|
+
/>
|
|
113
|
+
</Stack>
|
|
114
|
+
</Row>
|
|
115
|
+
|
|
116
|
+
<Row sx={{ gap: 16 }}>
|
|
117
|
+
<Stack sx={{ gap: 4, flex: 1 }}>
|
|
118
|
+
<Text sx={{ fontSize: 13, fontWeight: 'medium' }}>Department</Text>
|
|
119
|
+
<Select
|
|
120
|
+
bind="department"
|
|
121
|
+
options={['Engineering', 'Marketing', 'Sales', 'Operations', 'Finance', 'HR']}
|
|
122
|
+
sx={{ width: '100%' }}
|
|
123
|
+
/>
|
|
124
|
+
</Stack>
|
|
125
|
+
<Stack sx={{ gap: 4, flex: 1 }}>
|
|
126
|
+
<Text sx={{ fontSize: 13, fontWeight: 'medium' }}>PO Number</Text>
|
|
127
|
+
<TextInput
|
|
128
|
+
bind="poNumber"
|
|
129
|
+
placeholder="PO-XXXX"
|
|
130
|
+
sx={{ width: '100%' }}
|
|
131
|
+
/>
|
|
132
|
+
</Stack>
|
|
133
|
+
</Row>
|
|
134
|
+
</Stack>
|
|
135
|
+
</Section>
|
|
136
|
+
</Card>
|
|
137
|
+
|
|
138
|
+
{/* Line Items */}
|
|
139
|
+
<Card sx={{ p: 20 }}>
|
|
140
|
+
<Section title="Line Items">
|
|
141
|
+
<Stack sx={{ gap: 8 }}>
|
|
142
|
+
<Show when={lineItems.length > 0}>
|
|
143
|
+
<Row sx={{ fontWeight: 'semibold', fontSize: 12, color: 'gray.500', px: 4 }}>
|
|
144
|
+
<Text sx={{ flex: 3 }}>Description</Text>
|
|
145
|
+
<Text sx={{ flex: 1 }}>Qty</Text>
|
|
146
|
+
<Text sx={{ flex: 1 }}>Unit Price</Text>
|
|
147
|
+
<Text sx={{ flex: 1 }}>Total</Text>
|
|
148
|
+
<Text sx={{ width: 32 }} />
|
|
149
|
+
</Row>
|
|
150
|
+
<Each items={lineItems}>
|
|
151
|
+
{(item: InvoiceLineItem, index: number) => (
|
|
152
|
+
<Row sx={{ gap: 8, alignItems: 'center' }}>
|
|
153
|
+
<TextInput bind={`lineItems[${index}].description`} placeholder="Description" sx={{ flex: 3 }} />
|
|
154
|
+
<TextInput bind={`lineItems[${index}].quantity`} placeholder="1" sx={{ flex: 1 }} />
|
|
155
|
+
<TextInput bind={`lineItems[${index}].unitPrice`} placeholder="0.00" sx={{ flex: 1 }} />
|
|
156
|
+
<Text sx={{ flex: 1, fontWeight: 'medium' }}>${item.total}</Text>
|
|
157
|
+
<Button onClick={() => removeLineItem(index)} sx={{ variant: 'ghost', size: 'sm', color: 'red.500' }}>
|
|
158
|
+
✕
|
|
159
|
+
</Button>
|
|
160
|
+
</Row>
|
|
161
|
+
)}
|
|
162
|
+
</Each>
|
|
163
|
+
<Divider sx={{ my: 8 }} />
|
|
164
|
+
<Row sx={{ justifyContent: 'end', px: 4 }}>
|
|
165
|
+
<Text sx={{ fontWeight: 'bold' }}>Total: ${lineItemTotal.toLocaleString()}</Text>
|
|
166
|
+
</Row>
|
|
167
|
+
</Show>
|
|
168
|
+
|
|
169
|
+
<Button onClick={addLineItem} sx={{ variant: 'outline', size: 'sm', alignSelf: 'start' }}>
|
|
170
|
+
+ Add Line Item
|
|
171
|
+
</Button>
|
|
172
|
+
</Stack>
|
|
173
|
+
</Section>
|
|
174
|
+
</Card>
|
|
175
|
+
|
|
176
|
+
{/* Notes */}
|
|
177
|
+
<Card sx={{ p: 20 }}>
|
|
178
|
+
<Section title="Notes">
|
|
179
|
+
<TextInput
|
|
180
|
+
bind="notes"
|
|
181
|
+
placeholder="Add any notes or context for the approver..."
|
|
182
|
+
sx={{ width: '100%', minHeight: 80 }}
|
|
183
|
+
/>
|
|
184
|
+
</Section>
|
|
185
|
+
</Card>
|
|
186
|
+
|
|
187
|
+
{/* Actions */}
|
|
188
|
+
<Row sx={{ justifyContent: 'end', gap: 8, pt: 8 }}>
|
|
189
|
+
<Button onClick={onCancel} sx={{ variant: 'outline' }}>
|
|
190
|
+
Cancel
|
|
191
|
+
</Button>
|
|
192
|
+
<Button onClick={handleSubmit} sx={{ colorScheme: 'blue' }}>
|
|
193
|
+
Submit for Approval
|
|
194
|
+
</Button>
|
|
195
|
+
</Row>
|
|
196
|
+
</Stack>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InvoiceListPage — list view of all invoices with filtering.
|
|
3
|
+
*
|
|
4
|
+
* Displays a data grid of invoices with status filtering,
|
|
5
|
+
* search, and click-to-select for detail navigation.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
Stack,
|
|
9
|
+
Row,
|
|
10
|
+
Heading,
|
|
11
|
+
Text,
|
|
12
|
+
Button,
|
|
13
|
+
Select,
|
|
14
|
+
TextInput,
|
|
15
|
+
DataGrid,
|
|
16
|
+
Badge,
|
|
17
|
+
Show,
|
|
18
|
+
Each,
|
|
19
|
+
Card,
|
|
20
|
+
useQuery,
|
|
21
|
+
} from '@mindmatrix/react';
|
|
22
|
+
import { useState } from 'react';
|
|
23
|
+
import type { Invoice, InvoiceState } from '../models/invoice';
|
|
24
|
+
|
|
25
|
+
interface InvoiceListPageProps {
|
|
26
|
+
filterStatus: string;
|
|
27
|
+
onFilterChange: (status: string) => void;
|
|
28
|
+
onSelect: (id: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function InvoiceListPage({ filterStatus, onFilterChange, onSelect }: InvoiceListPageProps) {
|
|
32
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
33
|
+
|
|
34
|
+
// Query invoices from the data API
|
|
35
|
+
const { data: invoices, loading } = useQuery('invoice-approval', {
|
|
36
|
+
limit: 50,
|
|
37
|
+
orderBy: 'date',
|
|
38
|
+
order: 'desc',
|
|
39
|
+
filter: filterStatus !== 'all' ? { status: filterStatus } : undefined,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Status badge color mapping
|
|
43
|
+
const statusColor = (status: string) => {
|
|
44
|
+
switch (status) {
|
|
45
|
+
case 'draft': return 'gray';
|
|
46
|
+
case 'submitted': return 'blue';
|
|
47
|
+
case 'approved': return 'green';
|
|
48
|
+
case 'rejected': return 'red';
|
|
49
|
+
default: return 'gray';
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Stack sx={{ p: 24, gap: 16, flex: 1 }}>
|
|
55
|
+
{/* Toolbar */}
|
|
56
|
+
<Row sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
|
|
57
|
+
<Heading sx={{ fontSize: 18, fontWeight: 'semibold' }}>All Invoices</Heading>
|
|
58
|
+
<Row sx={{ gap: 8 }}>
|
|
59
|
+
<TextInput
|
|
60
|
+
bind="searchQuery"
|
|
61
|
+
placeholder="Search vendor..."
|
|
62
|
+
sx={{ width: 200 }}
|
|
63
|
+
/>
|
|
64
|
+
<Select
|
|
65
|
+
bind="filterStatus"
|
|
66
|
+
options={['all', 'draft', 'submitted', 'approved', 'rejected']}
|
|
67
|
+
sx={{ width: 140 }}
|
|
68
|
+
/>
|
|
69
|
+
</Row>
|
|
70
|
+
</Row>
|
|
71
|
+
|
|
72
|
+
{/* Summary cards */}
|
|
73
|
+
<Row sx={{ gap: 12 }}>
|
|
74
|
+
<Card sx={{ flex: 1, p: 12 }}>
|
|
75
|
+
<Text sx={{ fontSize: 11, color: 'gray.500', textTransform: 'uppercase' }}>Total</Text>
|
|
76
|
+
<Text sx={{ fontSize: 24, fontWeight: 'bold' }}>{invoices?.length ?? 0}</Text>
|
|
77
|
+
</Card>
|
|
78
|
+
<Card sx={{ flex: 1, p: 12 }}>
|
|
79
|
+
<Text sx={{ fontSize: 11, color: 'gray.500', textTransform: 'uppercase' }}>Pending</Text>
|
|
80
|
+
<Text sx={{ fontSize: 24, fontWeight: 'bold', color: 'blue.600' }}>
|
|
81
|
+
{invoices?.filter((i: Invoice) => i.status === 'submitted').length ?? 0}
|
|
82
|
+
</Text>
|
|
83
|
+
</Card>
|
|
84
|
+
<Card sx={{ flex: 1, p: 12 }}>
|
|
85
|
+
<Text sx={{ fontSize: 11, color: 'gray.500', textTransform: 'uppercase' }}>Approved</Text>
|
|
86
|
+
<Text sx={{ fontSize: 24, fontWeight: 'bold', color: 'green.600' }}>
|
|
87
|
+
{invoices?.filter((i: Invoice) => i.status === 'approved').length ?? 0}
|
|
88
|
+
</Text>
|
|
89
|
+
</Card>
|
|
90
|
+
</Row>
|
|
91
|
+
|
|
92
|
+
{/* Invoice list */}
|
|
93
|
+
<Show when={loading}>
|
|
94
|
+
<Text sx={{ color: 'gray.400', textAlign: 'center', py: 16 }}>Loading invoices...</Text>
|
|
95
|
+
</Show>
|
|
96
|
+
|
|
97
|
+
<Show when={!loading}>
|
|
98
|
+
<Stack sx={{ gap: 4, bg: 'white', rounded: 8, border: '1px solid', borderColor: 'gray.200' }}>
|
|
99
|
+
{/* Header row */}
|
|
100
|
+
<Row sx={{ p: 12, borderBottom: '1px solid', borderColor: 'gray.100', fontWeight: 'semibold', fontSize: 12, color: 'gray.500' }}>
|
|
101
|
+
<Text sx={{ flex: 2 }}>Vendor</Text>
|
|
102
|
+
<Text sx={{ flex: 1 }}>Amount</Text>
|
|
103
|
+
<Text sx={{ flex: 1 }}>Date</Text>
|
|
104
|
+
<Text sx={{ flex: 1 }}>Status</Text>
|
|
105
|
+
<Text sx={{ width: 80 }}>Action</Text>
|
|
106
|
+
</Row>
|
|
107
|
+
|
|
108
|
+
{/* Data rows */}
|
|
109
|
+
<Each items={invoices}>
|
|
110
|
+
{(invoice: Invoice & { id: string }) => (
|
|
111
|
+
<Row
|
|
112
|
+
sx={{
|
|
113
|
+
p: 12,
|
|
114
|
+
borderBottom: '1px solid',
|
|
115
|
+
borderColor: 'gray.50',
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
cursor: 'pointer',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<Text sx={{ flex: 2, fontWeight: 'medium' }}>{invoice.vendor}</Text>
|
|
121
|
+
<Text sx={{ flex: 1 }}>${invoice.amount.toLocaleString()}</Text>
|
|
122
|
+
<Text sx={{ flex: 1, color: 'gray.600', fontSize: 13 }}>
|
|
123
|
+
{new Date(invoice.date).toLocaleDateString()}
|
|
124
|
+
</Text>
|
|
125
|
+
<Stack sx={{ flex: 1 }}>
|
|
126
|
+
<Badge variant={statusColor(invoice.status)}>{invoice.status}</Badge>
|
|
127
|
+
</Stack>
|
|
128
|
+
<Button
|
|
129
|
+
onClick={() => onSelect(invoice.id)}
|
|
130
|
+
sx={{ variant: 'ghost', size: 'sm', width: 80 }}
|
|
131
|
+
>
|
|
132
|
+
View
|
|
133
|
+
</Button>
|
|
134
|
+
</Row>
|
|
135
|
+
)}
|
|
136
|
+
</Each>
|
|
137
|
+
</Stack>
|
|
138
|
+
</Show>
|
|
139
|
+
</Stack>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @workflow slug="todo-app" version="1.0.0" category="data"
|
|
3
|
+
* @description Todo list with add, remove, toggle — demonstrates data source binding,
|
|
4
|
+
* conditional rendering, list mapping, and role-based guards.
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
useTransition,
|
|
9
|
+
useOnEnter,
|
|
10
|
+
useOnExit,
|
|
11
|
+
useQuery,
|
|
12
|
+
useMutation,
|
|
13
|
+
useRole,
|
|
14
|
+
useWhileIn,
|
|
15
|
+
} from '@mindmatrix/react';
|
|
16
|
+
|
|
17
|
+
export function TodoApp() {
|
|
18
|
+
const [title, setTitle] = useState('');
|
|
19
|
+
const [filter, setFilter] = useState('all');
|
|
20
|
+
const [itemCount, setItemCount] = useState(0);
|
|
21
|
+
|
|
22
|
+
// --- Data Bindings ---
|
|
23
|
+
|
|
24
|
+
// Query todo items from the 'todo-item' data workflow, paginated, sorted by created_at
|
|
25
|
+
const { data: todos } = useQuery('todo-item', {
|
|
26
|
+
limit: 100,
|
|
27
|
+
orderBy: 'created_at',
|
|
28
|
+
filter: { status: 'active' },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Query completed items separately for the "Done" tab
|
|
32
|
+
const { data: completedTodos } = useQuery('todo-item', {
|
|
33
|
+
limit: 50,
|
|
34
|
+
filter: { status: 'completed' },
|
|
35
|
+
orderBy: 'completed_at',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Mutation target for creating/updating todo items
|
|
39
|
+
const todoMutation = useMutation('todo-item');
|
|
40
|
+
|
|
41
|
+
// --- Role Guard ---
|
|
42
|
+
|
|
43
|
+
// Only 'editor' and 'admin' roles can modify todos
|
|
44
|
+
const canEdit = useRole('editor');
|
|
45
|
+
const isAdmin = useRole('admin');
|
|
46
|
+
|
|
47
|
+
// --- States & Hooks ---
|
|
48
|
+
|
|
49
|
+
// When entering the 'active' state, refresh the item count
|
|
50
|
+
useOnEnter('active', () => {
|
|
51
|
+
setItemCount(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// When leaving 'active', clear the input
|
|
55
|
+
useOnExit('active', () => {
|
|
56
|
+
setTitle('');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Auto-refresh while in 'active' state (every 30s)
|
|
60
|
+
useWhileIn('active', 30000, () => {
|
|
61
|
+
// Poll for updates
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// --- Transitions ---
|
|
65
|
+
|
|
66
|
+
useTransition('activate', { from: 'draft', to: 'active' });
|
|
67
|
+
useTransition('archive', { from: 'active', to: 'archived' });
|
|
68
|
+
useTransition('restore', { from: 'archived', to: 'active' });
|
|
69
|
+
|
|
70
|
+
// --- UI ---
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Stack className="todo-app" sx={{ p: 6, maxWidth: '600px', mx: 'auto' }}>
|
|
74
|
+
<Heading sx={{ fontSize: '2xl', fontWeight: 'bold', mb: 4 }}>
|
|
75
|
+
Todo List ({itemCount})
|
|
76
|
+
</Heading>
|
|
77
|
+
|
|
78
|
+
{/* Add new todo — only shown if user has editor role */}
|
|
79
|
+
{canEdit && (
|
|
80
|
+
<Stack sx={{ direction: 'row', gap: 2, mb: 4 }}>
|
|
81
|
+
<Input
|
|
82
|
+
value={title}
|
|
83
|
+
onChange={(e: any) => setTitle(e.target.value)}
|
|
84
|
+
placeholder="What needs to be done?"
|
|
85
|
+
sx={{ flex: 1 }}
|
|
86
|
+
/>
|
|
87
|
+
<Button onClick={() => setTitle('')} sx={{ colorScheme: 'blue' }}>
|
|
88
|
+
Add
|
|
89
|
+
</Button>
|
|
90
|
+
</Stack>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Filter tabs */}
|
|
94
|
+
<Stack sx={{ direction: 'row', gap: 2, mb: 4 }}>
|
|
95
|
+
<Button onClick={() => setFilter('all')} sx={{ variant: filter === 'all' ? 'solid' : 'outline' }}>
|
|
96
|
+
All
|
|
97
|
+
</Button>
|
|
98
|
+
<Button onClick={() => setFilter('active')} sx={{ variant: filter === 'active' ? 'solid' : 'outline' }}>
|
|
99
|
+
Active
|
|
100
|
+
</Button>
|
|
101
|
+
<Button onClick={() => setFilter('done')} sx={{ variant: filter === 'done' ? 'solid' : 'outline' }}>
|
|
102
|
+
Done
|
|
103
|
+
</Button>
|
|
104
|
+
</Stack>
|
|
105
|
+
|
|
106
|
+
{/* Todo items list */}
|
|
107
|
+
<Each items={todos}>
|
|
108
|
+
{(todo: any) => (
|
|
109
|
+
<Stack sx={{ direction: 'row', alignItems: 'center', gap: 2, py: 2, borderBottom: '1px solid', borderColor: 'gray.200' }}>
|
|
110
|
+
<Checkbox checked={todo.completed} sx={{ mr: 2 }} />
|
|
111
|
+
<Text sx={{ flex: 1, textDecoration: todo.completed ? 'line-through' : 'none' }}>
|
|
112
|
+
{todo.title}
|
|
113
|
+
</Text>
|
|
114
|
+
{isAdmin && (
|
|
115
|
+
<Button sx={{ size: 'sm', colorScheme: 'red', variant: 'ghost' }}>
|
|
116
|
+
Delete
|
|
117
|
+
</Button>
|
|
118
|
+
)}
|
|
119
|
+
</Stack>
|
|
120
|
+
)}
|
|
121
|
+
</Each>
|
|
122
|
+
|
|
123
|
+
{/* Empty state */}
|
|
124
|
+
{!todos && (
|
|
125
|
+
<Text sx={{ color: 'gray.400', textAlign: 'center', py: 8 }}>
|
|
126
|
+
No todos yet. Add one above!
|
|
127
|
+
</Text>
|
|
128
|
+
)}
|
|
129
|
+
</Stack>
|
|
130
|
+
);
|
|
131
|
+
}
|