609 lines
22 KiB
TypeScript
609 lines
22 KiB
TypeScript
"use client";
|
||
|
||
import * as React from "react";
|
||
import Link from "next/link";
|
||
import {
|
||
BottomSheetRoot,
|
||
BottomSheetBackdrop,
|
||
BottomSheetPositioner,
|
||
BottomSheetContent,
|
||
BottomSheetHeader,
|
||
BottomSheetTitle,
|
||
BottomSheetBody,
|
||
BottomSheetFooter,
|
||
TextFieldRoot,
|
||
TextFieldInput,
|
||
Skeleton,
|
||
} from "@seed-design/react";
|
||
import { useNetWorth, useManualAssets, useLending } from "@/api/hooks/reads";
|
||
import { useManualAssetMutations, useLendingMutations } from "@/api/hooks/mutations";
|
||
import { Card, HideAmountsToggle, MercuryButton, IconChip, SectionHeader, EmptyState, Icon, type IconName } from "@/ds";
|
||
import { tugrik, tugrikRaw } from "@/ds/money";
|
||
import type { Account, ManualAsset, Lending } from "@/api/schemas";
|
||
import { assetsStrings as s } from "./strings";
|
||
import { useHideAmountsTick } from "./useHideAmountsTick";
|
||
import { ConfirmDialog } from "./ConfirmDialog";
|
||
|
||
const mutedStyle: React.CSSProperties = { color: "var(--seed-color-fg-neutral-subtle)" };
|
||
const rowStyle: React.CSSProperties = {
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
gap: 12,
|
||
padding: "14px 16px",
|
||
};
|
||
const titleStyle: React.CSSProperties = {
|
||
fontSize: 15,
|
||
fontWeight: 700,
|
||
overflow: "hidden",
|
||
textOverflow: "ellipsis",
|
||
whiteSpace: "nowrap",
|
||
};
|
||
const subtitleStyle: React.CSSProperties = { fontSize: 12, ...mutedStyle };
|
||
const amountStyle: React.CSSProperties = { fontSize: 15, fontWeight: 700, flexShrink: 0 };
|
||
|
||
// Bank/lending/asset chip tints — soft tones matching the categoryStyle
|
||
// palette used elsewhere in Mercury (transactions, planner).
|
||
const BANK_TINT = { tint: "#DDEAF6", fg: "#215C9A" };
|
||
const LENDING_TINT = { tint: "#E3F0D8", fg: "#3F7A1E" };
|
||
|
||
function assetChip(category: string): { icon: IconName; tint: string; fg: string } {
|
||
switch (category) {
|
||
case "car":
|
||
return { icon: "car", tint: "#DDE6F6", fg: "#2A4B9A" };
|
||
case "electronics":
|
||
return { icon: "monitor", tint: "#E1E5EA", fg: "#42505F" };
|
||
default:
|
||
return { icon: "layers", tint: "#EDEBE7", fg: "#6B6257" };
|
||
}
|
||
}
|
||
|
||
export function AssetsView() {
|
||
// Re-renders when the global hide-amounts flag flips (tugrik()/tugrikRaw()
|
||
// read it from localStorage synchronously, so this is how the numbers on
|
||
// this page react to the header toggle without a reload).
|
||
useHideAmountsTick();
|
||
|
||
const netWorth = useNetWorth();
|
||
const manualAssets = useManualAssets();
|
||
const lending = useLending();
|
||
const assetMutations = useManualAssetMutations();
|
||
const lendingMutations = useLendingMutations();
|
||
|
||
const [addAssetOpen, setAddAssetOpen] = React.useState(false);
|
||
const [addLoanOpen, setAddLoanOpen] = React.useState(false);
|
||
const [deleteAssetName, setDeleteAssetName] = React.useState<string | null>(null);
|
||
const [revalueAssetName, setRevalueAssetName] = React.useState<string | null>(null);
|
||
const [deleteLoan, setDeleteLoan] = React.useState<Lending | null>(null);
|
||
|
||
const accounts: Account[] = netWorth.data?.accounts ?? [];
|
||
const assets: ManualAsset[] = manualAssets.data ?? [];
|
||
const loans: Lending[] = lending.data ?? [];
|
||
|
||
return (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
|
||
<header style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>{s.header.title}</h1>
|
||
<HideAmountsToggle />
|
||
</header>
|
||
|
||
<NetWorthCard netWorth={netWorth.data} loading={netWorth.isLoading} />
|
||
|
||
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||
<SectionHeader title={s.accounts.title} />
|
||
{netWorth.isLoading ? (
|
||
<SkeletonRows count={2} />
|
||
) : accounts.length === 0 ? (
|
||
<Card style={{ padding: 0 }}>
|
||
<EmptyState icon="bank" title={s.accounts.empty} hint={s.accounts.emptyHint} />
|
||
</Card>
|
||
) : (
|
||
<Card style={{ padding: 0 }}>
|
||
{accounts.map((account, i) => (
|
||
<React.Fragment key={account.accountId}>
|
||
{i > 0 && <Divider />}
|
||
<Link
|
||
href={`/assets/account/${account.accountId}`}
|
||
style={{ ...rowStyle, color: "inherit", textDecoration: "none" }}
|
||
>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 12, minWidth: 0 }}>
|
||
<IconChip icon="bank" {...BANK_TINT} />
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||
<span style={titleStyle}>{account.bank}</span>
|
||
<span style={subtitleStyle}>{account.accountNumber}</span>
|
||
</div>
|
||
</div>
|
||
<span style={amountStyle}>{tugrik(account.balance)}</span>
|
||
</Link>
|
||
</React.Fragment>
|
||
))}
|
||
</Card>
|
||
)}
|
||
</section>
|
||
|
||
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||
<SectionHeader title={s.manualAssets.title} onAction={() => setAddAssetOpen(true)} actionLabel={s.manualAssets.add} />
|
||
{manualAssets.isLoading ? (
|
||
<SkeletonRows count={3} />
|
||
) : assets.length === 0 ? (
|
||
<Card style={{ padding: 0 }}>
|
||
<EmptyState icon="layers" title={s.manualAssets.emptyTitle} hint={s.manualAssets.emptySubtitle} />
|
||
</Card>
|
||
) : (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||
{assets.map((asset) => (
|
||
<ManualAssetRow
|
||
key={asset.name}
|
||
asset={asset}
|
||
revaluing={assetMutations.revalue.isPending && revalueAssetName === asset.name}
|
||
onRevalue={() => setRevalueAssetName(asset.name)}
|
||
onDelete={() => setDeleteAssetName(asset.name)}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<section style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||
<SectionHeader title={s.lending.title} onAction={() => setAddLoanOpen(true)} actionLabel={s.lending.add} />
|
||
{lending.isLoading ? (
|
||
<SkeletonRows count={2} />
|
||
) : loans.length === 0 ? (
|
||
<Card style={{ padding: 0 }}>
|
||
<EmptyState icon="hand-coins" title={s.lending.empty} hint={s.lending.emptyHint} />
|
||
</Card>
|
||
) : (
|
||
<Card style={{ padding: 0 }}>
|
||
{loans.map((loan, i) => (
|
||
<React.Fragment key={loan.id}>
|
||
{i > 0 && <Divider />}
|
||
<LoanRow loan={loan} onDelete={() => setDeleteLoan(loan)} />
|
||
</React.Fragment>
|
||
))}
|
||
</Card>
|
||
)}
|
||
</section>
|
||
|
||
{addAssetOpen && (
|
||
<AddManualAssetSheet
|
||
onClose={() => setAddAssetOpen(false)}
|
||
saving={assetMutations.add.isPending}
|
||
onSave={async (values) => {
|
||
await assetMutations.add.mutateAsync(values);
|
||
setAddAssetOpen(false);
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{addLoanOpen && (
|
||
<AddLoanSheet
|
||
onClose={() => setAddLoanOpen(false)}
|
||
saving={lendingMutations.create.isPending}
|
||
onSave={async (values) => {
|
||
await lendingMutations.create.mutateAsync(values);
|
||
setAddLoanOpen(false);
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
<ConfirmDialog
|
||
open={deleteAssetName !== null}
|
||
title={s.manualAssets.deleteTitle}
|
||
description={deleteAssetName ? s.manualAssets.deleteDescription(deleteAssetName) : undefined}
|
||
busy={assetMutations.delete.isPending}
|
||
onCancel={() => setDeleteAssetName(null)}
|
||
onConfirm={async () => {
|
||
if (!deleteAssetName) return;
|
||
await assetMutations.delete.mutateAsync(deleteAssetName);
|
||
setDeleteAssetName(null);
|
||
}}
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
open={revalueAssetName !== null}
|
||
title={s.manualAssets.revalueTitle}
|
||
description={s.manualAssets.revalueDescription}
|
||
confirmLabel={s.manualAssets.revalue}
|
||
destructive={false}
|
||
busy={assetMutations.revalue.isPending}
|
||
onCancel={() => setRevalueAssetName(null)}
|
||
onConfirm={async () => {
|
||
const name = revalueAssetName;
|
||
if (!name) return;
|
||
setRevalueAssetName(null);
|
||
await assetMutations.revalue.mutateAsync(name);
|
||
}}
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
open={deleteLoan !== null}
|
||
title={s.lending.deleteTitle}
|
||
description={deleteLoan ? s.lending.deleteDescription(deleteLoan.person) : undefined}
|
||
busy={lendingMutations.delete.isPending}
|
||
onCancel={() => setDeleteLoan(null)}
|
||
onConfirm={async () => {
|
||
if (!deleteLoan) return;
|
||
await lendingMutations.delete.mutateAsync(deleteLoan.id);
|
||
setDeleteLoan(null);
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// --- Net worth header -------------------------------------------------------
|
||
|
||
function NetWorthCard({ netWorth, loading }: { netWorth?: { total: string; assets: string; liabilities: string }; loading: boolean }) {
|
||
return (
|
||
<Card style={{ padding: "26px 22px", background: "var(--mercury-balance-card)" }}>
|
||
{loading ? (
|
||
<Skeleton height="40px" />
|
||
) : (
|
||
<>
|
||
<div style={{ fontSize: 13, fontWeight: 600, letterSpacing: 0.2, opacity: 0.75 }}>{s.netWorth.total}</div>
|
||
<div style={{ fontSize: 34, fontWeight: 800, marginTop: 8, letterSpacing: -0.5 }}>{tugrik(netWorth?.total ?? "0")}</div>
|
||
<div style={{ display: "flex", gap: 28, marginTop: 20 }}>
|
||
<div>
|
||
<div style={{ fontSize: 12, opacity: 0.75 }}>{s.netWorth.assets}</div>
|
||
<div style={{ fontSize: 15, fontWeight: 700, marginTop: 2 }}>{tugrik(netWorth?.assets ?? "0")}</div>
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 12, opacity: 0.75 }}>{s.netWorth.liabilities}</div>
|
||
<div style={{ fontSize: 15, fontWeight: 700, marginTop: 2 }}>{tugrik(netWorth?.liabilities ?? "0")}</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// --- Small shared bits -------------------------------------------------------
|
||
|
||
function SkeletonRows({ count }: { count: number }) {
|
||
return (
|
||
<Card style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||
{Array.from({ length: count }).map((_, i) => (
|
||
<Skeleton key={i} height="48px" style={{ borderRadius: "var(--seed-radius-r2, 8px)" }} />
|
||
))}
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
function Divider() {
|
||
return <div style={{ height: 1, background: "var(--seed-color-border-neutral, #e5e5e5)" }} />;
|
||
}
|
||
|
||
// --- Manual asset row ---------------------------------------------------
|
||
|
||
function ManualAssetRow({
|
||
asset,
|
||
revaluing,
|
||
onRevalue,
|
||
onDelete,
|
||
}: {
|
||
asset: ManualAsset;
|
||
revaluing: boolean;
|
||
onRevalue: () => void;
|
||
onDelete: () => void;
|
||
}) {
|
||
const change = Number(asset.change || "0");
|
||
const positive = change >= 0;
|
||
const categoryLabel = s.manualAssets.categories[asset.category] ?? asset.category;
|
||
const conditionLabel = s.manualAssets.conditions[asset.condition] ?? asset.condition;
|
||
const chip = assetChip(asset.category);
|
||
|
||
return (
|
||
<Card style={{ padding: 0 }}>
|
||
<div style={rowStyle}>
|
||
<Link
|
||
href={`/assets/manual/${encodeURIComponent(asset.name)}`}
|
||
style={{ display: "flex", alignItems: "center", gap: 12, color: "inherit", textDecoration: "none", flex: 1, minWidth: 0 }}
|
||
>
|
||
<IconChip icon={chip.icon} tint={chip.tint} fg={chip.fg} />
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||
<span style={titleStyle}>{asset.name}</span>
|
||
<span style={subtitleStyle}>
|
||
{categoryLabel} · {conditionLabel}
|
||
</span>
|
||
</div>
|
||
</Link>
|
||
<div style={{ textAlign: "right", flexShrink: 0 }}>
|
||
<div style={amountStyle}>{tugrikRaw(asset.value)}</div>
|
||
<div
|
||
style={{
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
marginTop: 2,
|
||
color: positive ? "var(--seed-color-fg-positive)" : "var(--seed-color-fg-critical)",
|
||
}}
|
||
>
|
||
{positive ? "+" : "−"}
|
||
{tugrikRaw(Math.abs(change))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style={{ display: "flex", gap: 8, padding: "0 16px 14px" }}>
|
||
<button
|
||
type="button"
|
||
onClick={onRevalue}
|
||
disabled={revaluing}
|
||
style={{
|
||
flex: 1,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
gap: 6,
|
||
border: "none",
|
||
borderRadius: 10,
|
||
background: "var(--seed-color-bg-neutral-subtle, #eef0f2)",
|
||
padding: "8px 10px",
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: "var(--seed-color-fg-neutral)",
|
||
cursor: revaluing ? "default" : "pointer",
|
||
}}
|
||
>
|
||
<Icon name="trending" size={14} />
|
||
{revaluing ? "…" : s.manualAssets.revalue}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onDelete}
|
||
style={{
|
||
border: "none",
|
||
borderRadius: 10,
|
||
background: "var(--seed-color-bg-neutral-subtle, #eef0f2)",
|
||
padding: "8px 14px",
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: "var(--seed-color-fg-critical)",
|
||
cursor: "pointer",
|
||
}}
|
||
>
|
||
{s.manualAssets.delete}
|
||
</button>
|
||
</div>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// --- Lending row ---------------------------------------------------------
|
||
|
||
function statusLabel(loan: Lending): string {
|
||
if (loan.overdue) return s.lending.statusOverdue;
|
||
switch (loan.status) {
|
||
case "repaid":
|
||
return s.lending.statusPaid;
|
||
case "partial":
|
||
return s.lending.statusPartial;
|
||
default:
|
||
return s.lending.statusUnpaid;
|
||
}
|
||
}
|
||
|
||
function LoanRow({ loan, onDelete }: { loan: Lending; onDelete: () => void }) {
|
||
return (
|
||
<div style={rowStyle}>
|
||
<Link
|
||
href={`/assets/lending/${loan.id}`}
|
||
style={{ display: "flex", alignItems: "center", gap: 12, color: "inherit", textDecoration: "none", flex: 1, minWidth: 0 }}
|
||
>
|
||
<IconChip icon="hand-coins" {...LENDING_TINT} />
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||
<span style={titleStyle}>{loan.person}</span>
|
||
<span style={{ fontSize: 12, color: loan.overdue ? "var(--seed-color-fg-critical)" : "var(--seed-color-fg-neutral-subtle)" }}>
|
||
{statusLabel(loan)}
|
||
</span>
|
||
</div>
|
||
</Link>
|
||
<div style={{ textAlign: "right", flexShrink: 0 }}>
|
||
<div style={amountStyle}>{tugrikRaw(loan.remaining)}</div>
|
||
<div style={subtitleStyle}>/ {tugrikRaw(loan.principal)}</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={onDelete}
|
||
aria-label={s.lending.delete}
|
||
style={{
|
||
background: "none",
|
||
border: "none",
|
||
color: "var(--seed-color-fg-neutral-subtle)",
|
||
cursor: "pointer",
|
||
fontSize: 18,
|
||
lineHeight: 1,
|
||
padding: 4,
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// --- Add manual asset sheet ------------------------------------------------
|
||
|
||
interface NewAssetValues {
|
||
name: string;
|
||
category: string;
|
||
value: string;
|
||
acquiredValue: string;
|
||
condition: string;
|
||
}
|
||
|
||
function AddManualAssetSheet({
|
||
onClose,
|
||
onSave,
|
||
saving,
|
||
}: {
|
||
onClose: () => void;
|
||
onSave: (values: NewAssetValues) => void | Promise<void>;
|
||
saving: boolean;
|
||
}) {
|
||
const [name, setName] = React.useState("");
|
||
const [category, setCategory] = React.useState("car");
|
||
const [condition, setCondition] = React.useState("used");
|
||
const [price, setPrice] = React.useState("");
|
||
|
||
const canSave = name.trim().length > 0 && Number(price) > 0 && !saving;
|
||
|
||
return (
|
||
<BottomSheetRoot open onOpenChange={(next) => { if (!next) onClose(); }}>
|
||
<BottomSheetBackdrop />
|
||
<BottomSheetPositioner>
|
||
<BottomSheetContent>
|
||
<BottomSheetHeader>
|
||
<BottomSheetTitle>{s.manualAssets.add}</BottomSheetTitle>
|
||
</BottomSheetHeader>
|
||
<BottomSheetBody style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
||
<TextFieldRoot value={name} onValueChange={setName} name="asset-name">
|
||
<TextFieldInput placeholder={s.manualAssets.fields.name} aria-label={s.manualAssets.fields.name} autoFocus />
|
||
</TextFieldRoot>
|
||
<label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 13 }}>
|
||
{s.manualAssets.fields.category}
|
||
<select value={category} onChange={(e) => setCategory(e.target.value)} style={{ padding: 8, borderRadius: 8 }}>
|
||
{Object.entries(s.manualAssets.categories).map(([key, label]) => (
|
||
<option key={key} value={key}>
|
||
{label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<TextFieldRoot value={price} onValueChange={setPrice} name="asset-price">
|
||
<TextFieldInput
|
||
type="number"
|
||
inputMode="numeric"
|
||
placeholder={s.manualAssets.fields.price}
|
||
aria-label={s.manualAssets.fields.price}
|
||
/>
|
||
</TextFieldRoot>
|
||
<label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 13 }}>
|
||
{s.manualAssets.fields.condition}
|
||
<select value={condition} onChange={(e) => setCondition(e.target.value)} style={{ padding: 8, borderRadius: 8 }}>
|
||
{Object.entries(s.manualAssets.conditions).map(([key, label]) => (
|
||
<option key={key} value={key}>
|
||
{label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
</BottomSheetBody>
|
||
<BottomSheetFooter style={{ display: "flex", gap: 12 }}>
|
||
<MercuryButton variant="secondary" onClick={onClose} style={{ flex: 1 }}>
|
||
{s.common.cancel}
|
||
</MercuryButton>
|
||
<MercuryButton
|
||
variant="primary"
|
||
style={{ flex: 1 }}
|
||
disabled={!canSave}
|
||
loading={saving}
|
||
onClick={() =>
|
||
onSave({
|
||
name: name.trim(),
|
||
category,
|
||
value: price,
|
||
acquiredValue: price,
|
||
condition,
|
||
})
|
||
}
|
||
>
|
||
{s.common.save}
|
||
</MercuryButton>
|
||
</BottomSheetFooter>
|
||
</BottomSheetContent>
|
||
</BottomSheetPositioner>
|
||
</BottomSheetRoot>
|
||
);
|
||
}
|
||
|
||
// --- Add loan sheet ---------------------------------------------------------
|
||
|
||
interface NewLoanValues {
|
||
person: string;
|
||
amount: string;
|
||
lentOn: string;
|
||
dueOn?: string;
|
||
note?: string;
|
||
}
|
||
|
||
function todayISO(): string {
|
||
return new Date().toISOString().slice(0, 10);
|
||
}
|
||
|
||
function AddLoanSheet({
|
||
onClose,
|
||
onSave,
|
||
saving,
|
||
}: {
|
||
onClose: () => void;
|
||
onSave: (values: NewLoanValues) => void | Promise<void>;
|
||
saving: boolean;
|
||
}) {
|
||
const [person, setPerson] = React.useState("");
|
||
const [amount, setAmount] = React.useState("");
|
||
const [lentOn, setLentOn] = React.useState(todayISO());
|
||
const [dueOn, setDueOn] = React.useState("");
|
||
const [note, setNote] = React.useState("");
|
||
|
||
const canSave = person.trim().length > 0 && Number(amount) > 0 && !saving;
|
||
|
||
return (
|
||
<BottomSheetRoot open onOpenChange={(next) => { if (!next) onClose(); }}>
|
||
<BottomSheetBackdrop />
|
||
<BottomSheetPositioner>
|
||
<BottomSheetContent>
|
||
<BottomSheetHeader>
|
||
<BottomSheetTitle>{s.lending.add}</BottomSheetTitle>
|
||
</BottomSheetHeader>
|
||
<BottomSheetBody style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
||
<TextFieldRoot value={person} onValueChange={setPerson} name="loan-person">
|
||
<TextFieldInput placeholder={s.lending.fields.person} aria-label={s.lending.fields.person} autoFocus />
|
||
</TextFieldRoot>
|
||
<TextFieldRoot value={amount} onValueChange={setAmount} name="loan-amount">
|
||
<TextFieldInput
|
||
type="number"
|
||
inputMode="numeric"
|
||
placeholder={s.lending.fields.amount}
|
||
aria-label={s.lending.fields.amount}
|
||
/>
|
||
</TextFieldRoot>
|
||
<label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 13 }}>
|
||
{s.lending.fields.lentOn}
|
||
<input type="date" value={lentOn} onChange={(e) => setLentOn(e.target.value)} style={{ padding: 8, borderRadius: 8 }} />
|
||
</label>
|
||
<label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 13 }}>
|
||
{s.lending.fields.dueOn}
|
||
<input type="date" value={dueOn} onChange={(e) => setDueOn(e.target.value)} style={{ padding: 8, borderRadius: 8 }} />
|
||
</label>
|
||
<TextFieldRoot value={note} onValueChange={setNote} name="loan-note">
|
||
<TextFieldInput placeholder={s.lending.fields.note} aria-label={s.lending.fields.note} />
|
||
</TextFieldRoot>
|
||
</BottomSheetBody>
|
||
<BottomSheetFooter style={{ display: "flex", gap: 12 }}>
|
||
<MercuryButton variant="secondary" onClick={onClose} style={{ flex: 1 }}>
|
||
{s.common.cancel}
|
||
</MercuryButton>
|
||
<MercuryButton
|
||
variant="primary"
|
||
style={{ flex: 1 }}
|
||
disabled={!canSave}
|
||
loading={saving}
|
||
onClick={() =>
|
||
onSave({
|
||
person: person.trim(),
|
||
amount,
|
||
lentOn,
|
||
dueOn: dueOn || undefined,
|
||
note: note.trim() || undefined,
|
||
})
|
||
}
|
||
>
|
||
{s.common.save}
|
||
</MercuryButton>
|
||
</BottomSheetFooter>
|
||
</BottomSheetContent>
|
||
</BottomSheetPositioner>
|
||
</BottomSheetRoot>
|
||
);
|
||
}
|