@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.
Files changed (75) hide show
  1. package/AGENTS.md +298 -159
  2. package/examples/tpl_allocate.tsx +2 -2
  3. package/examples/tpl_attendance.tsx +2 -2
  4. package/examples/tpl_calendar.tsx +1 -1
  5. package/examples/tpl_dashboard.tsx +1 -1
  6. package/examples/tpl_dieline.tsx +3 -3
  7. package/examples/tpl_documents.tsx +790 -0
  8. package/examples/tpl_item_list.tsx +1015 -124
  9. package/examples/tpl_lookup.tsx +37 -10
  10. package/examples/tpl_pick.tsx +3 -3
  11. package/examples/tpl_pivot.tsx +1 -1
  12. package/examples/tpl_record.tsx +1354 -0
  13. package/examples/tpl_report.tsx +7 -7
  14. package/examples/tpl_rollup.tsx +6 -6
  15. package/examples/tpl_shifts.tsx +2 -2
  16. package/examples/tpl_statements.tsx +221 -0
  17. package/examples/tpl_stock.tsx +7 -7
  18. package/examples/tpl_task_board.tsx +48 -32
  19. package/examples/tpl_tasks.tsx +47 -47
  20. package/examples/tpl_tower.tsx +2 -2
  21. package/package.json +9 -9
  22. package/src/agent_run.tsx +1 -1
  23. package/src/capture_row.tsx +59 -0
  24. package/src/change_review.tsx +732 -236
  25. package/src/checklist.tsx +104 -0
  26. package/src/chip.tsx +12 -3
  27. package/src/composer.tsx +22 -1
  28. package/src/confidence.tsx +1 -1
  29. package/src/control_surface.ts +0 -14
  30. package/src/detail_row.tsx +137 -10
  31. package/src/finding.tsx +112 -80
  32. package/src/floating_action_bar.tsx +1 -1
  33. package/src/inline_date_picker.tsx +8 -3
  34. package/src/inline_edit.tsx +40 -10
  35. package/src/inline_member_select.tsx +3 -0
  36. package/src/inline_number_input.tsx +5 -2
  37. package/src/inline_select.tsx +8 -3
  38. package/src/inline_tag_select.tsx +140 -0
  39. package/src/inline_text_input.tsx +5 -2
  40. package/src/inline_time_picker.tsx +5 -2
  41. package/src/ledger.tsx +220 -0
  42. package/src/locale.tsx +43 -16
  43. package/src/progress_bar.tsx +32 -1
  44. package/src/record_summary.tsx +101 -0
  45. package/src/section_heading.tsx +16 -8
  46. package/src/sources.tsx +8 -5
  47. package/src/suggestion_chip.tsx +47 -0
  48. package/src/use_section_nav.test.ts +69 -0
  49. package/src/use_section_nav.ts +59 -0
  50. package/examples/tpl_assistant.tsx +0 -174
  51. package/examples/tpl_billing.tsx +0 -344
  52. package/examples/tpl_briefing.tsx +0 -121
  53. package/examples/tpl_compare.tsx +0 -133
  54. package/examples/tpl_crosscheck.tsx +0 -120
  55. package/examples/tpl_detail.tsx +0 -232
  56. package/examples/tpl_directory.tsx +0 -260
  57. package/examples/tpl_draft.tsx +0 -163
  58. package/examples/tpl_extract.tsx +0 -193
  59. package/examples/tpl_intake.tsx +0 -206
  60. package/examples/tpl_match.tsx +0 -134
  61. package/examples/tpl_order.tsx +0 -482
  62. package/examples/tpl_quick.tsx +0 -211
  63. package/examples/tpl_ratedesk.tsx +0 -386
  64. package/examples/tpl_record_plain.tsx +0 -259
  65. package/examples/tpl_settings.tsx +0 -178
  66. package/examples/tpl_timeline.tsx +0 -244
  67. package/examples/tpl_triage.tsx +0 -112
  68. package/examples/tpl_wizard.tsx +0 -223
  69. package/src/discrepancy.tsx +0 -114
  70. package/src/match_sides.tsx +0 -79
  71. package/src/record_fields.tsx +0 -68
  72. package/src/review_card.tsx +0 -172
  73. package/src/scored_option.tsx +0 -138
  74. package/src/spec_list.tsx +0 -81
  75. package/src/triage_row.tsx +0 -99
@@ -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
- }
@@ -1,232 +0,0 @@
1
- import { useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { Text } from "@lotics/ui/text";
4
- import { colors, solid } from "@lotics/ui/colors";
5
- import { Badge } from "@lotics/ui/badge";
6
- import { Button } from "@lotics/ui/button";
7
- import { Card, CardBody } from "@lotics/ui/card";
8
- import { Divider } from "@lotics/ui/divider";
9
- import { FileBadge } from "@lotics/ui/file_badge";
10
- import { formatMoney } from "@lotics/ui/format_money";
11
- import { KPICard } from "@lotics/ui/kpi_card";
12
- import { Stepper } from "@lotics/ui/stepper";
13
- import { Tabs } from "@lotics/ui/tabs";
14
- import { Timeline, TimelineItem } from "@lotics/ui/timeline";
15
-
16
- // ─────────────────────────────────────────────────────────────────────────────
17
- // Template · Record detail — a single order's detail screen. Bands:
18
- // breadcrumb line · header (id + status + actions) · stat rail (one card,
19
- // five hairline columns) · Tabs with real switching: Overview (pipeline +
20
- // key-value list) / Documents (file rows) / Audit log (timeline — every event
21
- // expands in place; document events link to the Documents tab).
22
- // ─────────────────────────────────────────────────────────────────────────────
23
-
24
- const PIPELINE = ["Intake", "Order", "Prod", "Ship", "Recon", "Paid"];
25
- const PIPE_AT = 3;
26
-
27
- const FIELDS: { label: string; value: string }[] = [
28
- { label: "Order received", value: "12/05" },
29
- { label: "Customer PO", value: "NEW-2668" },
30
- { label: "Specification", value: "500×300×200 · 5-ply BC" },
31
- { label: "Unit price", value: `${formatMoney(14_000)}/box` },
32
- { label: "Delivered", value: "1,800 / 1,800" },
33
- { label: "Payment terms", value: "Net 30" },
34
- ];
35
-
36
- const FILES: { name: string; mime: string; size: string }[] = [
37
- { name: "Quote_QT-2026-0042.pdf", mime: "application/pdf", size: "182 KB" },
38
- { name: "ProductionOrder_MO-2026-0042.pdf", mime: "application/pdf", size: "96 KB" },
39
- { name: "DeliveryNote_DN-0042.xlsx", mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", size: "24 KB" },
40
- ];
41
-
42
- // Every log event expands in place (Timeline rows become pressable when they
43
- // carry details) — the read-only drill-down door. Events that produced a
44
- // a document also carry the real path to it (the Documents tab).
45
- const LOG: {
46
- id: string;
47
- icon: TimelineItem["icon"];
48
- iconColor: string;
49
- label: string;
50
- time: string;
51
- detail: string;
52
- chungTu?: string;
53
- }[] = [
54
- {
55
- id: "log-5",
56
- icon: "circle-check",
57
- iconColor: solid("emerald"),
58
- label: "Delivered in full 1,800/1,800 — awaiting note reconciliation",
59
- time: "04/06 16:40",
60
- detail: "Customer signed for all 1,800 boxes · reconcile against DN-0042 before closing receivables.",
61
- },
62
- {
63
- id: "log-4",
64
- icon: "package",
65
- iconColor: solid("blue"),
66
- label: "Shipped 1,800 boxes — loaded on truck 29C-123.45",
67
- time: "04/06 07:15",
68
- detail: "Dispatched from finished-goods warehouse · truck 29C-123.45 · driver Ben Tran.",
69
- chungTu: "DeliveryNote_DN-0042.xlsx",
70
- },
71
- {
72
- id: "log-3",
73
- icon: "check",
74
- iconColor: solid("blue"),
75
- label: "Production complete — moved to finished goods",
76
- time: "02/06 17:05",
77
- detail: "Completed at the Eastside plant · 1,800 boxes received into finished goods.",
78
- },
79
- {
80
- id: "log-2",
81
- icon: "file-text",
82
- iconColor: solid("blue"),
83
- label: "Production order MO-2026-0042 issued to the plant",
84
- time: "26/05 09:20",
85
- detail: "Sent to the Eastside plant · 5-ply BC blanks · die KB-118 on hand.",
86
- chungTu: "ProductionOrder_MO-2026-0042.pdf",
87
- },
88
- {
89
- id: "log-1",
90
- icon: "plus",
91
- iconColor: colors.zinc[400],
92
- label: "Order created from PO NEW-2668 — Tessa Tran",
93
- time: "12/05 10:02",
94
- detail: "Created from PO NEW-2668 received by email · created by Tessa Tran.",
95
- },
96
- ];
97
-
98
- const STATS = [
99
- { label: "Customer", value: "NEWTECONS" },
100
- { label: "Quantity", value: "1,800" },
101
- { label: "Value", value: formatMoney(25_200_000) },
102
- { label: "Needed by", value: "28/05", tone: "danger" as const },
103
- { label: "Owner", value: "Tessa Tran" },
104
- ];
105
-
106
- const TABS = [
107
- { label: "Overview", value: "tongquan" },
108
- { label: "Documents", value: "chungtu" },
109
- { label: "Audit log", value: "nhatky" },
110
- ];
111
-
112
- function TongQuan() {
113
- return (
114
- <View style={{ gap: 12 }}>
115
- <Card style={{ padding: 0 }}>
116
- <CardBody style={{ gap: 12 }}>
117
- <Text size="xs" color="muted" transform="uppercase">
118
- Pipeline
119
- </Text>
120
- {/* a detail screen has room for the journey itself — every
121
- milestone labeled, the current one marked */}
122
- <Stepper steps={PIPELINE} current={PIPE_AT} color={solid("blue")} />
123
- <Text size="xs" color="muted">
124
- Delivered in full — awaiting reconciliation
125
- </Text>
126
- </CardBody>
127
- </Card>
128
- <Card style={{ padding: 0 }}>
129
- {FIELDS.map((f, i) => (
130
- <View key={f.label}>
131
- {i > 0 ? <Divider /> : null}
132
- <View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12, gap: 16 }}>
133
- <Text size="sm" color="muted">{f.label}</Text>
134
- <View style={{ flex: 1 }} />
135
- <Text size="sm" weight="medium" tabular align="right">
136
- {f.value}
137
- </Text>
138
- </View>
139
- </View>
140
- ))}
141
- </Card>
142
- </View>
143
- );
144
- }
145
-
146
- function ChungTu() {
147
- return (
148
- <Card style={{ padding: 0 }}>
149
- {FILES.map((f, i) => (
150
- <View key={f.name}>
151
- {i > 0 ? <Divider /> : null}
152
- <View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12, gap: 12 }}>
153
- <FileBadge mimeType={f.mime} size={24} />
154
- <Text size="sm" weight="medium" numberOfLines={1} style={{ flexShrink: 1 }}>{f.name}</Text>
155
- <Text size="xs" color="muted" tabular>{f.size}</Text>
156
- <View style={{ flex: 1 }} />
157
- <Button title="Download" color="muted" onPress={() => {}} />
158
- </View>
159
- </View>
160
- ))}
161
- </Card>
162
- );
163
- }
164
-
165
- function NhatKy({ onMoChungTu }: { onMoChungTu: () => void }) {
166
- const items: TimelineItem[] = LOG.map((e) => ({
167
- id: e.id,
168
- icon: e.icon,
169
- iconColor: e.iconColor,
170
- label: e.label,
171
- right: <Text size="xs" color="muted" tabular>{e.time}</Text>,
172
- details: (
173
- <View style={{ gap: 8 }}>
174
- <Text size="xs" color="muted">{e.detail}</Text>
175
- {e.chungTu ? (
176
- <View style={{ flexDirection: "row" }}>
177
- <Button title="Open document" color="muted" onPress={onMoChungTu} />
178
- </View>
179
- ) : null}
180
- </View>
181
- ),
182
- }));
183
- return (
184
- <Card style={{ padding: 16 }}>
185
- <Timeline items={items} />
186
- </Card>
187
- );
188
- }
189
-
190
- export function TplDetail() {
191
- const [tab, setTab] = useState<string>("tongquan");
192
-
193
- return (
194
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
195
- <View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
196
- {/* breadcrumb line */}
197
- <Text size="xs" color="muted">Orders / SO-2026-0042</Text>
198
-
199
- {/* header band */}
200
- <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
201
- <Text size="xl" weight="semibold" tabular>SO-2026-0042</Text>
202
- <Badge label="Shipping" color="blue" />
203
- <Badge label="Overdue" color="red" />
204
- <View style={{ flex: 1 }} />
205
- <Button title="Print documents" color="secondary" onPress={() => {}} />
206
- <Button title="Update" color="primary" onPress={() => {}} />
207
- </View>
208
- <Text size="sm" color="muted">Carton box NEW 2668 500×300×200 — NEWTECONS</Text>
209
-
210
- {/* stat rail: one card, five KPI columns */}
211
- <Card style={{ padding: 0 }}>
212
- <View style={{ flexDirection: "row", paddingVertical: 14, paddingHorizontal: 16, gap: 16 }}>
213
- {STATS.map((s) => (
214
- <KPICard key={s.label} label={s.label} value={s.value} tone={s.tone} size="sm" style={{ flex: 1 }} />
215
- ))}
216
- </View>
217
- </Card>
218
-
219
- <Tabs
220
- accessibilityLabel="Record sections"
221
- options={TABS}
222
- selectedTab={tab}
223
- onSelectTab={setTab}
224
- />
225
-
226
- {tab === "tongquan" ? <TongQuan /> : null}
227
- {tab === "chungtu" ? <ChungTu /> : null}
228
- {tab === "nhatky" ? <NhatKy onMoChungTu={() => setTab("chungtu")} /> : null}
229
- </View>
230
- </ScrollView>
231
- );
232
- }