@lotics/ui 7.8.0 → 7.9.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 +71 -32
- package/examples/tpl_allocate.tsx +6 -6
- package/examples/tpl_assistant.tsx +1 -1
- package/examples/tpl_attendance.tsx +1 -1
- package/examples/tpl_billing.tsx +2 -2
- package/examples/tpl_briefing.tsx +1 -1
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_compare.tsx +1 -1
- package/examples/tpl_crosscheck.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_detail.tsx +1 -1
- package/examples/tpl_dieline.tsx +2 -2
- package/examples/tpl_directory.tsx +5 -8
- package/examples/tpl_draft.tsx +2 -2
- package/examples/tpl_extract.tsx +1 -1
- package/examples/tpl_intake.tsx +1 -1
- package/examples/{tpl_dossier.tsx → tpl_item_list.tsx} +138 -97
- package/examples/tpl_lookup.tsx +2 -2
- package/examples/tpl_match.tsx +2 -2
- package/examples/tpl_order.tsx +1 -1
- package/examples/tpl_pick.tsx +64 -68
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_quick.tsx +1 -1
- package/examples/tpl_ratedesk.tsx +1 -1
- package/examples/tpl_record.tsx +1 -1
- package/examples/tpl_record_plain.tsx +1 -1
- package/examples/tpl_report.tsx +1 -1
- package/examples/tpl_rollup.tsx +1 -1
- package/examples/tpl_settings.tsx +1 -1
- package/examples/tpl_shifts.tsx +1 -1
- package/examples/tpl_stock.tsx +1 -1
- package/examples/tpl_task_board.tsx +2 -2
- package/examples/tpl_tasks.tsx +1 -1
- package/examples/tpl_timeline.tsx +1 -1
- package/examples/tpl_tower.tsx +1 -1
- package/examples/tpl_triage.tsx +1 -1
- package/examples/tpl_wizard.tsx +1 -1
- package/package.json +3 -1
- package/src/card.tsx +6 -13
- package/src/data_grid.tsx +1 -1
- package/src/kpi_strip.tsx +5 -5
- package/src/pressable_row.tsx +22 -10
- package/src/sort_header.tsx +9 -4
- package/src/summary_line.tsx +76 -0
- package/src/table.tsx +27 -11
- package/src/use_selection.test.ts +66 -0
- package/src/use_selection.ts +75 -0
- package/examples/app_orders.tsx +0 -405
- package/examples/tpl_approvals.tsx +0 -375
- package/examples/tpl_batch.tsx +0 -234
- package/examples/tpl_callsheet.tsx +0 -481
- package/examples/tpl_convert.tsx +0 -478
- package/examples/tpl_crm_desk.tsx +0 -541
- package/examples/tpl_dispatch.tsx +0 -289
- package/examples/tpl_inventory.tsx +0 -299
- package/examples/tpl_pipeline.tsx +0 -400
- package/examples/tpl_reconcile.tsx +0 -275
- package/examples/tpl_run.tsx +0 -235
|
@@ -6,36 +6,42 @@ import { ActionMenu, type ActionMenuItem } from "@lotics/ui/action_menu";
|
|
|
6
6
|
import { Alert } from "@lotics/ui/alert";
|
|
7
7
|
import { Badge } from "@lotics/ui/badge";
|
|
8
8
|
import { Button } from "@lotics/ui/button";
|
|
9
|
-
import {
|
|
9
|
+
import { CardFooter } from "@lotics/ui/card";
|
|
10
|
+
import { CheckboxInput } from "@lotics/ui/checkbox_input";
|
|
10
11
|
import { DetailRow } from "@lotics/ui/detail_row";
|
|
11
12
|
import { Divider } from "@lotics/ui/divider";
|
|
12
13
|
import { Drawer, DrawerFooter } from "@lotics/ui/drawer";
|
|
13
14
|
import { EmptyState } from "@lotics/ui/empty_state";
|
|
15
|
+
import { FloatingActionBar } from "@lotics/ui/floating_action_bar";
|
|
14
16
|
import { FormField } from "@lotics/ui/form_field";
|
|
15
17
|
import { type IconName } from "@lotics/ui/icon";
|
|
16
|
-
import {
|
|
18
|
+
import { SummaryLine } from "@lotics/ui/summary_line";
|
|
17
19
|
import { NumberInput } from "@lotics/ui/number_input";
|
|
18
20
|
import { Pagination } from "@lotics/ui/pagination";
|
|
19
21
|
import { Popover, PopoverContent, PopoverFooter, PopoverTrigger } from "@lotics/ui/popover";
|
|
20
22
|
import { Table, TableRow, TableCell, type TableColumn } from "@lotics/ui/table";
|
|
21
23
|
import { SearchInput } from "@lotics/ui/search_input";
|
|
22
|
-
import {
|
|
24
|
+
import { Select } from "@lotics/ui/select";
|
|
23
25
|
import { FilterChip, selectSummary } from "@lotics/ui/filter_chip";
|
|
24
26
|
import { OptionList } from "@lotics/ui/option_list";
|
|
25
27
|
import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
|
|
26
28
|
import { Combobox, ComboboxInput, ComboboxContent } from "@lotics/ui/combobox";
|
|
27
29
|
import type { PickerOption } from "@lotics/ui/picker";
|
|
28
30
|
import { formatMoney } from "@lotics/ui/format_money";
|
|
31
|
+
import { useSelection } from "@lotics/ui/use_selection";
|
|
29
32
|
|
|
30
33
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
-
// Template ·
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// (
|
|
36
|
-
//
|
|
37
|
-
// sequenced workspace Drawer
|
|
38
|
-
//
|
|
34
|
+
// Template · Item List — THE canonical register, and the ONE work-execution list
|
|
35
|
+
// shape. A flat, paginated, SELECTABLE list of records: one toolbar row (search +
|
|
36
|
+
// consolidated filters LEFT, New CTA RIGHT) · a light SummaryLine of the filtered
|
|
37
|
+
// view · a sortable, Divider-separated register where every row has a leading
|
|
38
|
+
// checkbox (a row that can't take the bulk action — here an "Awaiting docs" case —
|
|
39
|
+
// gets a DISABLED one), a per-row PRINT action button, and a ⋯ overflow (press the
|
|
40
|
+
// row = the sequenced workspace Drawer) · a select-all band · footer totals +
|
|
41
|
+
// Pagination · a FloatingActionBar for the bulk action over the ticked rows. The
|
|
42
|
+
// example is a case-document register, but this one shape subsumes the old
|
|
43
|
+
// approvals / dispatch / batch / run screens — register, per-row action, gated
|
|
44
|
+
// selection, and act-on-many all in one.
|
|
39
45
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
46
|
|
|
41
47
|
// The customer book the new-case form searches — find-or-create (pick an existing
|
|
@@ -120,26 +126,32 @@ function docFor(r: HoSo): { title: string; icon: IconName } {
|
|
|
120
126
|
return r.daThu ? TRANG_THAI[r.trangThai].doc : { title: "Print payment slip", icon: "receipt" };
|
|
121
127
|
}
|
|
122
128
|
|
|
123
|
-
// The
|
|
124
|
-
//
|
|
129
|
+
// The per-row Print button (the trailing action column) prints the stage's
|
|
130
|
+
// document; the ⋯ menu carries the secondary actions — payment (disabled once
|
|
131
|
+
// collected), edit, and the destructive cancel last.
|
|
125
132
|
function menuFor(r: HoSo): ActionMenuItem[] {
|
|
126
|
-
const doc = docFor(r);
|
|
127
133
|
return [
|
|
128
|
-
{ key: "chung_tu", label: doc.title, icon: doc.icon, onPress: () => {} },
|
|
129
134
|
{ key: "thanh_toan", label: "Record payment", icon: "credit-card", disabled: r.daThu, onPress: () => {} },
|
|
130
135
|
{ key: "sua", label: "Edit case", icon: "pencil", onPress: () => {} },
|
|
131
136
|
{ key: "huy", label: "Cancel case", icon: "trash", danger: true, onPress: () => Alert.alert("Cancel this case?", "The case is closed and removed from the active register — it can't be undone.", [{ text: "Keep case", style: "cancel" }, { text: "Cancel case", style: "destructive", onPress: () => {} }]) },
|
|
132
137
|
];
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
function HoSoRow({ hs, selected, onPress }: { hs: HoSo; selected: boolean; onPress: () => void }) {
|
|
140
|
+
function HoSoRow({ hs, selected, marked, selectable, onToggle, onPress }: { hs: HoSo; selected: boolean; marked: boolean; selectable: boolean; onToggle: (on: boolean) => void; onPress: () => void }) {
|
|
136
141
|
return (
|
|
137
142
|
<TableRow
|
|
138
143
|
onPress={onPress}
|
|
139
144
|
selected={selected}
|
|
145
|
+
marked={marked}
|
|
140
146
|
minHeight={56}
|
|
141
147
|
accessibilityLabel={`Open case ${hs.ma} — ${hs.khach}`}
|
|
142
|
-
|
|
148
|
+
leading={<CheckboxInput accessibilityLabel={selectable ? `Select ${hs.ma}` : `${hs.ma} — awaiting docs, not ready to export`} checked={marked} disabled={!selectable} onChange={onToggle} />}
|
|
149
|
+
trailing={
|
|
150
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
151
|
+
<Button title="Print" icon={docFor(hs).icon} color="secondary" onPress={() => {}} />
|
|
152
|
+
<ActionMenu items={menuFor(hs)} accessibilityLabel={`Actions for ${hs.ma}`} />
|
|
153
|
+
</View>
|
|
154
|
+
}
|
|
143
155
|
>
|
|
144
156
|
<TableCell>
|
|
145
157
|
<Text size="sm" weight="semibold" tabular>{hs.ma}</Text>
|
|
@@ -244,7 +256,7 @@ function TaoHoSoForm() {
|
|
|
244
256
|
);
|
|
245
257
|
}
|
|
246
258
|
|
|
247
|
-
export function
|
|
259
|
+
export function TplItemList() {
|
|
248
260
|
const [tab, setTab] = useState<Tab>("all");
|
|
249
261
|
const [search, setSearch] = useState("");
|
|
250
262
|
const [page, setPage] = useState(0);
|
|
@@ -252,6 +264,7 @@ export function TplDossier() {
|
|
|
252
264
|
const [sort, setSort] = useState<SortState | null>(null);
|
|
253
265
|
const [assignee, setAssignee] = useState<string[]>([]);
|
|
254
266
|
const [feeStatus, setFeeStatus] = useState<"paid" | "unpaid" | null>(null);
|
|
267
|
+
const sel = useSelection();
|
|
255
268
|
|
|
256
269
|
// Filter / sort changes reset paging — page 3 of the old ordering is
|
|
257
270
|
// meaningless under the new one.
|
|
@@ -271,7 +284,6 @@ export function TplDossier() {
|
|
|
271
284
|
|
|
272
285
|
const dem = (t: TrangThai) => HO_SO.filter((r) => r.trangThai === t).length;
|
|
273
286
|
const phiDaThu = HO_SO.filter((r) => r.daThu).reduce((s, r) => s + r.phi, 0);
|
|
274
|
-
const chuaThu = HO_SO.filter((r) => !r.daThu);
|
|
275
287
|
|
|
276
288
|
// Search + status tab filter FIRST, then paging over the filtered ordering.
|
|
277
289
|
const q = search.trim().toLowerCase();
|
|
@@ -296,99 +308,118 @@ export function TplDossier() {
|
|
|
296
308
|
const pageRows = sorted.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
|
297
309
|
const hasMore = (page + 1) * PAGE_SIZE < sorted.length;
|
|
298
310
|
|
|
311
|
+
// Bulk selection (the shared `useSelection` hook) — tick rows for the floating
|
|
312
|
+
// action bar; select-all spans the visible page and ticks persist across pages,
|
|
313
|
+
// so the bar's count is the running total. Selectability GATING stays here (the
|
|
314
|
+
// hook is agnostic): an "Awaiting docs" case is still gathering paperwork, so it
|
|
315
|
+
// can't be exported — only the selectable ids drive select-all, the rest get a
|
|
316
|
+
// DISABLED checkbox (the same shape as a blocked run line).
|
|
317
|
+
const isSelectable = (r: HoSo) => r.trangThai !== "chobosung";
|
|
318
|
+
const selectableIds = pageRows.filter(isSelectable).map((r) => r.ma);
|
|
319
|
+
|
|
299
320
|
// The drawer sequences over the CURRENTLY-VISIBLE page slice. If the open
|
|
300
321
|
// case falls out of view, the chevrons disappear but the drawer stays.
|
|
301
322
|
const openRow = openMa === null ? null : HO_SO.find((r) => r.ma === openMa) ?? null;
|
|
302
323
|
const seqIndex = openRow ? pageRows.findIndex((r) => r.ma === openRow.ma) : -1;
|
|
303
324
|
|
|
304
325
|
return (
|
|
305
|
-
<
|
|
326
|
+
<View style={{ flex: 1 }}>
|
|
327
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
306
328
|
<View style={{ width: "100%", maxWidth: 1040, alignSelf: "center", gap: 16 }}>
|
|
307
|
-
{/* header
|
|
308
|
-
<View style={{
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
<Text size="sm" color="muted">Track each case's status and print the right document for its stage</Text>
|
|
312
|
-
</View>
|
|
313
|
-
<TaoHoSoForm />
|
|
329
|
+
{/* header — title only; the New case action lives on the toolbar row */}
|
|
330
|
+
<View style={{ gap: 2 }}>
|
|
331
|
+
<Text size="xl" weight="semibold">Cases & documents</Text>
|
|
332
|
+
<Text size="sm" color="muted">Track each case's status and print the right document for its stage</Text>
|
|
314
333
|
</View>
|
|
315
334
|
|
|
316
|
-
{/*
|
|
317
|
-
|
|
318
|
-
<
|
|
319
|
-
|
|
320
|
-
{
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
<SearchInput
|
|
338
|
-
placeholder="Search by ID, name, or phone…"
|
|
339
|
-
value={search}
|
|
340
|
-
onChangeText={doiTimKiem}
|
|
341
|
-
accessibilityLabel="Search cases"
|
|
342
|
-
/>
|
|
343
|
-
</View>
|
|
344
|
-
<ChipGroup
|
|
345
|
-
accessibilityLabel="Filter by status"
|
|
346
|
-
options={TABS.map((t) => ({
|
|
347
|
-
label: `${t.label} · ${t.value === "all" ? HO_SO.length : dem(t.value)}`,
|
|
348
|
-
value: t.value,
|
|
349
|
-
}))}
|
|
350
|
-
value={tab}
|
|
351
|
-
onValueChange={chonTab}
|
|
352
|
-
/>
|
|
353
|
-
<FilterChip
|
|
354
|
-
label="Assignee"
|
|
355
|
-
summary={assigneeSummary}
|
|
356
|
-
onClear={() => { setAssignee([]); setPage(0); }}
|
|
357
|
-
clearLabel="Clear assignee filter"
|
|
358
|
-
>
|
|
359
|
-
<OptionList
|
|
360
|
-
search={{ mode: "none" }}
|
|
361
|
-
multi
|
|
362
|
-
options={ASSIGNEES}
|
|
363
|
-
value={assignee}
|
|
364
|
-
onValueChange={(v) => { setAssignee(v); setPage(0); }}
|
|
335
|
+
{/* toolbar — search + consolidated filters LEFT, New case RIGHT, one row.
|
|
336
|
+
A search is present, so status is a dropdown (Select), not pills. */}
|
|
337
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
338
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, alignItems: "center", flex: 1 }}>
|
|
339
|
+
<View style={{ flexGrow: 1, flexBasis: 220, minWidth: 200, maxWidth: 340 }}>
|
|
340
|
+
<SearchInput
|
|
341
|
+
placeholder="Search by ID, name, or phone…"
|
|
342
|
+
value={search}
|
|
343
|
+
onChangeText={doiTimKiem}
|
|
344
|
+
accessibilityLabel="Search cases"
|
|
345
|
+
/>
|
|
346
|
+
</View>
|
|
347
|
+
<Select
|
|
348
|
+
placeholder="Status"
|
|
349
|
+
options={TABS.map((t) => ({
|
|
350
|
+
label: `${t.label} · ${t.value === "all" ? HO_SO.length : dem(t.value)}`,
|
|
351
|
+
value: t.value,
|
|
352
|
+
}))}
|
|
353
|
+
value={tab}
|
|
354
|
+
onValueChange={chonTab}
|
|
355
|
+
accessibilityLabel="Filter by status"
|
|
365
356
|
/>
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
onClear={() => { setFeeStatus(null); setPage(0); }}
|
|
373
|
-
clearLabel="Clear fee filter"
|
|
374
|
-
>
|
|
375
|
-
{({ close }) => (
|
|
357
|
+
<FilterChip
|
|
358
|
+
label="Assignee"
|
|
359
|
+
summary={assigneeSummary}
|
|
360
|
+
onClear={() => { setAssignee([]); setPage(0); }}
|
|
361
|
+
clearLabel="Clear assignee filter"
|
|
362
|
+
>
|
|
376
363
|
<OptionList
|
|
377
364
|
search={{ mode: "none" }}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
365
|
+
multi
|
|
366
|
+
options={ASSIGNEES}
|
|
367
|
+
value={assignee}
|
|
368
|
+
onValueChange={(v) => { setAssignee(v); setPage(0); }}
|
|
382
369
|
/>
|
|
383
|
-
|
|
384
|
-
|
|
370
|
+
</FilterChip>
|
|
371
|
+
{/* Single-select sibling — the render-prop `close` shuts the popover on
|
|
372
|
+
pick (the multi above stays open to collect several values). */}
|
|
373
|
+
<FilterChip
|
|
374
|
+
label="Fee"
|
|
375
|
+
summary={feeStatus === "paid" ? "Paid" : feeStatus === "unpaid" ? "Unpaid" : undefined}
|
|
376
|
+
onClear={() => { setFeeStatus(null); setPage(0); }}
|
|
377
|
+
clearLabel="Clear fee filter"
|
|
378
|
+
>
|
|
379
|
+
{({ close }) => (
|
|
380
|
+
<OptionList
|
|
381
|
+
search={{ mode: "none" }}
|
|
382
|
+
options={[{ value: "paid", label: "Paid" }, { value: "unpaid", label: "Unpaid" }]}
|
|
383
|
+
value={feeStatus}
|
|
384
|
+
onValueChange={(v) => { setFeeStatus(v); setPage(0); }}
|
|
385
|
+
onRequestClose={close}
|
|
386
|
+
/>
|
|
387
|
+
)}
|
|
388
|
+
</FilterChip>
|
|
389
|
+
</View>
|
|
390
|
+
<TaoHoSoForm />
|
|
385
391
|
</View>
|
|
386
392
|
|
|
393
|
+
{/* light summary of the filtered register — each count drills into the matching tab above */}
|
|
394
|
+
<SummaryLine
|
|
395
|
+
items={[
|
|
396
|
+
{ label: "processing", value: dem("dangxuly") },
|
|
397
|
+
{ label: "awaiting docs", value: dem("chobosung"), tone: "warning", info: "Cases waiting on customer paperwork — their record isn't complete, so they can't be ticked for export until the docs arrive." },
|
|
398
|
+
{ label: "complete", value: dem("hoantat") },
|
|
399
|
+
{ label: "fees collected", value: phiDaThu, format: "currency", compact: true },
|
|
400
|
+
]}
|
|
401
|
+
/>
|
|
402
|
+
|
|
387
403
|
{/* the register — eyebrow band (Table header) → list rows → totals + paging */}
|
|
388
|
-
<
|
|
389
|
-
<Table
|
|
404
|
+
<View>
|
|
405
|
+
<Table
|
|
406
|
+
columns={COLUMNS}
|
|
407
|
+
leading={24}
|
|
408
|
+
trailing={132}
|
|
409
|
+
sort={sort}
|
|
410
|
+
onSort={onSort}
|
|
411
|
+
selectAll={
|
|
412
|
+
<CheckboxInput
|
|
413
|
+
accessibilityLabel="Select all cases on this page"
|
|
414
|
+
checked={sel.allSelected(selectableIds)}
|
|
415
|
+
indeterminate={sel.indeterminate(selectableIds)}
|
|
416
|
+
disabled={selectableIds.length === 0}
|
|
417
|
+
onChange={(on) => sel.setAll(selectableIds, on)}
|
|
418
|
+
/>
|
|
419
|
+
}
|
|
420
|
+
>
|
|
390
421
|
{pageRows.map((r) => (
|
|
391
|
-
<HoSoRow key={r.ma} hs={r} selected={r.ma === openMa} onPress={() => setOpenMa(r.ma)} />
|
|
422
|
+
<HoSoRow key={r.ma} hs={r} selected={r.ma === openMa} marked={sel.has(r.ma)} selectable={isSelectable(r)} onToggle={(on) => sel.toggle(r.ma, on)} onPress={() => setOpenMa(r.ma)} />
|
|
392
423
|
))}
|
|
393
424
|
</Table>
|
|
394
425
|
{pageRows.length === 0 ? (
|
|
@@ -410,8 +441,18 @@ export function TplDossier() {
|
|
|
410
441
|
onPageChange={setPage}
|
|
411
442
|
/>
|
|
412
443
|
</CardFooter>
|
|
413
|
-
</
|
|
444
|
+
</View>
|
|
414
445
|
</View>
|
|
446
|
+
</ScrollView>
|
|
447
|
+
|
|
448
|
+
{/* bulk action over the ticked cases — the shared floating bar */}
|
|
449
|
+
<FloatingActionBar
|
|
450
|
+
count={sel.count}
|
|
451
|
+
label={sel.count === 1 ? "case selected" : "cases selected"}
|
|
452
|
+
onClear={sel.clear}
|
|
453
|
+
>
|
|
454
|
+
<Button title="Export" color="primary" onPress={() => {}} />
|
|
455
|
+
</FloatingActionBar>
|
|
415
456
|
|
|
416
457
|
{/* the case workspace — sequenced over the visible page ordering */}
|
|
417
458
|
{openRow ? (
|
|
@@ -427,6 +468,6 @@ export function TplDossier() {
|
|
|
427
468
|
<HoSoWorkspace key={openRow.ma} hs={openRow} />
|
|
428
469
|
</Drawer>
|
|
429
470
|
) : null}
|
|
430
|
-
</
|
|
471
|
+
</View>
|
|
431
472
|
);
|
|
432
473
|
}
|
package/examples/tpl_lookup.tsx
CHANGED
|
@@ -191,7 +191,7 @@ export function TplLookup() {
|
|
|
191
191
|
}));
|
|
192
192
|
|
|
193
193
|
return (
|
|
194
|
-
<View style={{ flex: 1, flexDirection: "row", backgroundColor: colors.
|
|
194
|
+
<View style={{ flex: 1, flexDirection: "row", backgroundColor: colors.white }}>
|
|
195
195
|
{/* ── LEFT · command + session ───────────────────────────────── */}
|
|
196
196
|
<View style={{ width: 380, backgroundColor: colors.white, borderRightWidth: 1, borderRightColor: colors.zinc[200] }}>
|
|
197
197
|
<View style={{ paddingHorizontal: 20, paddingTop: 18, paddingBottom: 12, gap: 2 }}>
|
|
@@ -249,7 +249,7 @@ export function TplLookup() {
|
|
|
249
249
|
<View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
|
|
250
250
|
<View style={{ borderWidth: 1, borderColor: colors.zinc[200], borderRadius: 16, backgroundColor: colors.white, overflow: "hidden" }}>
|
|
251
251
|
{/* verdict header */}
|
|
252
|
-
<View style={{ padding: 22, gap: 10, backgroundColor: colors.
|
|
252
|
+
<View style={{ padding: 22, gap: 10, backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.zinc[200] }}>
|
|
253
253
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
254
254
|
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Suggested HS code</Text>
|
|
255
255
|
<Confidence level={active.confidence} />
|
package/examples/tpl_match.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import type { ConfidenceLevel } from "@lotics/ui/confidence";
|
|
|
16
16
|
// (a known item ↔ the agent's proposed counterpart) with a confidence on the
|
|
17
17
|
// bridge. High-confidence pairs batch-accept; the rest are worked one by one.
|
|
18
18
|
// Same shape for bank-line ↔ invoice, invoice ↔ PO, dedup A ↔ B, entity
|
|
19
|
-
// resolution.
|
|
19
|
+
// resolution. Strip the agent's reasoning and it's just a deterministic match register.
|
|
20
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
21
|
|
|
22
22
|
interface M {
|
|
@@ -64,7 +64,7 @@ export function TplMatch() {
|
|
|
64
64
|
const highOpen = open.filter((r) => r.confidence === "high" && r.match).length;
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
67
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
68
68
|
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
69
69
|
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
|
|
70
70
|
<View style={{ flex: 1, gap: 2 }}>
|
package/examples/tpl_order.tsx
CHANGED
|
@@ -213,7 +213,7 @@ export function TplOrder() {
|
|
|
213
213
|
const ready = customer !== null && lines.length > 0 && lines.every((l) => l.product !== "" && !l.editing);
|
|
214
214
|
|
|
215
215
|
return (
|
|
216
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
216
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
217
217
|
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
218
218
|
{/* header band */}
|
|
219
219
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
package/examples/tpl_pick.tsx
CHANGED
|
@@ -2,9 +2,9 @@ import { useState } from "react";
|
|
|
2
2
|
import { ScrollView, View } from "react-native";
|
|
3
3
|
import { Text } from "@lotics/ui/text";
|
|
4
4
|
import { colors, solid } from "@lotics/ui/colors";
|
|
5
|
+
import { Accordion, AccordionContent, AccordionHeader, AccordionMeta, AccordionTitle } from "@lotics/ui/accordion";
|
|
5
6
|
import { Badge } from "@lotics/ui/badge";
|
|
6
7
|
import { Button } from "@lotics/ui/button";
|
|
7
|
-
import { Card } from "@lotics/ui/card";
|
|
8
8
|
import { ChipGroup } from "@lotics/ui/chip_group";
|
|
9
9
|
import { Counter } from "@lotics/ui/counter";
|
|
10
10
|
import { Divider } from "@lotics/ui/divider";
|
|
@@ -18,11 +18,11 @@ import { CompletionState } from "@lotics/ui/completion_state";
|
|
|
18
18
|
// Template · Guided execution — a warehouse PICK RUN. The work is a SEQUENCE of
|
|
19
19
|
// physical tasks, not a list to browse: the run hands the operator the next bin,
|
|
20
20
|
// they scan to verify they're at the right shelf, confirm the count, and the
|
|
21
|
-
// next task takes its place.
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// flags-and-advances without stalling
|
|
21
|
+
// next task takes its place. A single focus column — the CURRENT task is the only
|
|
22
|
+
// thing in view (where to walk, what to take, how many); the whole PICK PATH is a
|
|
23
|
+
// COLLAPSIBLE section below, collapsed by default, so position is one tap away and
|
|
24
|
+
// never competes with the task for the screen. Scan-to-verify, can't-skip
|
|
25
|
+
// ordering, and a short-pick exception that flags-and-advances without stalling.
|
|
26
26
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
28
|
interface PickLine {
|
|
@@ -92,7 +92,7 @@ function CurrentTask({ line, index, total, onConfirm, onShort }: {
|
|
|
92
92
|
const [scan, setScan] = useState("");
|
|
93
93
|
const matched = scan.trim().toUpperCase() === line.bin.toUpperCase();
|
|
94
94
|
return (
|
|
95
|
-
<
|
|
95
|
+
<View>
|
|
96
96
|
<View style={{ padding: 24, gap: 18 }}>
|
|
97
97
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
98
98
|
<Badge label={line.zone} color="blue" />
|
|
@@ -132,7 +132,7 @@ function CurrentTask({ line, index, total, onConfirm, onShort }: {
|
|
|
132
132
|
<View style={{ flex: 1 }} />
|
|
133
133
|
<Button title={`Confirm ${line.qty}`} color="primary" onPress={onConfirm} />
|
|
134
134
|
</View>
|
|
135
|
-
</
|
|
135
|
+
</View>
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -140,51 +140,42 @@ function Complete({ lines, onReset }: { lines: PickLine[]; onReset: () => void }
|
|
|
140
140
|
const shorts = lines.filter((l) => l.status === "short").length;
|
|
141
141
|
const units = lines.reduce((s, l) => s + (l.status === "short" ? l.picked ?? 0 : l.qty), 0);
|
|
142
142
|
return (
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
</CompletionState>
|
|
151
|
-
</Card>
|
|
143
|
+
<CompletionState
|
|
144
|
+
title="Wave picked"
|
|
145
|
+
summary={`${lines.length} lines · ${units} units${shorts > 0 ? ` · ${shorts} short` : ""}`}
|
|
146
|
+
>
|
|
147
|
+
<Button title="Start next wave" color="muted" onPress={onReset} />
|
|
148
|
+
<Button title="Hand to packing" color="primary" onPress={() => {}} />
|
|
149
|
+
</CompletionState>
|
|
152
150
|
);
|
|
153
151
|
}
|
|
154
152
|
|
|
155
|
-
// The
|
|
156
|
-
//
|
|
153
|
+
// The whole run at a glance: done · now · up next. Read-only — the run is
|
|
154
|
+
// sequential, so position is shown, not chosen. Lives in a collapsed disclosure
|
|
155
|
+
// below the task so the current step keeps the focus.
|
|
157
156
|
function RunPath({ lines, currentIndex }: { lines: PickLine[]; currentIndex: number }) {
|
|
158
157
|
return (
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)}
|
|
181
|
-
</View>
|
|
182
|
-
</Step>
|
|
183
|
-
);
|
|
184
|
-
})}
|
|
185
|
-
</Stepper>
|
|
186
|
-
</View>
|
|
187
|
-
</Card>
|
|
158
|
+
<Stepper orientation="vertical" accessibilityLabel="Pick path">
|
|
159
|
+
{lines.map((l, i) => {
|
|
160
|
+
const status: StepStatus =
|
|
161
|
+
l.status === "short" ? "warning" : l.status === "picked" ? "done" : i === currentIndex ? "current" : "upcoming";
|
|
162
|
+
return (
|
|
163
|
+
<Step key={l.id} status={status}>
|
|
164
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 12, flex: 1 }}>
|
|
165
|
+
<View style={{ flex: 1, gap: 1 }}>
|
|
166
|
+
<Text size="sm">{l.bin}</Text>
|
|
167
|
+
<Text size="xs" color="muted" numberOfLines={1}>{l.item}</Text>
|
|
168
|
+
</View>
|
|
169
|
+
{l.status === "short" ? (
|
|
170
|
+
<Badge variant="dot" label={`${l.picked ?? 0} of ${l.qty}`} color="amber" />
|
|
171
|
+
) : (
|
|
172
|
+
<Text size="sm" color="muted" tabular>{`×${l.qty}`}</Text>
|
|
173
|
+
)}
|
|
174
|
+
</View>
|
|
175
|
+
</Step>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
</Stepper>
|
|
188
179
|
);
|
|
189
180
|
}
|
|
190
181
|
|
|
@@ -203,8 +194,8 @@ export function TplPick() {
|
|
|
203
194
|
setLines((prev) => prev.map((l) => (l.id === id ? { ...l, status, picked } : l)));
|
|
204
195
|
|
|
205
196
|
return (
|
|
206
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
207
|
-
<View style={{ width: "100%", maxWidth:
|
|
197
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
198
|
+
<View style={{ width: "100%", maxWidth: 560, alignSelf: "center", gap: 16 }}>
|
|
208
199
|
{/* header — the wave + the cart it's filling */}
|
|
209
200
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12, flexWrap: "wrap" }}>
|
|
210
201
|
<View style={{ gap: 2, flex: 1 }}>
|
|
@@ -223,25 +214,30 @@ export function TplPick() {
|
|
|
223
214
|
<ProgressBar value={done.length} max={lines.length} format="none" color={solid("blue")} />
|
|
224
215
|
</View>
|
|
225
216
|
|
|
226
|
-
{/*
|
|
227
|
-
|
|
228
|
-
<
|
|
229
|
-
{current
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
217
|
+
{/* single focus — the current task is the only thing in view */}
|
|
218
|
+
{current ? (
|
|
219
|
+
<CurrentTask
|
|
220
|
+
line={current}
|
|
221
|
+
index={idx}
|
|
222
|
+
total={lines.length}
|
|
223
|
+
onConfirm={() => setStatus(current.id, "picked", current.qty)}
|
|
224
|
+
onShort={(actual) => setStatus(current.id, actual >= current.qty ? "picked" : "short", actual)}
|
|
225
|
+
/>
|
|
226
|
+
) : (
|
|
227
|
+
<Complete lines={lines} onReset={() => setLines(WAVE)} />
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* the whole route, secondary — a collapsed disclosure under the task,
|
|
231
|
+
never a persistent side panel */}
|
|
232
|
+
<Accordion defaultExpanded={false}>
|
|
233
|
+
<AccordionHeader accessibilityLabel="Pick path">
|
|
234
|
+
<AccordionTitle>Pick path</AccordionTitle>
|
|
235
|
+
<AccordionMeta>{`${done.length} of ${lines.length} done`}</AccordionMeta>
|
|
236
|
+
</AccordionHeader>
|
|
237
|
+
<AccordionContent>
|
|
242
238
|
<RunPath lines={lines} currentIndex={idx} />
|
|
243
|
-
</
|
|
244
|
-
</
|
|
239
|
+
</AccordionContent>
|
|
240
|
+
</Accordion>
|
|
245
241
|
</View>
|
|
246
242
|
</ScrollView>
|
|
247
243
|
);
|
package/examples/tpl_pivot.tsx
CHANGED
|
@@ -125,7 +125,7 @@ export function TplPivot() {
|
|
|
125
125
|
const lowOrOut = SKUS.filter((s) => s.status !== "in_stock").length;
|
|
126
126
|
|
|
127
127
|
return (
|
|
128
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
128
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
129
129
|
<View style={{ width: "100%", maxWidth: 960, alignSelf: "center", gap: 16 }}>
|
|
130
130
|
<View style={{ gap: 2 }}>
|
|
131
131
|
<Text size="xl" weight="semibold">Stock distribution</Text>
|
package/examples/tpl_quick.tsx
CHANGED
|
@@ -104,7 +104,7 @@ export function TplQuick() {
|
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
return (
|
|
107
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
107
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
108
108
|
<View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
|
|
109
109
|
<View style={{ gap: 2 }}>
|
|
110
110
|
<Text size="xl" weight="semibold">Log activity</Text>
|
|
@@ -174,7 +174,7 @@ export function TplRatedesk() {
|
|
|
174
174
|
|
|
175
175
|
return (
|
|
176
176
|
<>
|
|
177
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
177
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ paddingHorizontal: 24, paddingTop: 20, paddingBottom: 72, gap: 18 }}>
|
|
178
178
|
{/* HEADER BAND — the register is the page; updating prices is one CTA */}
|
|
179
179
|
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 12 }}>
|
|
180
180
|
<View style={{ gap: 2, flex: 1 }}>
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -185,7 +185,7 @@ export function TplRecord() {
|
|
|
185
185
|
const money = (v: number | null) => (v == null ? "" : `$${v.toLocaleString("en-US")}`);
|
|
186
186
|
|
|
187
187
|
return (
|
|
188
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
188
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
189
189
|
<View style={{ width: "100%", maxWidth: 560, alignSelf: "center", gap: 16 }}>
|
|
190
190
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
191
191
|
<Text size="xl" weight="semibold">
|