@lotics/ui 7.19.2 → 8.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.
@@ -1,174 +0,0 @@
1
- import { useRef, useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { colors } from "@lotics/ui/colors";
4
- import { Text } from "@lotics/ui/text";
5
- import { DotsIndicator } from "@lotics/ui/dots_indicator";
6
- import { Composer } from "@lotics/ui/composer";
7
- import { ChangeReview, ChangeReviewActions, ChangeDiff, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
8
-
9
- // ─────────────────────────────────────────────────────────────────────────────
10
- // Template · AI assistant — a CHAT workflow, a different shape from the design
11
- // canvas: a conversation where the agent's answer carries SUGGESTED EDITS the
12
- // human approves or rejects ONE BY ONE (a before→after diff per change, ✓/✕),
13
- // then applies only the accepted set. The review-each-edit pattern — an AI
14
- // copilot proposing changes to a record, never committing on its own. All mock.
15
- // ─────────────────────────────────────────────────────────────────────────────
16
-
17
- /** One field's before→after edit — the assistant's proposals, reviewed 1-by-1. */
18
- interface EditChange {
19
- label: string;
20
- before?: string;
21
- after: string;
22
- }
23
-
24
- interface ChatMsg {
25
- id: string;
26
- role: "user" | "agent";
27
- text?: string;
28
- /** An agent turn may carry suggested edits to review 1-by-1. */
29
- changes?: EditChange[];
30
- applied?: boolean;
31
- typing?: boolean;
32
- }
33
-
34
- const SUGGESTED: EditChange[] = [
35
- { label: "Company", before: "acme packaging co", after: "Acme Packaging Co." },
36
- { label: "Phone", before: "0901234567", after: "+84 90 123 4567" },
37
- { label: "Email", before: "SALES@ACME.VN", after: "sales@acme.vn" },
38
- { label: "Industry", after: "Packaging" },
39
- ];
40
-
41
- export function TplAssistant() {
42
- const idRef = useRef(1);
43
- const nextId = () => `m${idRef.current++}`;
44
- const [messages, setMessages] = useState<ChatMsg[]>([
45
- { id: "m0", role: "agent", text: "Paste a record or ask me to tidy one up — I'll propose changes you can approve one by one." },
46
- ]);
47
- const [decisions, setDecisions] = useState<Record<string, ChangeReviewItemStatus>>({});
48
- const [prompt, setPrompt] = useState("");
49
- const [busy, setBusy] = useState(false);
50
- const [proposed, setProposed] = useState(false);
51
-
52
- const send = (text: string) => {
53
- setPrompt("");
54
- const userMsg: ChatMsg = { id: nextId(), role: "user", text };
55
- const typingId = nextId();
56
- setMessages((m) => [...m, userMsg, { id: typingId, role: "agent", typing: true }]);
57
- setBusy(true);
58
- setTimeout(() => {
59
- setBusy(false);
60
- setMessages((m) =>
61
- m.map((msg) =>
62
- msg.id === typingId
63
- ? proposed
64
- ? { id: msg.id, role: "agent", text: "Done — anything else to adjust?" }
65
- : { id: msg.id, role: "agent", text: "Here's what I'd standardize on this contact. Review each:", changes: SUGGESTED.map((c) => ({ ...c, status: "pending" })) }
66
- : msg,
67
- ),
68
- );
69
- setProposed(true);
70
- }, 1300);
71
- };
72
-
73
- const decide = (msgId: string, index: number, status: ChangeReviewItemStatus) => {
74
- setDecisions((d) => ({ ...d, [`${msgId}:${index}`]: status }));
75
- };
76
-
77
- const applyMsg = (msgId: string) => {
78
- const msg = messages.find((m) => m.id === msgId);
79
- const n = (msg?.changes ?? []).filter((_, i) => decisions[`${msgId}:${i}`] === "accepted").length;
80
- setMessages((m) => [
81
- ...m.map((x) => (x.id === msgId ? { ...x, applied: true } : x)),
82
- { id: nextId(), role: "agent", text: `Applied ${n} change${n === 1 ? "" : "s"} to the contact.` },
83
- ]);
84
- };
85
-
86
- return (
87
- <View style={{ flex: 1, backgroundColor: colors.white }}>
88
- <View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.zinc[200], backgroundColor: colors.background }}>
89
- <View style={{ width: 24, height: 24, borderRadius: 7, alignItems: "center", justifyContent: "center", backgroundColor: colors.zinc[100] }}>
90
- <View style={{ width: 4, height: 4, borderRadius: 2, backgroundColor: colors.zinc[900] }} />
91
- </View>
92
- <Text size="sm" weight="semibold">Record assistant</Text>
93
- </View>
94
-
95
- <ScrollView contentContainerStyle={{ padding: 20, paddingBottom: 140, gap: 16, maxWidth: 760, width: "100%", alignSelf: "center" }}>
96
- {messages.map((m) => {
97
- if (m.role === "user") {
98
- return (
99
- <View key={m.id} style={{ alignItems: "flex-end" }}>
100
- <View style={{ maxWidth: "82%", backgroundColor: colors.zinc[900], borderRadius: 14, paddingHorizontal: 14, paddingVertical: 10 }}>
101
- <Text size="sm" color="inverted">{m.text}</Text>
102
- </View>
103
- </View>
104
- );
105
- }
106
- const active = m.changes != null && !m.applied;
107
- return (
108
- <View key={m.id} style={{ flexDirection: "row", gap: 10, alignItems: "flex-start" }}>
109
- <View style={{ width: 26, height: 26, borderRadius: 8, alignItems: "center", justifyContent: "center", backgroundColor: colors.zinc[100], marginTop: 1 }}>
110
- <View style={{ width: 4, height: 4, borderRadius: 2, backgroundColor: colors.zinc[900] }} />
111
- </View>
112
- <View style={{ flex: 1, gap: 10 }}>
113
- {m.typing ? (
114
- <View style={{ paddingVertical: 6 }}><DotsIndicator size={6} color={colors.zinc[400]} /></View>
115
- ) : null}
116
- {m.text ? <Text size="sm">{m.text}</Text> : null}
117
- {m.changes ? (
118
- <View style={{ gap: 14 }}>
119
- <ChangeReview
120
- items={m.changes}
121
- getKey={(c, i) => `${m.id}:${i}`}
122
- statusOf={(_, i) => decisions[`${m.id}:${i}`] ?? "pending"}
123
- renderItem={(c) => (
124
- <>
125
- <Text size="xs" color="muted" weight="medium">{c.label}</Text>
126
- <ChangeDiff before={c.before} after={c.after} />
127
- </>
128
- )}
129
- renderSummary={(c, kept) => (
130
- <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
131
- <Text size="sm" weight="medium" numberOfLines={1}>{c.label}</Text>
132
- <Text size="sm" numberOfLines={1} color={kept ? "default" : "muted"} style={[{ flex: 1 }, kept ? undefined : { textDecorationLine: "line-through" }]}>{kept ? c.after : (c.before ?? c.after)}</Text>
133
- </View>
134
- )}
135
- status={m.applied ? "applied" : "open"}
136
- onAcceptItem={active ? (i) => decide(m.id, i, "accepted") : undefined}
137
- onRejectItem={active ? (i) => decide(m.id, i, "rejected") : undefined}
138
- onUndoItem={active ? (i) => setDecisions((d) => { const n = { ...d }; delete n[`${m.id}:${i}`]; return n; }) : undefined}
139
- />
140
- {/* Inline (no dialog) — the commit bar sits under the list behind a divider. */}
141
- {active && !m.applied ? (
142
- <View style={{ borderTopWidth: 1, borderTopColor: colors.zinc[200], paddingTop: 14, flexDirection: "row" }}>
143
- <ChangeReviewActions
144
- items={m.changes}
145
- statusOf={(_, i) => decisions[`${m.id}:${i}`] ?? "pending"}
146
- applyLabel="Apply accepted"
147
- onAcceptItem={(i) => decide(m.id, i, "accepted")}
148
- onApply={() => applyMsg(m.id)}
149
- onDiscard={() => setMessages((ms) => ms.map((x) => (x.id === m.id ? { ...x, applied: true } : x)))}
150
- />
151
- </View>
152
- ) : null}
153
- </View>
154
- ) : null}
155
- </View>
156
- </View>
157
- );
158
- })}
159
- </ScrollView>
160
-
161
- <View style={{ position: "absolute", left: 0, right: 0, bottom: 20, alignItems: "center", paddingHorizontal: 16 }}>
162
- <View style={{ width: "100%", maxWidth: 720 }}>
163
- <Composer
164
- value={prompt}
165
- onChangeText={setPrompt}
166
- onSend={send}
167
- disabled={busy}
168
- placeholder={proposed ? "Ask for another change…" : "Try: “Tidy up this contact”"}
169
- />
170
- </View>
171
- </View>
172
- </View>
173
- );
174
- }
@@ -1,121 +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 { KPIStrip } from "@lotics/ui/kpi_strip";
6
- import { SegmentedControl } from "@lotics/ui/segmented_control";
7
- import { Sources } from "@lotics/ui/sources";
8
- import { Finding, type FindingSeverity } from "@lotics/ui/finding";
9
-
10
- // ─────────────────────────────────────────────────────────────────────────────
11
- // Template · Briefing — the agent reads the period's data and writes a short
12
- // narrative, then a RANKED list of what needs attention. Each finding carries a
13
- // severity, a headline metric, the sources it rests on, and the one action it
14
- // suggests. Unlike a dashboard (raw charts), this is the agent's COMMENTARY:
15
- // what changed, what's at risk, what to do. The leadership digest / daily ops
16
- // brief / anomaly scan. The narrative explains; the findings are the worklist.
17
- // ─────────────────────────────────────────────────────────────────────────────
18
-
19
- interface F {
20
- severity: FindingSeverity;
21
- title: string;
22
- detail: string;
23
- metric: string;
24
- metricCaption: string;
25
- action: string;
26
- sources: { id: string; label: string; kind: "record" | "table" | "document" }[];
27
- }
28
-
29
- const NARRATIVE: Record<"today" | "week", string> = {
30
- today:
31
- "Volume held steady at 41 active shipments. Three reefer containers to Hamburg are inside the demurrage window and need a pickup booked today. Cash collection is lagging — five invoices crossed 30 days overdue. On-time delivery improved on the back of the new Cát Lái slotting.",
32
- week:
33
- "A solid week: 268 shipments moved, on-time delivery up 6 points after the Cát Lái slotting change. Two ocean lanes slipped below the 12% margin target on bunker surcharges. Receivables remain the soft spot — ₫120M is now past 30 days, concentrated in two accounts.",
34
- };
35
-
36
- const FINDINGS: Record<"today" | "week", F[]> = {
37
- today: [
38
- { severity: "critical", title: "3 reefer containers at demurrage risk", detail: "Hamburg shipments HBL-4471/4472/4480 hit free-time expiry at 18:00 today; no pickup is booked.", metric: "₫48M", metricCaption: "exposure", action: "Open the shipments", sources: [{ id: "a1", label: "SHIP-4471", kind: "record" }, { id: "a2", label: "Demurrage tracker", kind: "table" }] },
39
- { severity: "warning", title: "5 invoices overdue past 30 days", detail: "Two accounts — Crestline and Meridian — make up most of the balance. No promise-to-pay logged this week.", metric: "₫120M", metricCaption: "overdue", action: "Open receivables", sources: [{ id: "a3", label: "Aged receivables", kind: "table" }] },
40
- { severity: "warning", title: "Margin on the Hamburg lane below target", detail: "Bunker surcharge rose 9%; the lane is at 9.8% gross vs the 12% floor.", metric: "−2.2 pts", metricCaption: "vs target", action: "Review lane pricing", sources: [{ id: "a4", label: "Lane P&L", kind: "table" }] },
41
- { severity: "positive", title: "On-time delivery improving", detail: "The Cát Lái slotting change lifted on-time pickups for the third day running.", metric: "+6 pts", metricCaption: "3-day", action: "See the trend", sources: [{ id: "a5", label: "OTD report", kind: "document" }] },
42
- ],
43
- week: [
44
- { severity: "critical", title: "Receivables concentration risk", detail: "₫120M past 30 days sits in two accounts; one is also near its credit limit.", metric: "2", metricCaption: "accounts", action: "Open receivables", sources: [{ id: "b1", label: "Aged receivables", kind: "table" }] },
45
- { severity: "warning", title: "Two ocean lanes below margin target", detail: "Hamburg and Rotterdam slipped under 12% on bunker surcharges sustained all week.", metric: "2", metricCaption: "lanes", action: "Review lane pricing", sources: [{ id: "b2", label: "Lane P&L", kind: "table" }] },
46
- { severity: "positive", title: "On-time delivery up 6 points", detail: "The slotting change held all week — the strongest OTD in two months.", metric: "+6 pts", metricCaption: "this week", action: "See the trend", sources: [{ id: "b3", label: "OTD report", kind: "document" }] },
47
- { severity: "info", title: "RFQ volume rising", detail: "12 new quote requests this week, ahead of the trailing average — capacity planning should get ahead of it.", metric: "12", metricCaption: "RFQs", action: "Open the pipeline", sources: [{ id: "b4", label: "Quote pipeline", kind: "table" }] },
48
- ],
49
- };
50
-
51
- export function TplBriefing() {
52
- const [period, setPeriod] = useState<"today" | "week">("today");
53
- const findings = FINDINGS[period];
54
- const attention = findings.filter((f) => f.severity === "critical" || f.severity === "warning").length;
55
-
56
- return (
57
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
58
- <View style={{ width: "100%", maxWidth: 860, alignSelf: "center", gap: 16 }}>
59
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
60
- <View style={{ flex: 1, gap: 2 }}>
61
- <Text size="xl" weight="semibold">Operations briefing</Text>
62
- <Text size="sm" color="muted">What the agent sees in the numbers — and what needs a decision</Text>
63
- </View>
64
- <SegmentedControl
65
- accessibilityLabel="Period"
66
- options={[
67
- { label: "Today", value: "today" },
68
- { label: "This week", value: "week" },
69
- ]}
70
- value={period}
71
- onValueChange={setPeriod}
72
- />
73
- </View>
74
-
75
- {/* the generated narrative */}
76
- <View style={{ borderWidth: 1, borderColor: colors.zinc[200], backgroundColor: colors.white, borderRadius: 14, padding: 18, gap: 12 }}>
77
- <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
78
- <Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Summary</Text>
79
- <Text size="xs" color="muted">{period === "today" ? "as of 14:20" : "week to date"}</Text>
80
- </View>
81
- <Text size="md" style={{ lineHeight: 22 }}>{NARRATIVE[period]}</Text>
82
- <Sources
83
- label="Read from"
84
- onOpen={() => {}}
85
- sources={[
86
- { id: "n1", label: "Shipments", kind: "table" },
87
- { id: "n2", label: "Aged receivables", kind: "table" },
88
- { id: "n3", label: "Lane P&L", kind: "table" },
89
- ]}
90
- />
91
- </View>
92
-
93
- <KPIStrip
94
- items={[
95
- { label: "Active shipments", value: period === "today" ? 41 : 268, format: "number", trend: period === "today" ? 0 : 4 },
96
- { label: "On-time delivery", value: "94%", trend: 6 },
97
- { label: "Overdue receivables", value: 120_000_000, format: "currency", compact: true, tone: "danger" },
98
- { label: "Needs attention", value: attention, format: "number", tone: attention > 0 ? "warning" : "default" },
99
- ]}
100
- />
101
-
102
- <View style={{ gap: 10 }}>
103
- <Text size="sm" weight="semibold">Needs attention</Text>
104
- {findings.map((f, i) => (
105
- <Finding
106
- key={i}
107
- severity={f.severity}
108
- title={f.title}
109
- detail={f.detail}
110
- metric={f.metric}
111
- metricCaption={f.metricCaption}
112
- sources={f.sources}
113
- onOpenSource={() => {}}
114
- action={{ label: f.action, onPress: () => {} }}
115
- />
116
- ))}
117
- </View>
118
- </View>
119
- </ScrollView>
120
- );
121
- }
@@ -1,133 +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 { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
6
- import { ScoredOption } from "@lotics/ui/scored_option";
7
- import type { SpecRow } from "@lotics/ui/spec_list";
8
-
9
- // ─────────────────────────────────────────────────────────────────────────────
10
- // Template · Compare — the agent SCORES and ranks the options against the
11
- // criteria, shows its reasoning and the key specs, and the human picks one. The
12
- // agent does the legwork (gather, normalise, score); the choice stays the
13
- // human's. Quotes, carriers, suppliers, plans, routes. The recommended option
14
- // wears a RECOMMENDED label; selecting one settles the shortlist to the decision.
15
- // ─────────────────────────────────────────────────────────────────────────────
16
-
17
- interface O {
18
- id: string;
19
- rank: number;
20
- title: string;
21
- subtitle: string;
22
- score: number;
23
- rationale: string;
24
- specs: SpecRow[];
25
- recommended?: boolean;
26
- }
27
-
28
- const OPTIONS: O[] = [
29
- {
30
- id: "o1",
31
- rank: 1,
32
- title: "Maersk — Cát Lái → Hamburg",
33
- subtitle: "Direct, weekly service · reefer guaranteed",
34
- score: 0.92,
35
- rationale: "Best balance: only 4% above the cheapest, but the most reliable schedule and guaranteed reefer plugs — lowest demurrage risk for time-sensitive cargo.",
36
- recommended: true,
37
- specs: [
38
- { label: "All-in rate", value: "$2,840" },
39
- { label: "Transit", value: "28 days" },
40
- { label: "Schedule reliability", value: "94%" },
41
- { label: "Free time", value: "14 days" },
42
- ],
43
- },
44
- {
45
- id: "o2",
46
- rank: 2,
47
- title: "CMA CGM — Cát Lái → Hamburg",
48
- subtitle: "1 transshipment at Singapore",
49
- score: 0.78,
50
- rationale: "Cheapest of the four, but the Singapore transshipment adds three days and a reliability hit — workable for non-urgent cargo.",
51
- specs: [
52
- { label: "All-in rate", value: "$2,730" },
53
- { label: "Transit", value: "31 days" },
54
- { label: "Schedule reliability", value: "86%" },
55
- { label: "Free time", value: "10 days" },
56
- ],
57
- },
58
- {
59
- id: "o3",
60
- rank: 3,
61
- title: "Hapag-Lloyd — Cát Lái → Hamburg",
62
- subtitle: "Direct, premium service",
63
- score: 0.64,
64
- rationale: "Fastest transit and strong reliability, but the premium rate is hard to justify unless the customer pays for speed.",
65
- specs: [
66
- { label: "All-in rate", value: "$3,210" },
67
- { label: "Transit", value: "26 days" },
68
- { label: "Schedule reliability", value: "92%" },
69
- { label: "Free time", value: "10 days" },
70
- ],
71
- },
72
- {
73
- id: "o4",
74
- rank: 4,
75
- title: "ONE — Cát Lái → Hamburg",
76
- subtitle: "2 transshipments · no reefer guarantee",
77
- score: 0.41,
78
- rationale: "Lowest score: two transshipments and no guaranteed reefer plug make it unsuitable for frozen cargo, despite a mid-range rate.",
79
- specs: [
80
- { label: "All-in rate", value: "$2,910" },
81
- { label: "Transit", value: "34 days" },
82
- { label: "Schedule reliability", value: "79%" },
83
- { label: "Free time", value: "7 days" },
84
- ],
85
- },
86
- ];
87
-
88
- export function TplCompare() {
89
- const [chosen, setChosen] = useState<string | null>(null);
90
- const pick = OPTIONS.find((o) => o.id === chosen) ?? null;
91
-
92
- return (
93
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
94
- <View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
95
- <View style={{ gap: 2 }}>
96
- <Text size="xl" weight="semibold">Freight options</Text>
97
- <Text size="sm" color="muted">Ocean · Cát Lái → Hamburg · 2×40HC reefer · frozen shrimp</Text>
98
- </View>
99
-
100
- <View style={{ borderLeftWidth: 2, borderLeftColor: colors.zinc[300], paddingLeft: 12 }}>
101
- <Text size="sm" color="muted">
102
- The agent scored four quotes against cost, transit time and schedule reliability, weighted for time-sensitive reefer cargo.
103
- </Text>
104
- </View>
105
-
106
- {pick ? (
107
- <Callout tone="neutral">
108
- <CalloutTitle>Chosen · {pick.title}</CalloutTitle>
109
- <CalloutText>{pick.specs[0].label} {String(pick.specs[0].value)} · {String(pick.specs[1].value)} transit. Booking can proceed on this option.</CalloutText>
110
- </Callout>
111
- ) : null}
112
-
113
- <View style={{ gap: 12 }}>
114
- {OPTIONS.map((o) => (
115
- <ScoredOption
116
- key={o.id}
117
- rank={o.rank}
118
- title={o.title}
119
- subtitle={o.subtitle}
120
- score={o.score}
121
- rationale={o.rationale}
122
- specs={o.specs}
123
- recommended={o.recommended}
124
- selected={chosen === o.id}
125
- selectLabel="Choose this option"
126
- onSelect={() => setChosen((c) => (c === o.id ? null : o.id))}
127
- />
128
- ))}
129
- </View>
130
- </View>
131
- </ScrollView>
132
- );
133
- }
@@ -1,120 +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 { KPIStrip } from "@lotics/ui/kpi_strip";
6
- import { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
7
- import { Discrepancy, type DiscrepancyValue } from "@lotics/ui/discrepancy";
8
-
9
- // ─────────────────────────────────────────────────────────────────────────────
10
- // Template · Cross-check — the agent compares the documents of one transaction
11
- // and surfaces every field whose value DISAGREES across sources. Each card lays
12
- // the conflicting values side by side (with where each came from), marks the one
13
- // the agent believes, and the human resolves to a truth or flags it. The audit /
14
- // 3-way-match / contract-vs-invoice desk. Symmetric (N sources disagree), unlike
15
- // a before→after edit. The agreeing fields stay out of the way — only conflicts
16
- // need a decision.
17
- // ─────────────────────────────────────────────────────────────────────────────
18
-
19
- interface D {
20
- id: string;
21
- field: string;
22
- values: DiscrepancyValue[];
23
- note: string;
24
- }
25
-
26
- const SEED: D[] = [
27
- {
28
- id: "d1",
29
- field: "Total cartons",
30
- values: [
31
- { source: "Shipping instruction", value: "525" },
32
- { source: "Commercial invoice", value: "520", recommended: true },
33
- { source: "Packing list", value: "520" },
34
- ],
35
- note: "Two of three documents agree at 520; the SI looks stale — it predates the final load.",
36
- },
37
- {
38
- id: "d2",
39
- field: "Net weight",
40
- values: [
41
- { source: "Commercial invoice", value: "11,980 kg" },
42
- { source: "Packing list", value: "12,050 kg", recommended: true },
43
- ],
44
- note: "The packing list carries the measured weight; the CI rounded for the invoice.",
45
- },
46
- {
47
- id: "d3",
48
- field: "Incoterm",
49
- values: [
50
- { source: "Shipping instruction", value: "FOB" },
51
- { source: "Commercial invoice", value: "CIF", recommended: true },
52
- ],
53
- note: "The commercial invoice governs the sale terms — CIF matches the freight prepaid on the bill of lading.",
54
- },
55
- {
56
- id: "d4",
57
- field: "HS code",
58
- values: [
59
- { source: "Commercial invoice", value: "0306.17", recommended: true },
60
- { source: "Customs draft", value: "0306.16" },
61
- ],
62
- note: "0306.16 is cold-water shrimp; the goods are warm-water Penaeidae → 0306.17. The CI is right — fix the draft.",
63
- },
64
- ];
65
-
66
- const CHECKED = 22;
67
-
68
- export function TplCrosscheck() {
69
- const [res, setRes] = useState<Record<string, { idx?: number; flagged?: boolean }>>({});
70
-
71
- const resolvedCount = Object.values(res).filter((r) => r.idx != null || r.flagged).length;
72
- const openCount = SEED.length - resolvedCount;
73
-
74
- return (
75
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
76
- <View style={{ width: "100%", maxWidth: 760, alignSelf: "center", gap: 16 }}>
77
- <View style={{ gap: 2 }}>
78
- <Text size="xl" weight="semibold">Document cross-check</Text>
79
- <Text size="sm" color="muted">Shipment HBL-4471 · Shipping instruction · Commercial invoice · Packing list · Customs draft</Text>
80
- </View>
81
-
82
- <KPIStrip
83
- items={[
84
- { label: "Fields checked", value: CHECKED, format: "number" },
85
- { label: "In agreement", value: CHECKED - SEED.length, format: "number" },
86
- { label: "Disagree", value: openCount, format: "number", tone: openCount > 0 ? "danger" : "default", info: "Fields whose value differs across the documents — each needs a resolution before the set can be filed." },
87
- { label: "Resolved", value: resolvedCount, format: "number" },
88
- ]}
89
- />
90
-
91
- {openCount > 0 ? (
92
- <Callout tone="warning">
93
- <CalloutTitle>{openCount} of {CHECKED} fields disagree across the documents</CalloutTitle>
94
- <CalloutText>The agent reconciled the other {CHECKED - SEED.length}. Resolve each conflict to a source of truth, or flag it for the desk.</CalloutText>
95
- </Callout>
96
- ) : (
97
- <Callout tone="success">
98
- <CalloutTitle>All conflicts resolved</CalloutTitle>
99
- <CalloutText>Every field now has a single source of truth — the document set is ready to file.</CalloutText>
100
- </Callout>
101
- )}
102
-
103
- <View style={{ gap: 10 }}>
104
- {SEED.map((d) => (
105
- <Discrepancy
106
- key={d.id}
107
- field={d.field}
108
- values={d.values}
109
- note={d.note}
110
- resolvedIndex={res[d.id]?.idx}
111
- flagged={res[d.id]?.flagged}
112
- onResolve={(idx) => setRes((p) => ({ ...p, [d.id]: { idx } }))}
113
- onFlag={() => setRes((p) => ({ ...p, [d.id]: { flagged: true } }))}
114
- />
115
- ))}
116
- </View>
117
- </View>
118
- </ScrollView>
119
- );
120
- }