Display in a chip the number of unmatched inbox entries

This commit is contained in:
Pierre HUBERT 2025-05-12 18:46:40 +02:00
parent 0b586039c3
commit 1e8064946a
5 changed files with 138 additions and 41 deletions

View File

@ -21,6 +21,17 @@ export interface InboxEntryUpdate {
}
export class InboxApi {
/**
* Get the number of unmatched entries
*/
static async CountUnmatched(): Promise<number> {
return (
await APIClient.exec({
uri: `/inbox/count?include_attached=false`,
method: "GET",
})
).data.count;
}
/**
* Create a new inbox entry
*/

View File

@ -0,0 +1,67 @@
import React from "react";
import { InboxApi } from "../api/InboxApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
interface UnmatchedInboxEntriesCountContext {
count: number;
reload: () => Promise<void>;
}
const UnmatchedInboxEntriesCountContextK =
React.createContext<UnmatchedInboxEntriesCountContext | null>(null);
export function UnmatchedInboxEntriesCountProvider(
p: React.PropsWithChildren
): React.ReactElement {
const [count, setCount] = React.useState<number | null>(null);
const loadKey = React.useRef(1);
const loadPromise = React.useRef<(() => void) | null>(null);
const load = async () => {
setCount(await InboxApi.CountUnmatched());
};
const onReload = async () => {
loadKey.current += 1;
load();
return new Promise<void>((res) => {
loadPromise.current = () => {
res();
};
});
};
return (
<AsyncWidget
ready={true}
loadKey={loadKey.current}
load={load}
errMsg="Failed to get the number of unread inbox entries!"
build={() => {
if (loadPromise.current != null) {
loadPromise.current();
loadPromise.current = null;
}
return (
<UnmatchedInboxEntriesCountContextK
value={{
count: count ?? 0,
reload: onReload,
}}
>
{p.children}
</UnmatchedInboxEntriesCountContextK>
);
}}
/>
);
}
export function useUnmatchedInboxEntriesCount(): UnmatchedInboxEntriesCountContext {
return React.use(UnmatchedInboxEntriesCountContextK)!;
}

View File

@ -7,11 +7,14 @@ import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProv
import { AsyncWidget } from "../widgets/AsyncWidget";
import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
import { NewMovementWidget } from "../widgets/NewMovementWidget";
import { useUnmatchedInboxEntriesCount } from "../hooks/UnmatchedInboxEntriesCountProvider";
export function InboxRoute(): React.ReactElement {
const loadingMessage = useLoadingMessage();
const alert = useAlert();
const unmatched = useUnmatchedInboxEntriesCount();
const [entries, setEntries] = React.useState<InboxEntry[] | undefined>();
const [includeAttached, setIncludeAttached] = React.useState(false);
@ -24,7 +27,7 @@ export function InboxRoute(): React.ReactElement {
const reload = async (skipEntries: boolean) => {
try {
loadingMessage.show("Refreshing the list of inbox entries...");
// TODO : trigger reload number of inbox entries
unmatched.reload();
if (!skipEntries) await load();
} catch (e) {
console.error("Failed to load list of inbox entries!", e);

View File

@ -1,13 +1,14 @@
import { Box, Button } from "@mui/material";
import * as React from "react";
import { Outlet, useNavigate } from "react-router-dom";
import { useAuth } from "../App";
import { AuthApi, AuthInfo } from "../api/AuthApi";
import { useAuth } from "../App";
import { AccountsListProvider } from "../hooks/AccountsListProvider";
import { ChooseAccountDialogProvider } from "../hooks/context_providers/ChooseAccountDialogProvider";
import { UnmatchedInboxEntriesCountProvider } from "../hooks/UnmatchedInboxEntriesCountProvider";
import { AsyncWidget } from "./AsyncWidget";
import { MoneyNavList } from "./MoneyNavList";
import { MoneyWebAppBar } from "./MoneyWebAppBar";
import { ChooseAccountDialogProvider } from "../hooks/context_providers/ChooseAccountDialogProvider";
interface AuthInfoContext {
info: AuthInfo;
@ -48,6 +49,7 @@ export function BaseAuthenticatedPage(): React.ReactElement {
reloadAuthInfo: load,
}}
>
<UnmatchedInboxEntriesCountProvider>
<AccountsListProvider>
<ChooseAccountDialogProvider>
<Box
@ -90,6 +92,7 @@ export function BaseAuthenticatedPage(): React.ReactElement {
</Box>
</ChooseAccountDialogProvider>
</AccountsListProvider>
</UnmatchedInboxEntriesCountProvider>
</AuthInfoContextK>
)}
/>

View File

@ -1,8 +1,10 @@
import { mdiCashMultiple, mdiHome, mdiInbox } from "@mdi/js";
import Icon from "@mdi/react";
import {
Chip,
Divider,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
@ -15,10 +17,12 @@ import { usePublicMode } from "../hooks/context_providers/PublicModeProvider";
import { AccountWidget } from "./AccountWidget";
import { AmountWidget } from "./AmountWidget";
import { RouterLink } from "./RouterLink";
import { useUnmatchedInboxEntriesCount } from "../hooks/UnmatchedInboxEntriesCountProvider";
export function MoneyNavList(): React.ReactElement {
const publicMode = usePublicMode();
const accounts = useAccounts();
const unmatched = useUnmatchedInboxEntriesCount();
return (
<List
dense
@ -35,11 +39,17 @@ export function MoneyNavList(): React.ReactElement {
uri="/accounts"
icon={<Icon path={mdiCashMultiple} size={1} />}
/>
{/* TODO : show number of unmatched */}
<NavLink
label="Inbox"
uri="/inbox"
icon={<Icon path={mdiInbox} size={1} />}
secondary={
unmatched.count > 0 ? (
<Chip label={unmatched.count} size="small" />
) : (
<></>
)
}
/>
<Divider />
{accounts.list.isEmpty && (
@ -76,14 +86,17 @@ function NavLink(p: {
uri: string;
label: string | React.ReactElement;
secondaryLabel?: string | React.ReactElement;
secondary?: React.ReactElement;
}): React.ReactElement {
const location = useLocation();
return (
<RouterLink to={p.uri}>
<ListItem secondaryAction={p.secondary} disablePadding>
<ListItemButton selected={p.uri === location.pathname}>
<ListItemIcon>{p.icon}</ListItemIcon>
<ListItemText primary={p.label} secondary={p.secondaryLabel} />
</ListItemButton>
</ListItem>
</RouterLink>
);
}