@lotics/ui 7.19.3 → 9.0.0
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/AGENTS.md +298 -159
- package/examples/tpl_allocate.tsx +2 -2
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_dieline.tsx +3 -3
- package/examples/tpl_documents.tsx +790 -0
- package/examples/tpl_item_list.tsx +1015 -124
- package/examples/tpl_lookup.tsx +37 -10
- package/examples/tpl_pick.tsx +3 -3
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_record.tsx +1354 -0
- package/examples/tpl_report.tsx +7 -7
- package/examples/tpl_rollup.tsx +6 -6
- package/examples/tpl_shifts.tsx +2 -2
- package/examples/tpl_statements.tsx +221 -0
- package/examples/tpl_stock.tsx +7 -7
- package/examples/tpl_task_board.tsx +48 -32
- package/examples/tpl_tasks.tsx +47 -47
- package/examples/tpl_tower.tsx +2 -2
- package/package.json +9 -9
- package/src/agent_run.tsx +1 -1
- package/src/capture_row.tsx +59 -0
- package/src/change_review.tsx +732 -236
- package/src/checklist.tsx +104 -0
- package/src/chip.tsx +12 -3
- package/src/composer.tsx +22 -1
- package/src/confidence.tsx +1 -1
- package/src/control_surface.ts +0 -14
- package/src/detail_row.tsx +137 -10
- package/src/finding.tsx +112 -80
- package/src/floating_action_bar.tsx +1 -1
- package/src/inline_date_picker.tsx +8 -3
- package/src/inline_edit.tsx +40 -10
- package/src/inline_member_select.tsx +3 -0
- package/src/inline_number_input.tsx +5 -2
- package/src/inline_select.tsx +8 -3
- package/src/inline_tag_select.tsx +140 -0
- package/src/inline_text_input.tsx +5 -2
- package/src/inline_time_picker.tsx +5 -2
- package/src/ledger.tsx +220 -0
- package/src/locale.tsx +43 -16
- package/src/progress_bar.tsx +32 -1
- package/src/record_summary.tsx +101 -0
- package/src/section_heading.tsx +16 -8
- package/src/sources.tsx +8 -5
- package/src/suggestion_chip.tsx +47 -0
- package/src/use_section_nav.test.ts +69 -0
- package/src/use_section_nav.ts +59 -0
- package/examples/tpl_assistant.tsx +0 -174
- package/examples/tpl_billing.tsx +0 -344
- package/examples/tpl_briefing.tsx +0 -121
- package/examples/tpl_compare.tsx +0 -133
- package/examples/tpl_crosscheck.tsx +0 -120
- package/examples/tpl_detail.tsx +0 -232
- package/examples/tpl_directory.tsx +0 -260
- package/examples/tpl_draft.tsx +0 -163
- package/examples/tpl_extract.tsx +0 -193
- package/examples/tpl_intake.tsx +0 -206
- package/examples/tpl_match.tsx +0 -134
- package/examples/tpl_order.tsx +0 -482
- package/examples/tpl_quick.tsx +0 -211
- package/examples/tpl_ratedesk.tsx +0 -386
- package/examples/tpl_record_plain.tsx +0 -259
- package/examples/tpl_settings.tsx +0 -178
- package/examples/tpl_timeline.tsx +0 -244
- package/examples/tpl_triage.tsx +0 -112
- package/examples/tpl_wizard.tsx +0 -223
- package/src/discrepancy.tsx +0 -114
- package/src/match_sides.tsx +0 -79
- package/src/record_fields.tsx +0 -68
- package/src/review_card.tsx +0 -172
- package/src/scored_option.tsx +0 -138
- package/src/spec_list.tsx +0 -81
- package/src/triage_row.tsx +0 -99
package/examples/tpl_intake.tsx
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { ScrollView, View } from "react-native";
|
|
3
|
-
import { Text } from "@lotics/ui/text";
|
|
4
|
-
import { colors } from "@lotics/ui/colors";
|
|
5
|
-
import { Button } from "@lotics/ui/button";
|
|
6
|
-
import { Card, CardFooter, CardHeader, CardHeaderTitle } from "@lotics/ui/card";
|
|
7
|
-
import { FileDropzone } from "@lotics/ui/file_dropzone";
|
|
8
|
-
import type { DisplayFile } from "@lotics/ui/file_thumbnail";
|
|
9
|
-
import { FileThumbnailGrid } from "@lotics/ui/file_thumbnail_grid";
|
|
10
|
-
import { Divider } from "@lotics/ui/divider";
|
|
11
|
-
import { FormField } from "@lotics/ui/form_field";
|
|
12
|
-
import { Picker } from "@lotics/ui/picker";
|
|
13
|
-
import { Switch } from "@lotics/ui/switch";
|
|
14
|
-
import { TextInputField } from "@lotics/ui/text_input_field";
|
|
15
|
-
|
|
16
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
-
// Template · Client intake — the multi-section intake form for a new
|
|
18
|
-
// business partner. One banded Card: header (title + why it matters) →
|
|
19
|
-
// three titled fieldsets separated by Dividers → footer per Action Layout
|
|
20
|
-
// (helper text left · Cancel muted + ONE primary right). Fields wrap on a
|
|
21
|
-
// flexBasis grid; every field holds state; Tax ID demonstrates the error band.
|
|
22
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
const INDUSTRIES = [
|
|
25
|
-
{ label: "Manufacturing", value: "san_xuat" },
|
|
26
|
-
{ label: "Trading", value: "thuong_mai" },
|
|
27
|
-
{ label: "Construction", value: "xay_dung" },
|
|
28
|
-
{ label: "Transportation", value: "van_tai" },
|
|
29
|
-
{ label: "Packaging", value: "bao_bi" },
|
|
30
|
-
{ label: "Other", value: "khac" },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const PAYMENT_TERMS = [
|
|
34
|
-
{ label: "Due on receipt", value: "ngay" },
|
|
35
|
-
{ label: "Net 15", value: "15" },
|
|
36
|
-
{ label: "Net 30", value: "30" },
|
|
37
|
-
{ label: "Net 45", value: "45" },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
/** Grid cells: half-width on wide screens, full row when they can't fit. */
|
|
41
|
-
const half = { flexGrow: 1, flexBasis: 280 } as const;
|
|
42
|
-
const full = { flexGrow: 1, flexBasis: "100%" } as const;
|
|
43
|
-
|
|
44
|
-
function FieldsetTitle({ title, hint }: { title: string; hint: string }) {
|
|
45
|
-
return (
|
|
46
|
-
<View style={{ gap: 2, paddingBottom: 14 }}>
|
|
47
|
-
<Text size="xs" color="muted" transform="uppercase">{title}</Text>
|
|
48
|
-
<Text size="xs" color="muted">{hint}</Text>
|
|
49
|
-
</View>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function TplIntake() {
|
|
54
|
-
const [tenCongTy, setTenCongTy] = useState("Northwind Packaging Co., Ltd.");
|
|
55
|
-
const [mst, setMst] = useState("031245678");
|
|
56
|
-
const [linhVuc, setLinhVuc] = useState("");
|
|
57
|
-
const [nguoiLienHe, setNguoiLienHe] = useState("Maria Lopez");
|
|
58
|
-
const [dienThoai, setDienThoai] = useState("0903 558 214");
|
|
59
|
-
const [email, setEmail] = useState("maria.lopez@northwindpkg.com");
|
|
60
|
-
const [nhomZalo, setNhomZalo] = useState("");
|
|
61
|
-
const [hanThanhToan, setHanThanhToan] = useState("30");
|
|
62
|
-
const [hoaDonVat, setHoaDonVat] = useState(true);
|
|
63
|
-
const [ghiChu, setGhiChu] = useState("");
|
|
64
|
-
const [giayTo, setGiayTo] = useState<DisplayFile[]>([]);
|
|
65
|
-
|
|
66
|
-
const soChuSo = mst.replace(/\D/g, "").length;
|
|
67
|
-
const mstError =
|
|
68
|
-
mst.length > 0 && !/^\d{10}(\d{3})?$/.test(mst.replace(/[\s.-]/g, ""))
|
|
69
|
-
? `Tax ID must be 10 or 13 digits — currently ${soChuSo}.`
|
|
70
|
-
: undefined;
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
74
|
-
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
75
|
-
{/* header band */}
|
|
76
|
-
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|
|
77
|
-
<View style={{ gap: 2, flex: 1 }}>
|
|
78
|
-
<Text size="xl" weight="semibold">Client intake</Text>
|
|
79
|
-
<Text size="sm" color="muted">Set up a new partner record — complete details up front mean quotes, orders, and invoices never need a follow-up question</Text>
|
|
80
|
-
</View>
|
|
81
|
-
</View>
|
|
82
|
-
|
|
83
|
-
<Card style={{ padding: 0 }}>
|
|
84
|
-
{/* card header: what this form decides */}
|
|
85
|
-
<CardHeader>
|
|
86
|
-
<CardHeaderTitle description="Enter once, reuse on every future quote and invoice — the tax ID is the matching key, double-check it before creating">
|
|
87
|
-
Partner record
|
|
88
|
-
</CardHeaderTitle>
|
|
89
|
-
</CardHeader>
|
|
90
|
-
|
|
91
|
-
{/* fieldset 1 · General information */}
|
|
92
|
-
<View style={{ paddingHorizontal: 20, paddingTop: 18, paddingBottom: 4 }}>
|
|
93
|
-
<FieldsetTitle title="General information" hint="Legal name as on the business registration, not the trading name" />
|
|
94
|
-
<View style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 16 }}>
|
|
95
|
-
<FormField label="Company name" style={full}>
|
|
96
|
-
<TextInputField value={tenCongTy} onChangeText={setTenCongTy} placeholder="Name on the business registration" />
|
|
97
|
-
</FormField>
|
|
98
|
-
<FormField label="Tax ID" error={mstError} style={half}>
|
|
99
|
-
<TextInputField
|
|
100
|
-
value={mst}
|
|
101
|
-
onChangeText={setMst}
|
|
102
|
-
placeholder="10 or 13 digits"
|
|
103
|
-
inputMode="numeric"
|
|
104
|
-
style={{ fontVariant: ["tabular-nums"] }}
|
|
105
|
-
/>
|
|
106
|
-
</FormField>
|
|
107
|
-
<FormField label="Industry" style={half}>
|
|
108
|
-
<Picker options={INDUSTRIES} value={linhVuc} onValueChange={setLinhVuc} placeholder="Select industry" />
|
|
109
|
-
</FormField>
|
|
110
|
-
</View>
|
|
111
|
-
</View>
|
|
112
|
-
<Divider />
|
|
113
|
-
|
|
114
|
-
{/* fieldset 2 · Contact */}
|
|
115
|
-
<View style={{ paddingHorizontal: 20, paddingTop: 18, paddingBottom: 4 }}>
|
|
116
|
-
<FieldsetTitle title="Contact" hint="The person who receives quotes and the monthly statement reconciliation" />
|
|
117
|
-
<View style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 16 }}>
|
|
118
|
-
<FormField label="Contact person" style={half}>
|
|
119
|
-
<TextInputField value={nguoiLienHe} onChangeText={setNguoiLienHe} placeholder="Full name" />
|
|
120
|
-
</FormField>
|
|
121
|
-
<FormField label="Phone" style={half}>
|
|
122
|
-
<TextInputField
|
|
123
|
-
value={dienThoai}
|
|
124
|
-
onChangeText={setDienThoai}
|
|
125
|
-
placeholder="Mobile number"
|
|
126
|
-
inputMode="tel"
|
|
127
|
-
style={{ fontVariant: ["tabular-nums"] }}
|
|
128
|
-
/>
|
|
129
|
-
</FormField>
|
|
130
|
-
<FormField label="Email" style={half}>
|
|
131
|
-
<TextInputField value={email} onChangeText={setEmail} placeholder="name@company.com" inputMode="email" autoCapitalize="none" />
|
|
132
|
-
</FormField>
|
|
133
|
-
<FormField label="Chat group" optional optionalLabel="Optional" style={half}>
|
|
134
|
-
<TextInputField value={nhomZalo} onChangeText={setNhomZalo} placeholder="Group used to coordinate orders" />
|
|
135
|
-
</FormField>
|
|
136
|
-
</View>
|
|
137
|
-
</View>
|
|
138
|
-
<Divider />
|
|
139
|
-
|
|
140
|
-
{/* fieldset 3 · Terms */}
|
|
141
|
-
<View style={{ paddingHorizontal: 20, paddingTop: 18, paddingBottom: 4 }}>
|
|
142
|
-
<FieldsetTitle title="Terms" hint="Applied as the default on every order for this partner, adjustable per order" />
|
|
143
|
-
<View style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 16 }}>
|
|
144
|
-
<FormField label="Payment terms" style={half}>
|
|
145
|
-
<Picker options={PAYMENT_TERMS} value={hanThanhToan} onValueChange={setHanThanhToan} placeholder="Select payment terms" />
|
|
146
|
-
</FormField>
|
|
147
|
-
<FormField label="VAT invoice" style={half}>
|
|
148
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 10, minHeight: 40 }}>
|
|
149
|
-
<Switch accessibilityLabel="VAT invoice" value={hoaDonVat} onChange={setHoaDonVat} />
|
|
150
|
-
<Text size="sm" color="muted">{hoaDonVat ? "Issue an invoice for every order" : "No invoices issued"}</Text>
|
|
151
|
-
</View>
|
|
152
|
-
</FormField>
|
|
153
|
-
<FormField label="Notes" optional optionalLabel="Optional" style={full}>
|
|
154
|
-
<TextInputField
|
|
155
|
-
value={ghiChu}
|
|
156
|
-
onChangeText={setGhiChu}
|
|
157
|
-
placeholder="Special requirements for delivery, documents, reconciliation…"
|
|
158
|
-
numberOfLines={3}
|
|
159
|
-
/>
|
|
160
|
-
</FormField>
|
|
161
|
-
</View>
|
|
162
|
-
</View>
|
|
163
|
-
|
|
164
|
-
<Divider />
|
|
165
|
-
|
|
166
|
-
{/* fieldset 4 · Documents — the attachment pattern: FileDropzone
|
|
167
|
-
captures, FileThumbnailGrid displays what landed */}
|
|
168
|
-
<View style={{ paddingHorizontal: 20, paddingTop: 18, paddingBottom: 16, gap: 12 }}>
|
|
169
|
-
<FieldsetTitle title="Documents" hint="Business registration and legal documents — clear scans or photos" />
|
|
170
|
-
<FileDropzone
|
|
171
|
-
height={120}
|
|
172
|
-
label="Drop the business registration here"
|
|
173
|
-
hint="or click to browse · PDF, images"
|
|
174
|
-
dropLabel="Release to attach"
|
|
175
|
-
accept="application/pdf,image/*"
|
|
176
|
-
accessibilityLabel="Attach business registration"
|
|
177
|
-
onFiles={(files) =>
|
|
178
|
-
setGiayTo((prev) => [
|
|
179
|
-
...prev,
|
|
180
|
-
...files.map((f, i) => ({
|
|
181
|
-
id: `${f.name}-${prev.length + i}`,
|
|
182
|
-
filename: f.name,
|
|
183
|
-
mimeType: f.type || "application/octet-stream",
|
|
184
|
-
url: URL.createObjectURL(f),
|
|
185
|
-
})),
|
|
186
|
-
])
|
|
187
|
-
}
|
|
188
|
-
/>
|
|
189
|
-
{giayTo.length > 0 ? <FileThumbnailGrid files={giayTo} itemSize={88} /> : null}
|
|
190
|
-
</View>
|
|
191
|
-
|
|
192
|
-
{/* footer per Action Layout: helper left · Cancel muted + ONE primary right */}
|
|
193
|
-
<CardFooter>
|
|
194
|
-
<Text size="xs" color="muted" style={{ flex: 1 }}>
|
|
195
|
-
{giayTo.length > 0
|
|
196
|
-
? `${giayTo.length} files attached · record awaits Accounting approval before the first order`
|
|
197
|
-
: "New records await Accounting approval before the first order"}
|
|
198
|
-
</Text>
|
|
199
|
-
<Button title="Cancel" color="muted" onPress={() => {}} />
|
|
200
|
-
<Button title="Create record" color="primary" onPress={() => {}} />
|
|
201
|
-
</CardFooter>
|
|
202
|
-
</Card>
|
|
203
|
-
</View>
|
|
204
|
-
</ScrollView>
|
|
205
|
-
);
|
|
206
|
-
}
|
package/examples/tpl_match.tsx
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { ScrollView, View } from "react-native";
|
|
3
|
-
import { Text } from "@lotics/ui/text";
|
|
4
|
-
import { colors } from "@lotics/ui/colors";
|
|
5
|
-
import { Button } from "@lotics/ui/button";
|
|
6
|
-
import { KPIStrip } from "@lotics/ui/kpi_strip";
|
|
7
|
-
import { SegmentedControl } from "@lotics/ui/segmented_control";
|
|
8
|
-
import { EmptyState } from "@lotics/ui/empty_state";
|
|
9
|
-
import { ReviewCard, type ReviewAction } from "@lotics/ui/review_card";
|
|
10
|
-
import { MatchSides, MatchSideText } from "@lotics/ui/match_sides";
|
|
11
|
-
import type { ConfidenceLevel } from "@lotics/ui/confidence";
|
|
12
|
-
|
|
13
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
-
// Template · Match & reconcile — the agent proposes PAIRINGS and shows its
|
|
15
|
-
// reasoning; the human confirms, reassigns, or rejects. Each row is two-sided
|
|
16
|
-
// (a known item ↔ the agent's proposed counterpart) with a confidence on the
|
|
17
|
-
// bridge. High-confidence pairs batch-accept; the rest are worked one by one.
|
|
18
|
-
// Same shape for bank-line ↔ invoice, invoice ↔ PO, dedup A ↔ B, entity
|
|
19
|
-
// resolution. Strip the agent's reasoning and it's just a deterministic match register.
|
|
20
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
interface M {
|
|
23
|
-
id: string;
|
|
24
|
-
amount: number;
|
|
25
|
-
source: { title: string; detail: string };
|
|
26
|
-
match?: { title: string; detail: string };
|
|
27
|
-
/** An alternate candidate the agent considered — drives Reassign. */
|
|
28
|
-
alt?: { title: string; detail: string };
|
|
29
|
-
rationale: string;
|
|
30
|
-
confidence: ConfidenceLevel;
|
|
31
|
-
status: "open" | "accepted" | "dismissed";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const SEED: M[] = [
|
|
35
|
-
{ id: "m1", amount: 86_200_000, source: { title: "INCOMING TT — ATLAS COMPONENTS", detail: "09 Jun · 86,200,000 ₫" }, match: { title: "INV-2026-0312", detail: "Atlas Components · 86,200,000 ₫" }, rationale: "Exact amount and the invoice number appears in the transfer memo.", confidence: "high", status: "open" },
|
|
36
|
-
{ id: "m2", amount: 41_300_000, source: { title: "CRESTLINE TT PAYMENT", detail: "09 Jun · 41,300,000 ₫" }, match: { title: "INV-2026-0318", detail: "Crestline Furniture · 41,300,000 ₫" }, rationale: "Amount matches to the đồng and the payer name maps to the customer on file.", confidence: "high", status: "open" },
|
|
37
|
-
{ id: "m3", amount: 22_500_000, source: { title: "VITTORIA ACC — REF 0321", detail: "10 Jun · 22,500,000 ₫" }, match: { title: "INV-2026-0321", detail: "Vittoria Accessories · 22,500,000 ₫" }, rationale: "Reference 0321 cited; amount exact.", confidence: "high", status: "open" },
|
|
38
|
-
{ id: "m4", amount: 18_700_000, source: { title: "BRIGHTCELL JUNE", detail: "10 Jun · 18,700,000 ₫" }, match: { title: "INV-2026-0324", detail: "Brightcell Batteries · 18,700,000 ₫" }, alt: { title: "INV-2026-0319", detail: "Brightcell Batteries · 18,700,000 ₫" }, rationale: "Amount + customer match, but two open invoices share this total — confirm the period.", confidence: "medium", status: "open" },
|
|
39
|
-
{ id: "m5", amount: 12_600_000, source: { title: "MERIDIAN PART PAYMENT", detail: "12 Jun · 12,600,000 ₫" }, match: { title: "INV-2026-0326", detail: "Meridian Construction · 25,200,000 ₫" }, rationale: "Half the invoice total from the same payer — likely a partial payment.", confidence: "medium", status: "open" },
|
|
40
|
-
{ id: "m6", amount: 25_200_000, source: { title: "INCOMING TT REF 88412", detail: "11 Jun · 25,200,000 ₫" }, alt: { title: "INV-2026-0326", detail: "Meridian Construction · 25,200,000 ₫" }, rationale: "No name or reference on the line; the amount matches one open invoice but the agent isn't confident.", confidence: "low", status: "open" },
|
|
41
|
-
{ id: "m7", amount: 264_000, source: { title: "BANK CHARGES JUNE", detail: "11 Jun · 264,000 ₫" }, rationale: "No invoice fits; the description reads as a bank fee, not a receipt.", confidence: "low", status: "open" },
|
|
42
|
-
{ id: "m8", amount: 64_800_000, source: { title: "ATLAS COMPONENTS — INV-0301", detail: "08 Jun · 64,800,000 ₫" }, match: { title: "INV-2026-0301", detail: "Atlas Components · 64,800,000 ₫" }, rationale: "Exact reference and amount.", confidence: "high", status: "accepted" },
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
export function TplMatch() {
|
|
46
|
-
const [rows, setRows] = useState<M[]>(SEED);
|
|
47
|
-
const [tab, setTab] = useState<"open" | "resolved">("open");
|
|
48
|
-
|
|
49
|
-
const set = (id: string, status: M["status"]) => setRows((prev) => prev.map((r) => (r.id === id ? { ...r, status } : r)));
|
|
50
|
-
const reassign = (id: string) =>
|
|
51
|
-
setRows((prev) =>
|
|
52
|
-
prev.map((r) =>
|
|
53
|
-
r.id === id && r.alt ? { ...r, match: r.alt, alt: r.match, confidence: "medium", rationale: "Reassigned to the alternate candidate the agent considered." } : r,
|
|
54
|
-
),
|
|
55
|
-
);
|
|
56
|
-
const acceptAllHigh = () => setRows((prev) => prev.map((r) => (r.status === "open" && r.confidence === "high" && r.match ? { ...r, status: "accepted" } : r)));
|
|
57
|
-
|
|
58
|
-
const open = rows.filter((r) => r.status === "open");
|
|
59
|
-
const resolved = rows.filter((r) => r.status !== "open");
|
|
60
|
-
const visible = tab === "open" ? open : resolved;
|
|
61
|
-
const accepted = rows.filter((r) => r.status === "accepted");
|
|
62
|
-
const matchedValue = accepted.reduce((s, r) => s + r.amount, 0);
|
|
63
|
-
const unmatched = open.filter((r) => !r.match).length;
|
|
64
|
-
const highOpen = open.filter((r) => r.confidence === "high" && r.match).length;
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
68
|
-
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
69
|
-
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
|
|
70
|
-
<View style={{ flex: 1, gap: 2 }}>
|
|
71
|
-
<Text size="xl" weight="semibold">Payment matching</Text>
|
|
72
|
-
<Text size="sm" color="muted">The agent pairs incoming payments to open invoices — you confirm, reassign, or reject</Text>
|
|
73
|
-
</View>
|
|
74
|
-
{tab === "open" && highOpen > 0 ? (
|
|
75
|
-
<Button title={`Accept ${highOpen} high-confidence`} color="secondary" onPress={acceptAllHigh} />
|
|
76
|
-
) : null}
|
|
77
|
-
</View>
|
|
78
|
-
|
|
79
|
-
<KPIStrip
|
|
80
|
-
items={[
|
|
81
|
-
{ label: "Proposed", value: open.length, format: "number", caption: "awaiting a decision" },
|
|
82
|
-
{ label: "Accepted", value: accepted.length, format: "number" },
|
|
83
|
-
{ label: "Unmatched", value: unmatched, format: "number", tone: unmatched > 0 ? "warning" : "default", info: "Lines the agent couldn't pair confidently — they need a human to find the counterpart or take an escape hatch." },
|
|
84
|
-
{ label: "Matched value", value: matchedValue, format: "currency", compact: true, info: "Money tied to an invoice through an accepted pairing." },
|
|
85
|
-
]}
|
|
86
|
-
/>
|
|
87
|
-
|
|
88
|
-
<SegmentedControl
|
|
89
|
-
accessibilityLabel="Queue"
|
|
90
|
-
options={[
|
|
91
|
-
{ label: `Proposed · ${open.length}`, value: "open" },
|
|
92
|
-
{ label: `Resolved · ${resolved.length}`, value: "resolved" },
|
|
93
|
-
]}
|
|
94
|
-
value={tab}
|
|
95
|
-
onValueChange={setTab}
|
|
96
|
-
/>
|
|
97
|
-
|
|
98
|
-
{visible.length === 0 ? (
|
|
99
|
-
<EmptyState
|
|
100
|
-
icon="circle-check"
|
|
101
|
-
message={tab === "open" ? "Queue cleared" : "Nothing resolved yet"}
|
|
102
|
-
hint={tab === "open" ? "Every proposal has been accepted or rejected" : undefined}
|
|
103
|
-
/>
|
|
104
|
-
) : (
|
|
105
|
-
<View style={{ gap: 10 }}>
|
|
106
|
-
{visible.map((r) => {
|
|
107
|
-
const actions: ReviewAction[] = [
|
|
108
|
-
{ label: "Not a match", onPress: () => set(r.id, "dismissed"), kind: "muted" },
|
|
109
|
-
...(r.alt ? [{ label: r.match ? "Reassign" : "Find match", onPress: () => reassign(r.id), kind: "secondary" as const }] : []),
|
|
110
|
-
...(r.match ? [{ label: "Accept", onPress: () => set(r.id, "accepted"), kind: "primary" as const }] : []),
|
|
111
|
-
];
|
|
112
|
-
return (
|
|
113
|
-
<ReviewCard
|
|
114
|
-
key={r.id}
|
|
115
|
-
summary={r.source.title}
|
|
116
|
-
confidence={r.confidence}
|
|
117
|
-
rationale={r.rationale}
|
|
118
|
-
status={r.status}
|
|
119
|
-
acceptedLabel={r.match ? `Matched · ${r.match.title}` : "Matched"}
|
|
120
|
-
actions={actions}
|
|
121
|
-
>
|
|
122
|
-
<MatchSides
|
|
123
|
-
source={<MatchSideText title={r.source.title} detail={r.source.detail} />}
|
|
124
|
-
match={r.match ? <MatchSideText title={r.match.title} detail={r.match.detail} align="right" /> : undefined}
|
|
125
|
-
/>
|
|
126
|
-
</ReviewCard>
|
|
127
|
-
);
|
|
128
|
-
})}
|
|
129
|
-
</View>
|
|
130
|
-
)}
|
|
131
|
-
</View>
|
|
132
|
-
</ScrollView>
|
|
133
|
-
);
|
|
134
|
-
}
|