Display in a chip the number of unmatched inbox entries
This commit is contained in:
		| @@ -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 | ||||
|    */ | ||||
|   | ||||
| @@ -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)!; | ||||
| } | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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> | ||||
|       )} | ||||
|     /> | ||||
|   | ||||
| @@ -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> | ||||
|   ); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user