From a903055af4e0de46fc47fa89f26f064a9e01499a Mon Sep 17 00:00:00 2001 From: afane Date: Mon, 29 Jun 2026 16:11:20 +0100 Subject: [PATCH] [#602] Add unallocated funding visibility to financial goals & bumped major app version since its breaking change --- Dockerfile | 2 +- package-lock.json | 20 ++-- package.json | 2 +- public/locales/en/translation.json | 4 + public/locales/fr/translation.json | 4 + public/locales/pt/translation.json | 4 + src/features/goals/Goals.tsx | 146 +++++++++++++++++++++++++++-- src/services/goal/goalServices.ts | 13 ++- 8 files changed, 176 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 39cf5504..480513cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ FROM nginx:alpine WORKDIR /usr/share/nginx/html -ARG VERSION="9.6.0" +ARG VERSION="10.0.0" # Add metadata LABEL maintainer="José Valdiviesso " diff --git a/package-lock.json b/package-lock.json index 5b5e6514..64d7f744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "myfin-web", - "version": "9.6.0", + "version": "10.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "myfin-web", - "version": "9.6.0", + "version": "10.0.0", "dependencies": { "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", @@ -2867,16 +2867,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3028,9 +3028,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" diff --git a/package.json b/package.json index f1dc2ea1..4f48526d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "myfin-web", "private": true, - "version": "9.6.0", + "version": "10.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index e1b1b896..7508b28c 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -731,6 +731,10 @@ "fundingAmount": "Amount", "fundingPercentage": "Percentage", "underfundedTooltip": "This goal is underfunded because another goal with higher priority took the funds", + "unallocatedFunding": "Unallocated funding", + "unallocatedFundingDescription": "Money from goal funding accounts that is not assigned to any goal yet.", + "availableToAllocate": "available to allocate", + "accountFallback": "Account #{{id}}", "percentageMustBeBetween0And100": "Percentage must be between 0 and 100", "amountMustBePositive": "Amount must be positive", "amountCannotExceedGoal": "Amount cannot exceed the goal target", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index c1316f2b..5a194180 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -731,6 +731,10 @@ "fundingAmount": "Montant", "fundingPercentage": "Pourcentage", "underfundedTooltip": "Cet objectif est sous-financé car un objectif de priorité plus élevée a utilisé les fonds disponibles", + "unallocatedFunding": "Financement non attribué", + "unallocatedFundingDescription": "Argent des comptes de financement des objectifs qui n'est encore attribué à aucun objectif.", + "availableToAllocate": "disponible à attribuer", + "accountFallback": "Compte n°{{id}}", "percentageMustBeBetween0And100": "Le pourcentage doit être compris entre 0 et 100", "amountMustBePositive": "Le montant doit être positif", "amountCannotExceedGoal": "Le montant ne peut pas dépasser la cible de l'objectif", diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 88d10349..0c531763 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -731,6 +731,10 @@ "fundingAmount": "Montante", "fundingPercentage": "Percentagem", "underfundedTooltip": "Este objetivo está subfinanciado porque outro objetivo com maior prioridade utilizou os fundos", + "unallocatedFunding": "Financiamento por alocar", + "unallocatedFundingDescription": "Dinheiro das contas de financiamento dos objetivos que ainda não está atribuído a nenhum objetivo.", + "availableToAllocate": "disponível para alocar", + "accountFallback": "Conta #{{id}}", "percentageMustBeBetween0And100": "A percentagem deve estar entre 0 e 100", "amountMustBePositive": "O montante deve ser positivo", "amountCannotExceedGoal": "O montante não pode exceder o objetivo", diff --git a/src/features/goals/Goals.tsx b/src/features/goals/Goals.tsx index d759615e..a1964ccc 100644 --- a/src/features/goals/Goals.tsx +++ b/src/features/goals/Goals.tsx @@ -1,4 +1,5 @@ import { + AccountBalanceWallet, AddCircleOutline, CheckCircle, Delete, @@ -47,11 +48,14 @@ import { AlertSeverity, useSnackbar, } from '../../providers/SnackbarProvider.tsx'; +import { useGetAccounts } from '../../services/account/accountHooks.ts'; import { useDeleteGoal, useGetGoals } from '../../services/goal/goalHooks.ts'; -import { Goal } from '../../services/goal/goalServices.ts'; +import { Goal, GoalFundingSummary } from '../../services/goal/goalServices.ts'; import { useFormatNumberAsCurrency } from '../../utils/textHooks.ts'; import AddEditGoalDialog from './AddEditGoalDialog.tsx'; +type UnallocatedFunding = GoalFundingSummary['unallocated_funding']; + type UiState = { goals?: Goal[]; filteredGoals?: Goal[]; @@ -401,6 +405,100 @@ const GoalCard = ({ ); }; +const UnallocatedFundingCard = ({ + unallocatedFunding, + accountNameLookup, + formatCurrency, +}: { + unallocatedFunding: UnallocatedFunding; + accountNameLookup: Map; + formatCurrency: (value: number) => string; +}) => { + const theme = useTheme(); + const { t } = useTranslation(); + + if (unallocatedFunding.total_amount <= 0) return null; + + const accountBreakdown = unallocatedFunding.accounts + .map((account) => { + const accountName = + accountNameLookup.get(account.account_id) || + t('goals.accountFallback', { id: account.account_id }); + + return `${accountName}: ${formatCurrency(account.amount)}`; + }) + .join('\n'); + + return ( + + + + + + + {t('goals.unallocatedFunding')} + + + + {t('goals.unallocatedFundingDescription')} + + + + + + {formatCurrency(unallocatedFunding.total_amount)} + + + {t('goals.availableToAllocate')} + + + + {accountBreakdown} + } + > + + {unallocatedFunding.accounts.map((account) => { + const accountName = + accountNameLookup.get(account.account_id) || + t('goals.accountFallback', { id: account.account_id }); + + return ( + + ); + })} + + + + + ); +}; + const Goals = () => { const theme = useTheme(); const loader = useLoading(); @@ -409,38 +507,65 @@ const Goals = () => { const [showArchived, setShowArchived] = useState(false); const getGoalsRequest = useGetGoals(!showArchived); + const getAccountsRequest = useGetAccounts(); const deleteGoalRequest = useDeleteGoal(); const formatNumberAsCurrency = useFormatNumberAsCurrency(); const [state, dispatch] = useReducer(reduceState, null, createInitialState); // Loading useEffect(() => { - if (getGoalsRequest.isFetching || deleteGoalRequest.isPending) { + if ( + getGoalsRequest.isFetching || + getAccountsRequest.isFetching || + deleteGoalRequest.isPending + ) { loader.showLoading(); } else { loader.hideLoading(); } - }, [getGoalsRequest.isFetching, deleteGoalRequest.isPending]); + }, [ + getGoalsRequest.isFetching, + getAccountsRequest.isFetching, + deleteGoalRequest.isPending, + ]); // Error useEffect(() => { - if (getGoalsRequest.isError || deleteGoalRequest.isError) { + if ( + getGoalsRequest.isError || + getAccountsRequest.isError || + deleteGoalRequest.isError + ) { snackbar.showSnackbar( t('common.somethingWentWrongTryAgain'), AlertSeverity.ERROR, ); } - }, [getGoalsRequest.isError, deleteGoalRequest.isError]); + }, [ + getGoalsRequest.isError, + getAccountsRequest.isError, + deleteGoalRequest.isError, + ]); // Success useEffect(() => { if (!getGoalsRequest.data) return; dispatch({ type: StateActionType.DataLoaded, - payload: getGoalsRequest.data, + payload: getGoalsRequest.data.goals, }); }, [getGoalsRequest.data]); + const unallocatedFunding = getGoalsRequest.data?.unallocated_funding; + const accountNameLookup = useMemo(() => { + return new Map( + (getAccountsRequest.data || []).map((account) => [ + Number(account.account_id), + account.name, + ]), + ); + }, [getAccountsRequest.data]); + const formatDueDate = (timestamp: number | null) => { if (!timestamp) return '-'; const date = new Date(timestamp * 1000); @@ -713,6 +838,15 @@ const Goals = () => { {/* Visual Goal Cards Section - Only active goals, sorted by due date */} + {unallocatedFunding && unallocatedFunding.total_amount > 0 && ( + + + + )} {sortedGoalsForCards.map((goal) => ( ; + }; +}; + export type CreateGoalRequest = { name: string; description: string; @@ -34,7 +45,7 @@ export type UpdateGoalRequest = CreateGoalRequest & { }; const getGoals = (onlyActive: boolean = false) => { - return axios.get(`/goals`, { + return axios.get(`/goals`, { params: onlyActive ? { only_active: true } : undefined, }); };