update money tracker
All checks were successful
Test and deploy staging / staging (push) Successful in 30s

This commit is contained in:
2026-09-23 18:06:20 +00:00
parent 2466db74cd
commit 499dd94cbb
9 changed files with 641 additions and 77 deletions

View File

@@ -7,7 +7,8 @@ import {
moneyUtilitySettings,
moneyUtilityReadings,
moneyWishlist,
moneyInsurances
moneyInsurances,
moneyProjectionSettings
} from '$lib/server/db/schema';
import { requireAdmin } from '$lib/server/authorization';
import {
@@ -38,7 +39,8 @@ export const load: PageServerLoad = async (event) => {
utilitySettings,
utilityReadings,
wishlist,
insurances
insurances,
projectionSettings
] = await Promise.all([
db
.select()
@@ -75,7 +77,8 @@ export const load: PageServerLoad = async (event) => {
.select()
.from(moneyInsurances)
.where(eq(moneyInsurances.userId, userId))
.orderBy(asc(moneyInsurances.name))
.orderBy(asc(moneyInsurances.name)),
db.select().from(moneyProjectionSettings).where(eq(moneyProjectionSettings.userId, userId))
]);
return {
accounts,
@@ -85,7 +88,9 @@ export const load: PageServerLoad = async (event) => {
utilitySettings,
utilityReadings,
wishlist,
insurances
insurances,
projectionSettings: projectionSettings[0] ?? null,
today: new Date().toISOString().slice(0, 10)
};
};
@@ -111,18 +116,20 @@ export const actions: Actions = {
const form = await event.request.formData();
const name = textField(form, 'name', 80);
const openingBalanceCents = cents(form.get('openingBalance'));
if (!name || openingBalanceCents == null)
const kind = String(form.get('kind') ?? 'spendable');
if (!name || openingBalanceCents == null || !['spendable', 'investment'].includes(kind))
return invalid('Enter an account name and a valid opening balance.');
const id = recordId(form);
try {
if (id) {
const [row] = await db
.update(moneyAccounts)
.set({ name, openingBalanceCents })
.set({ name, openingBalanceCents, kind })
.where(and(eq(moneyAccounts.id, id), eq(moneyAccounts.userId, user.id)))
.returning({ id: moneyAccounts.id });
if (!row) error(404, 'Account not found');
} else await db.insert(moneyAccounts).values({ userId: user.id, name, openingBalanceCents });
} else
await db.insert(moneyAccounts).values({ userId: user.id, name, openingBalanceCents, kind });
} catch (cause) {
if (typeof cause === 'object' && cause && 'code' in cause && cause.code === '23505')
return invalid('An account with that name already exists.');
@@ -182,10 +189,21 @@ export const actions: Actions = {
const name = textField(form, 'name', 200);
const amountCents = cents(form.get('amount'));
const period = cadence(form.get('cadence'));
if (!name || amountCents == null || !period)
const startsOnRaw = String(form.get('startsOn') ?? '').trim();
const endsOnRaw = String(form.get('endsOn') ?? '').trim();
const startsOn = startsOnRaw ? validDate(startsOnRaw) : null;
const endsOn = endsOnRaw ? validDate(endsOnRaw) : null;
if (
!name ||
amountCents == null ||
!period ||
(startsOnRaw && !startsOn) ||
(endsOnRaw && !endsOn) ||
(startsOn && endsOn && endsOn < startsOn)
)
return invalid('Enter a name, valid signed amount and frequency.');
const accountId = await ownedAccount(user.id, form.get('accountId'), true);
const values = { name, amountCents, cadence: period, accountId };
const values = { name, amountCents, cadence: period, accountId, startsOn, endsOn };
const id = recordId(form);
if (id) {
const [row] = await db
@@ -236,6 +254,59 @@ export const actions: Actions = {
.where(and(eq(moneyForecasts.id, id), eq(moneyForecasts.userId, user.id)));
return { saved: true };
},
saveProjectionSettings: async (event) => {
const user = requireAdmin(event);
const form = await event.request.formData();
const investmentAccountId = await ownedAccount(user.id, form.get('investmentAccountId'), true);
if (investmentAccountId !== null) {
const [account] = await db
.select({ kind: moneyAccounts.kind })
.from(moneyAccounts)
.where(and(eq(moneyAccounts.id, investmentAccountId), eq(moneyAccounts.userId, user.id)));
if (account?.kind !== 'investment') return invalid('Choose an investment account.');
}
const reserveCents = cents(form.get('reserve'));
const fixedMonthlyCents = cents(form.get('fixedMonthly'));
const annualReturnPercent = String(form.get('annualReturnPercent') ?? '')
.trim()
.replace(',', '.');
const contributionMode = String(form.get('contributionMode') ?? 'excess');
const endDateRaw = String(form.get('endDate') ?? '').trim();
const endDate = endDateRaw ? validDate(endDateRaw) : null;
const maxEndDate = new Date();
maxEndDate.setUTCFullYear(maxEndDate.getUTCFullYear() + 20);
if (
reserveCents == null ||
reserveCents < 0 ||
fixedMonthlyCents == null ||
fixedMonthlyCents < 0 ||
!/^(?:\d{1,2}|100)(?:\.\d{1,2})?$/.test(annualReturnPercent) ||
Number(annualReturnPercent) > 100 ||
!['excess', 'fixed'].includes(contributionMode) ||
(endDateRaw && !endDate) ||
(endDate &&
(endDate < new Date().toISOString().slice(0, 10) ||
endDate > maxEndDate.toISOString().slice(0, 10)))
)
return invalid('Enter valid projection settings.');
const values = {
investmentAccountId,
reserveCents,
fixedMonthlyCents,
annualReturnPercent,
contributionMode,
includeRecurring: form.get('includeRecurring') === 'on' ? 1 : 0,
endDate
};
await db
.insert(moneyProjectionSettings)
.values({ userId: user.id, ...values })
.onConflictDoUpdate({
target: moneyProjectionSettings.userId,
set: values
});
return { saved: true };
},
saveUtilitySettings: async (event) => {
const user = requireAdmin(event);
const form = await event.request.formData();
@@ -300,14 +371,18 @@ export const actions: Actions = {
const name = textField(form, 'name', 200);
const estimatedCents = cents(form.get('estimated'), true);
const notes = optionalText(form, 'notes');
const priority = Number(form.get('priority') ?? 3);
if (
!name ||
estimatedCents === undefined ||
notes === undefined ||
(estimatedCents !== null && estimatedCents < 0)
(estimatedCents !== null && estimatedCents < 0) ||
!Number.isInteger(priority) ||
priority < 1 ||
priority > 5
)
return invalid('Enter a name and a valid estimated cost.');
const values = { name, estimatedCents, notes };
const values = { name, estimatedCents, notes, priority };
const id = recordId(form);
if (id) {
const [row] = await db