@lotics/ui 8.0.0 → 10.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 +177 -70
- 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_item_list.tsx +1015 -124
- 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 +16 -13
- package/examples/tpl_tasks.tsx +15 -28
- package/examples/tpl_tower.tsx +2 -2
- package/package.json +8 -7
- package/src/capture_row.tsx +59 -0
- package/src/checklist.tsx +104 -0
- package/src/chip.tsx +12 -3
- package/src/detail_row.tsx +137 -10
- 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 +25 -0
- package/src/page_header.tsx +0 -2
- package/src/popover_nav.tsx +40 -0
- package/src/progress_bar.tsx +32 -1
- package/src/record_summary.tsx +101 -0
- package/src/section_heading.tsx +16 -8
- package/src/suggestion_chip.tsx +47 -0
- package/src/trend_footer.tsx +3 -1
- package/src/use_screen_size.ts +1 -1
- package/src/use_section_nav.test.ts +69 -0
- package/src/use_section_nav.ts +59 -0
- package/examples/tpl_billing.tsx +0 -344
- package/examples/tpl_detail.tsx +0 -232
- package/examples/tpl_directory.tsx +0 -260
- package/examples/tpl_intake.tsx +0 -206
- package/examples/tpl_order.tsx +0 -482
- package/examples/tpl_quick.tsx +0 -211
- 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_wizard.tsx +0 -223
- package/src/animation_horizontal_slide.tsx +0 -75
- package/src/form_time_picker.tsx +0 -22
- package/src/highlighted_text.tsx +0 -92
- package/src/menu_title.tsx +0 -15
- package/src/pager_view.tsx +0 -167
- package/src/popover_header.tsx +0 -38
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { Pressable, ScrollView, View } from "react-native";
|
|
3
|
-
import { Text } from "@lotics/ui/text";
|
|
4
|
-
import { colors } from "@lotics/ui/colors";
|
|
5
|
-
import { ActionMenu } from "@lotics/ui/action_menu";
|
|
6
|
-
import { Avatar } from "@lotics/ui/avatar";
|
|
7
|
-
import { Badge } from "@lotics/ui/badge";
|
|
8
|
-
import { Button } from "@lotics/ui/button";
|
|
9
|
-
import { ChipGroup } from "@lotics/ui/chip_group";
|
|
10
|
-
import { FilterChip } from "@lotics/ui/filter_chip";
|
|
11
|
-
import { Counter } from "@lotics/ui/counter";
|
|
12
|
-
import { Card } from "@lotics/ui/card";
|
|
13
|
-
import { Divider } from "@lotics/ui/divider";
|
|
14
|
-
import { Drawer } from "@lotics/ui/drawer";
|
|
15
|
-
import { EmptyState } from "@lotics/ui/empty_state";
|
|
16
|
-
import { formatMoney } from "@lotics/ui/format_money";
|
|
17
|
-
import { StatusBadge } from "@lotics/ui/status_badge";
|
|
18
|
-
import { PressableRow } from "@lotics/ui/pressable_row";
|
|
19
|
-
import { SearchInput } from "@lotics/ui/search_input";
|
|
20
|
-
|
|
21
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
-
// Template · Directory — the partner directory every trading/logistics desk
|
|
23
|
-
// needs: one searchable, filterable register of customers and suppliers with
|
|
24
|
-
// the relationship status and volume at a glance.
|
|
25
|
-
//
|
|
26
|
-
// Grammar: zinc-50 canvas · one header band (count + ONE primary action) ·
|
|
27
|
-
// toolbar (live search + type tabs) · ONE banded card of rows separated by
|
|
28
|
-
// hairlines: Avatar → name + MST → type badge → tabular volume → StatusBadge.
|
|
29
|
-
// Doors: a partner row is the PRIMARY entity here — press opens the partner
|
|
30
|
-
// workspace Drawer (◀ ▶ / ←→ sequenced over the visible filtered ordering);
|
|
31
|
-
// ⋯ holds the row's quick operations (destructive last).
|
|
32
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
type PartnerKind = "khach" | "ncc";
|
|
35
|
-
|
|
36
|
-
interface Partner {
|
|
37
|
-
id: string;
|
|
38
|
-
ten: string;
|
|
39
|
-
mst: string;
|
|
40
|
-
kind: PartnerKind;
|
|
41
|
-
donHang: number;
|
|
42
|
-
doanhSo: number;
|
|
43
|
-
active: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const PARTNERS: Partner[] = [
|
|
47
|
-
{ id: "p1", ten: "Apex Plastics", mst: "0301452786", kind: "khach", donHang: 24, doanhSo: 860_000_000, active: true },
|
|
48
|
-
{ id: "p2", ten: "Eastland Packaging", mst: "0312908455", kind: "ncc", donHang: 18, doanhSo: 412_000_000, active: true },
|
|
49
|
-
{ id: "p3", ten: "Vista Paper Mills", mst: "0309977120", kind: "ncc", donHang: 31, doanhSo: 1_240_000_000, active: true },
|
|
50
|
-
{ id: "p4", ten: "Crestline Foods", mst: "0107643298", kind: "khach", donHang: 12, doanhSo: 340_000_000, active: true },
|
|
51
|
-
{ id: "p5", ten: "Harbor Engineering", mst: "0203318874", kind: "khach", donHang: 7, doanhSo: 156_000_000, active: false },
|
|
52
|
-
{ id: "p6", ten: "Summit Freight", mst: "0314226901", kind: "ncc", donHang: 42, doanhSo: 980_000_000, active: true },
|
|
53
|
-
{ id: "p7", ten: "Northway Appliances", mst: "0108812346", kind: "khach", donHang: 19, doanhSo: 624_000_000, active: true },
|
|
54
|
-
{ id: "p8", ten: "Brightline Inks", mst: "0305570213", kind: "ncc", donHang: 9, doanhSo: 87_000_000, active: false },
|
|
55
|
-
{ id: "p9", ten: "Phoenix Textiles", mst: "0300523847", kind: "khach", donHang: 28, doanhSo: 1_030_000_000, active: true },
|
|
56
|
-
{ id: "p10", ten: "Delta Chemicals", mst: "0204461185", kind: "ncc", donHang: 15, doanhSo: 295_000_000, active: true },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
const KIND_TABS = [
|
|
60
|
-
{ value: "all", label: "All" },
|
|
61
|
-
{ value: "khach", label: "Customers" },
|
|
62
|
-
{ value: "ncc", label: "Suppliers" },
|
|
63
|
-
] as const;
|
|
64
|
-
|
|
65
|
-
type KindTab = (typeof KIND_TABS)[number]["value"];
|
|
66
|
-
|
|
67
|
-
const kindLabel = (kind: PartnerKind) => (kind === "khach" ? "Customer" : "Supplier");
|
|
68
|
-
|
|
69
|
-
function PartnerRow({ p, selected, onPress }: { p: Partner; selected: boolean; onPress: () => void }) {
|
|
70
|
-
return (
|
|
71
|
-
<PressableRow onPress={onPress} selected={selected} style={{ minHeight: 64 }}>
|
|
72
|
-
<Pressable
|
|
73
|
-
accessibilityRole="button"
|
|
74
|
-
accessibilityLabel={`Open partner ${p.ten}`}
|
|
75
|
-
onPress={onPress}
|
|
76
|
-
style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 12 }}
|
|
77
|
-
>
|
|
78
|
-
<Avatar size={36} name={p.ten} />
|
|
79
|
-
<View style={{ flex: 1, gap: 2 }}>
|
|
80
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
81
|
-
<Text size="sm" weight="medium" numberOfLines={1}>{p.ten}</Text>
|
|
82
|
-
<Badge label={kindLabel(p.kind)} color={p.kind === "khach" ? "blue" : undefined} />
|
|
83
|
-
</View>
|
|
84
|
-
<Text size="xs" color="muted" tabular>MST {p.mst}</Text>
|
|
85
|
-
</View>
|
|
86
|
-
<Text size="sm" color="muted" tabular>
|
|
87
|
-
{p.donHang} orders · {formatMoney(p.doanhSo)}
|
|
88
|
-
</Text>
|
|
89
|
-
<StatusBadge enabled={p.active} label={p.active ? "Active" : "Paused"} />
|
|
90
|
-
</Pressable>
|
|
91
|
-
<ActionMenu
|
|
92
|
-
accessibilityLabel={`Actions for ${p.ten}`}
|
|
93
|
-
items={[
|
|
94
|
-
{ key: "don", label: "Create order", icon: "file-text", onPress: () => {} },
|
|
95
|
-
{ key: "sua", label: "Edit details", icon: "pencil", onPress: () => {} },
|
|
96
|
-
p.active
|
|
97
|
-
? { key: "dung", label: "Pause partnership", icon: "pause", danger: true, onPress: () => {} }
|
|
98
|
-
: { key: "molai", label: "Resume partnership", icon: "history", onPress: () => {} },
|
|
99
|
-
]}
|
|
100
|
-
/>
|
|
101
|
-
</PressableRow>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function KVRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
106
|
-
return (
|
|
107
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, minHeight: 24 }}>
|
|
108
|
-
<Text size="sm" color="muted" style={{ width: 110 }}>{label}</Text>
|
|
109
|
-
{typeof children === "string" ? <Text size="sm">{children}</Text> : children}
|
|
110
|
-
</View>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// The drawer body — compact partner workspace: identity, the key facts already
|
|
115
|
-
// on the row, and the two real next steps. Keyed by partner id by the caller
|
|
116
|
-
// so nothing leaks across ◀ ▶ steps.
|
|
117
|
-
function PartnerWorkspace({ p }: { p: Partner }) {
|
|
118
|
-
return (
|
|
119
|
-
<>
|
|
120
|
-
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 24, gap: 20 }}>
|
|
121
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
|
|
122
|
-
<Avatar size={44} name={p.ten} />
|
|
123
|
-
<View style={{ gap: 4, flex: 1 }}>
|
|
124
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
125
|
-
<Badge label={kindLabel(p.kind)} color={p.kind === "khach" ? "blue" : undefined} />
|
|
126
|
-
<StatusBadge enabled={p.active} label={p.active ? "Active" : "Paused"} />
|
|
127
|
-
</View>
|
|
128
|
-
<Text size="xs" color="muted" tabular userSelect="auto">MST {p.mst}</Text>
|
|
129
|
-
</View>
|
|
130
|
-
</View>
|
|
131
|
-
|
|
132
|
-
<Divider />
|
|
133
|
-
|
|
134
|
-
<View style={{ gap: 6 }}>
|
|
135
|
-
<KVRow label="Partner type">{kindLabel(p.kind)}</KVRow>
|
|
136
|
-
<KVRow label="Tax ID"><Text size="sm" tabular userSelect="auto">{p.mst}</Text></KVRow>
|
|
137
|
-
<KVRow label="Orders"><Text size="sm" tabular>{`${p.donHang} orders`}</Text></KVRow>
|
|
138
|
-
<KVRow label="Revenue"><Text size="sm" tabular>{formatMoney(p.doanhSo)}</Text></KVRow>
|
|
139
|
-
</View>
|
|
140
|
-
</ScrollView>
|
|
141
|
-
|
|
142
|
-
{/* pinned footer — the workspace's escalation, commit action at the right edge */}
|
|
143
|
-
<View style={{ borderTopWidth: 1, borderTopColor: colors.border, paddingHorizontal: 20, paddingVertical: 14, flexDirection: "row", alignItems: "center", gap: 12 }}>
|
|
144
|
-
<Text size="xs" color="muted" style={{ flex: 1 }}>New orders attach to this partner — track them in the Orders module.</Text>
|
|
145
|
-
<Button title="Edit details" color="secondary" onPress={() => {}} />
|
|
146
|
-
<Button title="Create order" color="primary" onPress={() => {}} />
|
|
147
|
-
</View>
|
|
148
|
-
</>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function TplDirectory() {
|
|
153
|
-
const [search, setSearch] = useState("");
|
|
154
|
-
const [tab, setTab] = useState<KindTab>("all");
|
|
155
|
-
const [openId, setOpenId] = useState<string | null>(null);
|
|
156
|
-
const [minOrders, setMinOrders] = useState(0);
|
|
157
|
-
|
|
158
|
-
const q = search.trim().toLowerCase();
|
|
159
|
-
const matches = PARTNERS.filter(
|
|
160
|
-
(p) =>
|
|
161
|
-
(tab === "all" || p.kind === tab) &&
|
|
162
|
-
p.donHang >= minOrders &&
|
|
163
|
-
(q === "" || p.ten.toLowerCase().includes(q) || p.mst.includes(q)),
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
// The drawer sequences over the visible filtered ordering. When the open
|
|
167
|
-
// partner falls out of it (search/tab changed), the chevrons disappear but
|
|
168
|
-
// the drawer stays on the record.
|
|
169
|
-
const openPartner = PARTNERS.find((p) => p.id === openId) ?? null;
|
|
170
|
-
const seqIndex = openPartner ? matches.findIndex((p) => p.id === openPartner.id) : -1;
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<View style={{ flex: 1, backgroundColor: colors.white }}>
|
|
174
|
-
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 28 }}>
|
|
175
|
-
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
176
|
-
{/* header band */}
|
|
177
|
-
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|
|
178
|
-
<View style={{ gap: 2, flex: 1 }}>
|
|
179
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
180
|
-
<Text size="xl" weight="semibold">Directory</Text>
|
|
181
|
-
</View>
|
|
182
|
-
<Text size="sm" color="muted">Customers and suppliers — look anyone up by name or tax ID</Text>
|
|
183
|
-
</View>
|
|
184
|
-
<Button title="Add partner" color="primary" onPress={() => {}} />
|
|
185
|
-
</View>
|
|
186
|
-
|
|
187
|
-
{/* toolbar: live search + a partner-type lens. Wraps when narrow;
|
|
188
|
-
every control is 40px so a wrapped band reads as clean rows. */}
|
|
189
|
-
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 8 }}>
|
|
190
|
-
<View style={{ flexGrow: 1, flexBasis: 240, minWidth: 200, maxWidth: 360 }}>
|
|
191
|
-
<SearchInput
|
|
192
|
-
placeholder="Search by name or tax ID…"
|
|
193
|
-
accessibilityLabel="Search partners"
|
|
194
|
-
value={search}
|
|
195
|
-
onChangeText={setSearch}
|
|
196
|
-
/>
|
|
197
|
-
</View>
|
|
198
|
-
<ChipGroup
|
|
199
|
-
accessibilityLabel="Partner type"
|
|
200
|
-
options={KIND_TABS.map((t) => ({
|
|
201
|
-
label: `${t.label} · ${PARTNERS.filter((p) => t.value === "all" || p.kind === t.value).length}`,
|
|
202
|
-
value: t.value,
|
|
203
|
-
}))}
|
|
204
|
-
value={tab}
|
|
205
|
-
onValueChange={setTab}
|
|
206
|
-
/>
|
|
207
|
-
<FilterChip
|
|
208
|
-
label="Min orders"
|
|
209
|
-
summary={minOrders > 0 ? `≥ ${minOrders}` : undefined}
|
|
210
|
-
onClear={() => setMinOrders(0)}
|
|
211
|
-
clearLabel="Clear min-orders filter"
|
|
212
|
-
>
|
|
213
|
-
<View style={{ gap: 10 }}>
|
|
214
|
-
<Text size="sm" color="muted">Show partners with at least</Text>
|
|
215
|
-
<Counter
|
|
216
|
-
value={minOrders}
|
|
217
|
-
min={0}
|
|
218
|
-
max={50}
|
|
219
|
-
step={5}
|
|
220
|
-
onValueChange={setMinOrders}
|
|
221
|
-
accessibilityLabel="minimum orders"
|
|
222
|
-
format={(n) => `${n} orders`}
|
|
223
|
-
/>
|
|
224
|
-
</View>
|
|
225
|
-
</FilterChip>
|
|
226
|
-
</View>
|
|
227
|
-
|
|
228
|
-
{/* the register: one card, gap-separated rounded `register` rows (no per-row Divider) */}
|
|
229
|
-
<Card style={{ padding: 0 }}>
|
|
230
|
-
{matches.length === 0 ? (
|
|
231
|
-
<EmptyState
|
|
232
|
-
message="No partners match"
|
|
233
|
-
hint="Try another keyword or switch the type filter"
|
|
234
|
-
/>
|
|
235
|
-
) : (
|
|
236
|
-
<View style={{ paddingVertical: 6, gap: 2 }}>
|
|
237
|
-
{matches.map((p) => (
|
|
238
|
-
<PartnerRow key={p.id} p={p} selected={p.id === openId} onPress={() => setOpenId(p.id)} />
|
|
239
|
-
))}
|
|
240
|
-
</View>
|
|
241
|
-
)}
|
|
242
|
-
</Card>
|
|
243
|
-
</View>
|
|
244
|
-
</ScrollView>
|
|
245
|
-
|
|
246
|
-
{/* the partner workspace — body keyed by partner id so per-record state
|
|
247
|
-
resets when ◀ ▶ (or ←/→) steps through the visible list */}
|
|
248
|
-
{openPartner ? (
|
|
249
|
-
<Drawer
|
|
250
|
-
open onOpenChange={(o) => !o && setOpenId(null)} title={openPartner.ten} width={480}
|
|
251
|
-
onPrev={seqIndex > 0 ? () => setOpenId(matches[seqIndex - 1].id) : undefined}
|
|
252
|
-
onNext={seqIndex >= 0 && seqIndex < matches.length - 1 ? () => setOpenId(matches[seqIndex + 1].id) : undefined}
|
|
253
|
-
position={seqIndex >= 0 ? `${seqIndex + 1}/${matches.length}` : undefined}
|
|
254
|
-
>
|
|
255
|
-
<PartnerWorkspace key={openPartner.id} p={openPartner} />
|
|
256
|
-
</Drawer>
|
|
257
|
-
) : null}
|
|
258
|
-
</View>
|
|
259
|
-
);
|
|
260
|
-
}
|
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
|
-
}
|