Money: move /projects/money-tracker to /trackers/money, rename to Money, 308 redirects from old paths (bank-callback query kept)
All checks were successful
Deploy production / deploy (push) Successful in 2m49s

This commit is contained in:
2026-10-24 02:37:07 +00:00
parent 2af279e8a9
commit 988c6c31a9
17 changed files with 67 additions and 29 deletions

View File

@@ -0,0 +1,114 @@
import { db } from '#lib/server/db/index.js';
import { moneyAccounts, moneyTransactions } from '#lib/server/db/schema/index.js';
import { requireAdmin } from '#lib/server/authorization.js';
import { positiveId, validDate } from '#lib/server/money-tracker.js';
import {
and,
asc,
count,
desc,
eq,
gt,
gte,
ilike,
inArray,
isNotNull,
lt,
lte,
or,
sql,
sum,
type SQL
} from 'drizzle-orm';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const PAGE_SIZE = 50;
const sources = ['manual', 'csv', 'enablebanking', 'linked'] as const;
const sorts = ['newest', 'oldest', 'largest', 'smallest'] as const;
export const load: PageServerLoad = async (event) => {
if (!event.locals.user)
redirect(303, '/login?returnTo=' + encodeURIComponent(event.url.pathname + event.url.search));
const user = requireAdmin(event);
const params = event.url.searchParams;
const q = (params.get('q') ?? '').trim().slice(0, 100);
const accountIds = params
.getAll('account')
.map(positiveId)
.filter((id): id is number => id !== null);
const source = sources.find((item) => item === params.get('source')) ?? '';
const type = params.get('type') === 'in' ? 'in' : params.get('type') === 'out' ? 'out' : '';
const from = validDate(params.get('from')) ?? '';
const to = validDate(params.get('to')) ?? '';
const sort = sorts.find((item) => item === params.get('sort')) ?? 'newest';
const requestedPage = positiveId(params.get('page')) ?? 1;
const t = moneyTransactions;
const filters: (SQL | undefined)[] = [eq(t.userId, user.id)];
if (q) {
const pattern = '%' + q.replace(/[\\%_]/g, (c) => '\\' + c) + '%';
filters.push(or(ilike(t.description, pattern), ilike(t.counterparty, pattern)));
}
if (accountIds.length) filters.push(inArray(t.accountId, accountIds));
if (source === 'linked') filters.push(and(eq(t.source, 'manual'), isNotNull(t.externalId)));
else if (source) filters.push(eq(t.source, source));
if (type === 'in') filters.push(gt(t.amountCents, 0));
if (type === 'out') filters.push(lt(t.amountCents, 0));
if (from) filters.push(gte(t.date, from));
if (to) filters.push(lte(t.date, to));
const where = and(...filters);
const [accounts, [totals]] = await Promise.all([
db
.select({
id: moneyAccounts.id,
name: moneyAccounts.name,
color: moneyAccounts.color,
scope: moneyAccounts.scope
})
.from(moneyAccounts)
.where(eq(moneyAccounts.userId, user.id))
.orderBy(asc(moneyAccounts.name)),
db
.select({ count: count(), sum: sum(t.amountCents) })
.from(t)
.where(where)
]);
const pages = Math.max(1, Math.ceil(totals.count / PAGE_SIZE));
const page = Math.min(requestedPage, pages);
const order =
sort === 'oldest'
? [asc(t.date), asc(t.id)]
: sort === 'largest'
? [desc(t.amountCents), desc(t.date)]
: sort === 'smallest'
? [asc(t.amountCents), desc(t.date)]
: [desc(t.date), desc(t.id)];
const rows = await db
.select({
id: t.id,
date: t.date,
description: t.description,
counterparty: t.counterparty,
amountCents: t.amountCents,
accountId: t.accountId,
source: t.source,
linked: sql<boolean>`${t.externalId} is not null`
})
.from(t)
.where(where)
.orderBy(...order)
.limit(PAGE_SIZE)
.offset((page - 1) * PAGE_SIZE);
return {
accounts,
rows,
total: totals.count,
sumCents: Number(totals.sum ?? 0),
page,
pages,
filters: { q, accountIds, source, type, from, to, sort }
};
};