diff --git a/moneymgr_web/src/api/InboxApi.ts b/moneymgr_web/src/api/InboxApi.ts index 55a3748..e5f491c 100644 --- a/moneymgr_web/src/api/InboxApi.ts +++ b/moneymgr_web/src/api/InboxApi.ts @@ -21,6 +21,17 @@ export interface InboxEntryUpdate { } export class InboxApi { + /** + * Get the number of unmatched entries + */ + static async CountUnmatched(): Promise { + return ( + await APIClient.exec({ + uri: `/inbox/count?include_attached=false`, + method: "GET", + }) + ).data.count; + } /** * Create a new inbox entry */ diff --git a/moneymgr_web/src/hooks/UnmatchedInboxEntriesCountProvider.tsx b/moneymgr_web/src/hooks/UnmatchedInboxEntriesCountProvider.tsx new file mode 100644 index 0000000..255094f --- /dev/null +++ b/moneymgr_web/src/hooks/UnmatchedInboxEntriesCountProvider.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { InboxApi } from "../api/InboxApi"; +import { AsyncWidget } from "../widgets/AsyncWidget"; + +interface UnmatchedInboxEntriesCountContext { + count: number; + reload: () => Promise; +} + +const UnmatchedInboxEntriesCountContextK = + React.createContext(null); + +export function UnmatchedInboxEntriesCountProvider( + p: React.PropsWithChildren +): React.ReactElement { + const [count, setCount] = React.useState(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((res) => { + loadPromise.current = () => { + res(); + }; + }); + }; + + return ( + { + if (loadPromise.current != null) { + loadPromise.current(); + loadPromise.current = null; + } + + return ( + + {p.children} + + ); + }} + /> + ); +} + +export function useUnmatchedInboxEntriesCount(): UnmatchedInboxEntriesCountContext { + return React.use(UnmatchedInboxEntriesCountContextK)!; +} diff --git a/moneymgr_web/src/routes/InboxRoute.tsx b/moneymgr_web/src/routes/InboxRoute.tsx index 7d30d2f..96dce97 100644 --- a/moneymgr_web/src/routes/InboxRoute.tsx +++ b/moneymgr_web/src/routes/InboxRoute.tsx @@ -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(); 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); diff --git a/moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx b/moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx index 8ffd231..27f687f 100644 --- a/moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx +++ b/moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx @@ -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,48 +49,50 @@ export function BaseAuthenticatedPage(): React.ReactElement { reloadAuthInfo: load, }} > - - - - theme.palette.mode === "light" - ? theme.palette.grey[100] - : theme.palette.grey[900], - color: (theme) => - theme.palette.mode === "light" - ? theme.palette.grey[900] - : theme.palette.grey[100], - }} - > - - + + + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + color: (theme) => + theme.palette.mode === "light" + ? theme.palette.grey[900] + : theme.palette.grey[100], }} > - -
+ + - -
+ +
+ +
+
- -
-
+ + + )} /> diff --git a/moneymgr_web/src/widgets/MoneyNavList.tsx b/moneymgr_web/src/widgets/MoneyNavList.tsx index d60777f..c21a241 100644 --- a/moneymgr_web/src/widgets/MoneyNavList.tsx +++ b/moneymgr_web/src/widgets/MoneyNavList.tsx @@ -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 ( } /> - {/* TODO : show number of unmatched */} } + secondary={ + unmatched.count > 0 ? ( + + ) : ( + <> + ) + } /> {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 ( - - {p.icon} - - + + + {p.icon} + + + ); }