Add Expenses app

A UI based on [Budget Zen](https://github.com/BrunoBernardino/budgetzen-web) but slightly updated and adjusted for bewCloud. It also features a chart with available money and spent by budgets.

This is useful for envelope-based budgeting.
This commit is contained in:
Bruno Bernardino
2025-02-26 17:43:53 +00:00
parent 869e712432
commit 874ab006f9
29 changed files with 2677 additions and 12 deletions

View File

@@ -1,5 +1,7 @@
import { convertObjectToFormData, helpEmail } from '/lib/utils/misc.ts';
import { FormField, generateFieldHtml, getFormDataField } from '/lib/form-utils.tsx';
import { currencyMap, SupportedCurrencySymbol } from '/lib/types.ts';
import { isAppEnabled } from '/lib/config.ts';
interface SettingsProps {
formData: Record<string, any>;
@@ -11,6 +13,7 @@ interface SettingsProps {
title: string;
message: string;
};
currency?: SupportedCurrencySymbol;
}
export type Action =
@@ -18,7 +21,8 @@ export type Action =
| 'verify-change-email'
| 'change-password'
| 'change-dav-password'
| 'delete-account';
| 'delete-account'
| 'change-currency';
export const actionWords = new Map<Action, string>([
['change-email', 'change email'],
@@ -26,9 +30,10 @@ export const actionWords = new Map<Action, string>([
['change-password', 'change password'],
['change-dav-password', 'change WebDav password'],
['delete-account', 'delete account'],
['change-currency', 'change currency'],
]);
function formFields(action: Action, formData: FormData) {
function formFields(action: Action, formData: FormData, currency?: SupportedCurrencySymbol) {
const fields: FormField[] = [
{
name: 'action',
@@ -98,11 +103,23 @@ function formFields(action: Action, formData: FormData) {
description: 'You need to input your password in order to delete your account.',
required: true,
});
} else if (action === 'change-currency') {
fields.push({
name: 'currency',
label: 'Currency',
type: 'select',
options: Array.from(currencyMap.keys()).map((currencySymbol) => ({
value: currencySymbol,
label: `${currencySymbol} (${currencyMap.get(currencySymbol)})`,
})),
value: getFormDataField(formData, 'currency') || currency,
required: true,
});
}
return fields;
}
export default function Settings({ formData: formDataObject, error, notice }: SettingsProps) {
export default function Settings({ formData: formDataObject, error, notice, currency }: SettingsProps) {
const formData = convertObjectToFormData(formDataObject);
const action = getFormDataField(formData, 'action') as Action;
@@ -157,6 +174,24 @@ export default function Settings({ formData: formDataObject, error, notice }: Se
</section>
</form>
{isAppEnabled('expenses')
? (
<>
<h2 class='text-2xl mb-4 text-left px-4 max-w-screen-md mx-auto lg:min-w-96'>Change your currency</h2>
<p class='text-left mt-2 mb-6 px-4 max-w-screen-md mx-auto lg:min-w-96'>
This is only used in the expenses app, visually. It changes nothing about the stored data or values.
</p>
<form method='POST' class='mb-12'>
{formFields('change-currency', formData, currency).map((field) => generateFieldHtml(field, formData))}
<section class='flex justify-end mt-8 mb-4'>
<button class='button-secondary' type='submit'>Change currency</button>
</section>
</form>
</>
)
: null}
<h2 class='text-2xl mb-4 text-left px-4 max-w-screen-md mx-auto lg:min-w-96'>Delete your account</h2>
<p class='text-left mt-2 mb-6 px-4 max-w-screen-md mx-auto lg:min-w-96'>
Deleting your account is instant and deletes all your data. If you need help, please{' '}

View File

@@ -0,0 +1,23 @@
import { Budget, Expense, SupportedCurrencySymbol } from '/lib/types.ts';
import MainExpenses from '/components/expenses/MainExpenses.tsx';
interface ExpensesWrapperProps {
initialBudgets: Budget[];
initialExpenses: Expense[];
initialMonth: string;
currency: SupportedCurrencySymbol;
}
// This wrapper is necessary because islands need to be the first frontend component, but they don't support functions as props, so the more complex logic needs to live in the component itself
export default function ExpensesWrapper(
{ initialBudgets, initialExpenses, initialMonth, currency }: ExpensesWrapperProps,
) {
return (
<MainExpenses
initialBudgets={initialBudgets}
initialExpenses={initialExpenses}
initialMonth={initialMonth}
currency={currency}
/>
);
}