mercury-web/src/features/assets/TransactionPickerSheet.tsx

138 lines
5.7 KiB
TypeScript

"use client";
import * as React from "react";
import {
BottomSheetRoot,
BottomSheetBackdrop,
BottomSheetPositioner,
BottomSheetContent,
BottomSheetHeader,
BottomSheetTitle,
BottomSheetCloseButton,
BottomSheetBody,
TextFieldRoot,
TextFieldInput,
ListRoot,
ListItem,
Icon,
} from "@seed-design/react";
import { useTransactions, todayLocalDate } from "@/api/hooks/reads";
import type { Txn } from "@/api/schemas";
import { tugrikRaw } from "@/ds/money";
import { assetsStrings as s } from "./strings";
const closeSvg = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round">
<path d="M6 6l12 12M18 6L6 18" />
</svg>
);
export interface TransactionPickerSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** "expense" for a new loan, "income" for a repayment — matches
* `TransactionPickerView`'s direction filter on iOS. */
direction: "income" | "expense";
/** Transaction ids already linked to some loan/repayment, so already-linked
* rows can carry a "Холбоотой" hint (mirrors `LendingModel.linkedTxnIds`). */
linkedTxnIds?: Set<number>;
onPick: (txn: Txn) => void;
}
/**
* A searchable bottom sheet of the user's transactions, filtered to one
* direction, for linking a lending entry or repayment to the real transaction
* that created it. Ports `TransactionPickerView.swift` + `LendingAutofill.swift`
* (the filtering/candidate logic lives inline below, small enough not to need
* its own module).
*/
export function TransactionPickerSheet({ open, onOpenChange, direction, linkedTxnIds, onPick }: TransactionPickerSheetProps) {
const [search, setSearch] = React.useState("");
// Pull ~1 year so older loans/repayments stay linkable, matching iOS.
const from = React.useMemo(() => {
const d = new Date();
d.setFullYear(d.getFullYear() - 1);
return todayLocalDate(d);
}, []);
const to = React.useMemo(() => todayLocalDate(), []);
const { data: transactions, isLoading } = useTransactions({ direction, from, to, limit: 500 });
const shown = React.useMemo(() => {
const q = search.trim().toLowerCase();
return (transactions ?? []).filter((t) => {
// Pending holds (no stable txnId) can't be linked.
if (t.txnId == null || t.txnId === 0) return false;
if (!q) return true;
return t.title.toLowerCase().includes(q) || t.amount.toLowerCase().includes(q);
});
}, [transactions, search]);
return (
<BottomSheetRoot open={open} onOpenChange={onOpenChange}>
<BottomSheetBackdrop />
<BottomSheetPositioner>
<BottomSheetContent>
<BottomSheetHeader style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<BottomSheetTitle>{s.lending.picker.title}</BottomSheetTitle>
<BottomSheetCloseButton aria-label={s.common.cancel}>
<Icon svg={closeSvg} size="16px" />
</BottomSheetCloseButton>
</BottomSheetHeader>
<BottomSheetBody style={{ display: "flex", flexDirection: "column", gap: 10, maxHeight: "70vh", overflowY: "auto" }}>
<TextFieldRoot value={search} onValueChange={setSearch} name="txn-picker-search">
<TextFieldInput placeholder={s.lending.picker.search} aria-label={s.lending.picker.search} autoFocus />
</TextFieldRoot>
{isLoading ? (
<p style={{ margin: 0, color: "var(--seed-color-fg-neutral-subtle)" }}></p>
) : shown.length === 0 ? (
<p style={{ margin: 0, color: "var(--seed-color-fg-neutral-subtle)" }}>{s.lending.picker.empty}</p>
) : (
<ListRoot>
{shown.map((txn) => {
const linked = txn.txnId != null && linkedTxnIds?.has(txn.txnId);
return (
<ListItem key={`${txn.txnId}-${txn.date}`} style={{ padding: 0 }}>
<button
type="button"
onClick={() => onPick(txn)}
style={{
display: "flex",
width: "100%",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
background: "none",
border: "none",
textAlign: "left",
cursor: "pointer",
padding: "10px 4px",
font: "inherit",
color: "inherit",
}}
>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{txn.title}
</div>
<div style={{ fontSize: 12, color: "var(--seed-color-fg-neutral-subtle)" }}>{txn.date.slice(0, 10)}</div>
</div>
<div style={{ textAlign: "right", flexShrink: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600 }}>{tugrikRaw(txn.amount)}</div>
{linked && (
<div style={{ fontSize: 11, color: "var(--seed-color-fg-neutral-subtle)" }}>{s.lending.linkedBadge}</div>
)}
</div>
</button>
</ListItem>
);
})}
</ListRoot>
)}
</BottomSheetBody>
</BottomSheetContent>
</BottomSheetPositioner>
</BottomSheetRoot>
);
}