Consume the provided resource-envelope API snapshot. Preserve financial snapshots during refresh, observe synchronization without importing, and fence late responses across account and connection changes.
105 lines
2.8 KiB
TypeScript
105 lines
2.8 KiB
TypeScript
import {
|
|
Alert,
|
|
Button,
|
|
Group,
|
|
Loader,
|
|
Stack,
|
|
Text,
|
|
Title,
|
|
} from "@mantine/core";
|
|
import { StateView } from "@wealthledger/ui";
|
|
import type { FC } from "react";
|
|
import { FormProvider } from "react-hook-form";
|
|
import { useTranslation } from "react-i18next";
|
|
import { formatSnapshotTime } from "@/shared/lib";
|
|
import classes from "../AccountIncomePage.module.css";
|
|
import type { AccountIncomePageViewModel } from "../AccountIncomePage.vm";
|
|
import { IncomeComparisons } from "./IncomeComparisons.view";
|
|
import { IncomePayments } from "./IncomePayments.view";
|
|
import { IncomePeriodForm } from "./IncomePeriodForm/IncomePeriodForm.view";
|
|
import { IncomeSummary } from "./IncomeSummary.view";
|
|
|
|
type LoadedProps = Extract<AccountIncomePageViewModel, { state: "loaded" }>;
|
|
|
|
const AccountIncomePageLoaded: FC<LoadedProps> = (props) => {
|
|
const {
|
|
analytics,
|
|
display,
|
|
form,
|
|
refreshing,
|
|
refreshError,
|
|
retry,
|
|
onSubmit,
|
|
onReset,
|
|
} = props;
|
|
const { t } = useTranslation();
|
|
return (
|
|
<Stack gap="lg" className={classes.page}>
|
|
{props.lastSuccessfulAt ? (
|
|
<Text size="xs" c="dimmed">
|
|
{t("investment.sourceTime", {
|
|
date: formatSnapshotTime(props.lastSuccessfulAt),
|
|
})}
|
|
</Text>
|
|
) : null}
|
|
<Title order={1}>{t("income.title")}</Title>
|
|
<Button onClick={retry} disabled={refreshing} w="fit-content">
|
|
{t("investment.refresh")}
|
|
</Button>
|
|
<FormProvider {...form}>
|
|
<IncomePeriodForm
|
|
key={props.formRevision}
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
onReset={onReset}
|
|
/>
|
|
</FormProvider>
|
|
{refreshing ? (
|
|
<Group gap="xs" role="status" aria-label={t("income.refreshing")}>
|
|
<Loader size="sm" />
|
|
<Text>{t("income.refreshing")}</Text>
|
|
</Group>
|
|
) : null}
|
|
{refreshError ? (
|
|
<Alert color="orange" role="status" aria-live="polite">
|
|
<Stack gap="sm">
|
|
<Text>
|
|
{t(
|
|
props.sourceError
|
|
? "investment.syncFailed"
|
|
: "income.refreshError",
|
|
)}
|
|
</Text>
|
|
<Button onClick={retry} variant="default">
|
|
{t("income.retry")}
|
|
</Button>
|
|
</Stack>
|
|
</Alert>
|
|
) : null}
|
|
{props.partial ? (
|
|
<Alert role="status">{t("investment.partial")}</Alert>
|
|
) : analytics.totalsByCurrency.length ? (
|
|
<IncomeSummary totals={analytics.totalsByCurrency} />
|
|
) : (
|
|
<StateView
|
|
title={t("income.emptyTitle")}
|
|
text={t("income.emptyText")}
|
|
/>
|
|
)}
|
|
<Alert color="blue">{t("income.taxBoundary")}</Alert>
|
|
<IncomeComparisons
|
|
title={t("income.previousPeriod")}
|
|
comparison={display.previousComparison}
|
|
notice={t(`income.${display.previousNotice}`)}
|
|
/>
|
|
<IncomeComparisons
|
|
title={t("income.yearAgo")}
|
|
comparison={display.comparison}
|
|
notice={t(`income.${display.yearNotice}`)}
|
|
/>
|
|
<IncomePayments entries={display.entries} />
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export { AccountIncomePageLoaded };
|