From cffd1a5ea0bf6c0731cfb2200469a0afac4e0e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=BCndig?= Date: Sat, 4 May 2024 09:10:26 +0200 Subject: [PATCH] feature(website): dynamic expenses stats on website (#826) --- shared/locales/de/website-finances.json | 14 ++-- shared/locales/en/website-finances.json | 14 ++-- shared/src/types/expense.ts | 4 +- .../utils/stats/ExpensesStatsCalculator.ts | 12 +-- .../transparency/finances/[currency]/page.tsx | 33 ++------ .../finances/[currency]/section-2.tsx | 4 +- .../finances/[currency]/section-3-cards.tsx | 5 +- .../finances/[currency]/section-3.tsx | 3 +- .../finances/[currency]/section-4.tsx | 77 +++++++++++++++---- 9 files changed, 97 insertions(+), 69 deletions(-) diff --git a/shared/locales/de/website-finances.json b/shared/locales/de/website-finances.json index f6880a607..2ec1c1b4b 100644 --- a/shared/locales/de/website-finances.json +++ b/shared/locales/de/website-finances.json @@ -38,12 +38,16 @@ "payments-last-month": "Letzter Monat: {{ value, currency }} an {{ recipientsCount }} Empfänger:innen", "payments-future": "Künftige Auszahlungen: {{ value, currency }}", "project-costs": "{{ value, currency }} Projektführungskosten", - "transaction-costs": "Zustellgebühren: {{ value, currency }}", - "transaction-costs-tooltip": "Kosten, die anfallen beim Geldtransfer von dir zu uns.", - "delivery-costs": "Auslieferungskosten: {{ value, currency }}", - "delivery-costs-tooltip": "Kosten, die anfallen beim Geldtransfer von uns zu den Empfänger:innen.", + "donation-fees": "Zustellgebühren: {{ value, currency }}", + "donation-fees-tooltip": "Kosten, die anfallen beim Geldtransfer von dir zu uns.", + "delivery-fees": "Auslieferungskosten: {{ value, currency }}", + "delivery-fees-tooltip": "Kosten, die anfallen beim Geldtransfer von uns zu den Empfänger:innen.", + "exchange-rate-loss": "Währungsverluste: {{ value, currency }}", + "exchange-rate-loss-tooltip": "Die Währungsverluste entstehen hauptsächlich durch die Inflation in Sierra Leone.", + "account-fees": "Kontoführungsgebühren: {{ value, currency }}", + "account-fees-tooltip": "Kosten, die anfallen für die Kontoführung in Sierra Leone und der Schweiz.", "administrative-costs": "Administrative Kosten: {{ value, currency }}", - "administrative-costs-tooltip": "Kosten für IT Lizenzen, Hosting der Webseite, etc.", + "administrative-costs-tooltip": "Kosten für IT Lizenzen, Hosting der Webseite, und andere Dienstleistungen.", "fundraising-costs": "Fundraising & Marketing: {{ value, currency }}", "fundraising-costs-tooltip": "Kosten, die für Werbung und Fundraising anfallen.", "staff-costs": "Lokales Personal: {{ value, currency }}", diff --git a/shared/locales/en/website-finances.json b/shared/locales/en/website-finances.json index 9682e86aa..7f9da9de2 100644 --- a/shared/locales/en/website-finances.json +++ b/shared/locales/en/website-finances.json @@ -38,12 +38,16 @@ "payments-last-month": "Last month: {{ value, currency }} to {{ recipientsCount }} recipients", "payments-future": "To be paid out: {{ value, currency }}", "project-costs": "{{ value, currency }} Project Management Costs", - "transaction-costs": "Transaction Costs: {{ value, currency }}", - "transaction-costs-tooltip": "Costs incurred during the transfer of money from you to us.", - "delivery-costs": "Delivery Costs: {{ value, currency }}", - "delivery-costs-tooltip": "Costs incurred during the transfer of money from us to the recipients.", + "donation-fees": "Transaction Costs: {{ value, currency }}", + "donation-fees-tooltip": "Costs incurred during the transfer of money from you to us.", + "delivery-fees": "Delivery Costs: {{ value, currency }}", + "delivery-fees-tooltip": "Costs incurred during the transfer of money from us to the recipients.", + "exchange-rate-loss": "Exchange Rate Loss: {{ value, currency }}", + "exchange-rate-loss-tooltip": "Exchange rate losses occur mainly due to the inflation in Sierra Leone.", + "account-fees": "Account Fees: {{ value, currency }}", + "account-fees-tooltip": "Fees for our bank accounts in Switzerland and Sierra Leone.", "administrative-costs": "Administrative Costs: {{ value, currency }}", - "administrative-costs-tooltip": "Costs for IT licenses, website hosting, etc.", + "administrative-costs-tooltip": "Fees for IT licenses, website hosting, and other services.", "fundraising-costs": "Fundraising & Marketing: {{ value, currency }}", "fundraising-costs-tooltip": "Costs incurred for advertising and fundraising.", "staff-costs": "Local Staff: {{ value, currency }}", diff --git a/shared/src/types/expense.ts b/shared/src/types/expense.ts index 1fd82db95..75c3cd369 100644 --- a/shared/src/types/expense.ts +++ b/shared/src/types/expense.ts @@ -2,11 +2,11 @@ export const EXPENSES_FIRESTORE_PATH = 'expenses'; export enum ExpenseType { AccountFees = 'account_fees', - Administrative = 'Administrative', + Administrative = 'administrative', DeliveryFees = 'delivery_fees', DonationFees = 'donation_fees', ExchangeRateLoss = 'exchange_rate_loss', - FundraisingAdvertising = 'FundraisingAdvertising', + FundraisingAdvertising = 'fundraising_advertising', Staff = 'staff', } diff --git a/shared/src/utils/stats/ExpensesStatsCalculator.ts b/shared/src/utils/stats/ExpensesStatsCalculator.ts index 53cdd546c..3c9b6273b 100644 --- a/shared/src/utils/stats/ExpensesStatsCalculator.ts +++ b/shared/src/utils/stats/ExpensesStatsCalculator.ts @@ -10,7 +10,7 @@ type ExpenseStatsEntry = Expense & { export type ExpenseStats = { totalExpensesByYear: { [year: string]: number }; - totalExpensesByType: { [type: string]: number }; + totalExpensesByType: Record; totalExpensesByYearAndType: { [year: string]: { [type in ExpenseType]: number } }; }; @@ -33,14 +33,14 @@ export class ExpensesStatsCalculator { ); } - public totalExpensesBy(group: 'type' | 'year'): { [type in string]?: number } { + public totalExpensesBy(group: 'type' | 'year'): Record { return this.expenses .groupBy(group) - .map((expenses, group) => ({ [group]: _.sumBy(expenses, 'amount') })) + .map((expenses, group) => ({ [group as ExpenseType]: _.sumBy(expenses, 'amount') })) .reduce((a, b) => ({ ...a, ...b }), {}); } - public totalExpensesByYearAndType(): { [year: string]: { [type in ExpenseType]: number } } { + public totalExpensesByYearAndType(): Record> { return this.expenses .groupBy('year') .map((expenses, year) => ({ @@ -54,8 +54,8 @@ export class ExpensesStatsCalculator { public allStats(): ExpenseStats { return { - totalExpensesByYear: this.totalExpensesBy('year') as { [year: string]: number }, - totalExpensesByType: this.totalExpensesBy('type') as { [type in ExpenseType]: number }, + totalExpensesByYear: this.totalExpensesBy('year') as Record, + totalExpensesByType: this.totalExpensesBy('type') as Record, totalExpensesByYearAndType: this.totalExpensesByYearAndType(), }; } diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/page.tsx b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/page.tsx index de554432a..fb8de66be 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/page.tsx @@ -29,14 +29,7 @@ export type SectionProps = { params: DefaultParams & { currency: string }; contributionStats: ContributionStats; paymentStats: PaymentStats; - expenseStats: ExpenseStats; - costs: { - transaction: number; - delivery: number; - administrative: number; - fundraising: number; - staff: number; - }; + expensesStats: ExpenseStats; }; export default async function Page({ params }: TransparencyPageProps) { @@ -51,17 +44,7 @@ export default async function Page({ params }: TransparencyPageProps) { return { contributionStats, expensesStats, paymentStats }; }; const currency = params.currency.toUpperCase() as WebsiteCurrency; - const { contributionStats, expensesStats: expenseStats, paymentStats } = await getStats(currency); - console.info(JSON.stringify(expenseStats, null, 2)); - - // TODO: Calculate these costs dynamically - const costs = { - transaction: 8800, - delivery: 5700, // "Total operative expenses" - administrative: 5600, // "Other project costs" - fundraising: 4500, - staff: 9600, - }; + const { contributionStats, expensesStats, paymentStats } = await getStats(currency); return (
@@ -70,29 +53,25 @@ export default async function Page({ params }: TransparencyPageProps) { params={params} contributionStats={contributionStats} paymentStats={paymentStats} - costs={costs} - expenseStats={expenseStats} + expensesStats={expensesStats} />
); diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-2.tsx b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-2.tsx index a0fb42ab8..597acb8e4 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-2.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-2.tsx @@ -6,9 +6,9 @@ import _ from 'lodash'; import { InfoCard } from './info-card'; import { SectionProps } from './page'; -export async function Section2({ params, contributionStats, paymentStats, costs }: SectionProps) { +export async function Section2({ params, contributionStats, expensesStats, paymentStats }: SectionProps) { const translator = await Translator.getInstance({ language: params.lang, namespaces: ['website-finances'] }); - const expensesProject = _.sum(Object.values(costs)); + const expensesProject = _.sum(Object.values(expensesStats.totalExpensesByType)); return (
diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-3-cards.tsx b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-3-cards.tsx index 49218ecec..b215c0587 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-3-cards.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-3-cards.tsx @@ -1,6 +1,5 @@ 'use client'; -import { ExpenseStats } from '@socialincome/shared/src/utils/stats/ExpensesStatsCalculator'; import { Button, Card, CardContent, Typography } from '@socialincome/ui'; import { Children, PropsWithChildren, useState } from 'react'; @@ -17,12 +16,10 @@ type CountryCardProps = { country: string; total: string; }; - expenseStats: ExpenseStats; // TODO: remove again }; /* eslint-disable @next/next/no-img-element */ -export function CountryCard({ country, translations, expenseStats }: CountryCardProps) { - console.log(expenseStats); +export function CountryCard({ country, translations }: CountryCardProps) { return (
  • diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-3.tsx b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-3.tsx index b68da352d..e83b7d025 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-3.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-3.tsx @@ -4,7 +4,7 @@ import { Typography } from '@socialincome/ui'; import { SectionProps } from './page'; import { CountryCard, CountryCardList } from './section-3-cards'; -export async function Section3({ params, contributionStats, expenseStats }: SectionProps) { +export async function Section3({ params, contributionStats }: SectionProps) { const translator = await Translator.getInstance({ language: params.lang, namespaces: ['countries', 'website-finances'], @@ -40,7 +40,6 @@ export async function Section3({ params, contributionStats, expenseStats }: Sect }, }), }} - expenseStats={expenseStats} /> ))} diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-4.tsx b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-4.tsx index dbb8caf85..637edacc0 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-4.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/section-4.tsx @@ -10,9 +10,9 @@ import { InfoCard } from './info-card'; import { SectionProps } from './page'; import { roundAmount } from './section-1'; -export async function Section4({ params, paymentStats, contributionStats, costs }: SectionProps) { +export async function Section4({ params, expensesStats, paymentStats, contributionStats }: SectionProps) { const translator = await Translator.getInstance({ language: params.lang, namespaces: ['website-finances'] }); - const expensesTotal = _.sum(Object.values(costs)) + paymentStats.totalPaymentsAmount; + const expensesTotal = _.sum(Object.values(expensesStats.totalExpensesByType)) + paymentStats.totalPaymentsAmount; const reservesTotal = contributionStats.totalContributionsAmount - expensesTotal; const exchangeRateSLE = await getLatestExchangeRate(firestoreAdmin, 'SLE'); @@ -74,14 +74,17 @@ export async function Section4({ params, paymentStats, contributionStats, costs
    {translator.t('section-4.project-costs', { - context: { value: roundAmount(_.sum(Object.values(costs))), currency: params.currency }, + context: { + value: roundAmount(_.sum(Object.values(expensesStats.totalExpensesByType))), + currency: params.currency, + }, })}
    - {translator.t('section-4.transaction-costs', { + {translator.t('section-4.donation-fees', { context: { - value: roundAmount(costs.transaction), + value: roundAmount(expensesStats.totalExpensesByType.donation_fees), currency: params.currency, }, })} @@ -90,52 +93,62 @@ export async function Section4({ params, paymentStats, contributionStats, costs - {translator.t('section-4.transaction-costs-tooltip')} + {translator.t('section-4.donation-fees-tooltip')} - {translator.t('section-4.delivery-costs', { - context: { value: roundAmount(costs.delivery), currency: params.currency }, + {translator.t('section-4.delivery-fees', { + context: { + value: roundAmount(expensesStats.totalExpensesByType.delivery_fees), + currency: params.currency, + }, })} - {translator.t('section-4.delivery-costs-tooltip')} + {translator.t('section-4.delivery-fees-tooltip')} - {translator.t('section-4.administrative-costs', { - context: { value: roundAmount(costs.administrative), currency: params.currency }, + {translator.t('section-4.exchange-rate-loss', { + context: { + value: roundAmount(expensesStats.totalExpensesByType.exchange_rate_loss), + currency: params.currency, + }, })} - {translator.t('section-4.administrative-costs-tooltip')} + {translator.t('section-4.exchange-rate-loss-tooltip')} - {translator.t('section-4.fundraising-costs', { - context: { value: roundAmount(costs.fundraising), currency: params.currency }, + {translator.t('section-4.account-fees', { + context: { + value: roundAmount(expensesStats.totalExpensesByType.account_fees), + currency: params.currency, + }, })} - {translator.t('section-4.fundraising-costs-tooltip')} + {translator.t('section-4.account-fees-tooltip')} + {translator.t('section-4.staff-costs', { - context: { value: roundAmount(costs.staff), currency: params.currency }, + context: { value: roundAmount(expensesStats.totalExpensesByType.staff), currency: params.currency }, })} @@ -146,6 +159,38 @@ export async function Section4({ params, paymentStats, contributionStats, costs + + {translator.t('section-4.fundraising-costs', { + context: { + value: roundAmount(expensesStats.totalExpensesByType.fundraising_advertising), + currency: params.currency, + }, + })} + + + + + + {translator.t('section-4.fundraising-costs-tooltip')} + + + + + {translator.t('section-4.administrative-costs', { + context: { + value: roundAmount(expensesStats.totalExpensesByType.administrative), + currency: params.currency, + }, + })} + + + + + + {translator.t('section-4.administrative-costs-tooltip')} + + +
  • } />