From 1bd550b42800818c3abab53284123545cf1466c4 Mon Sep 17 00:00:00 2001 From: Ricearoni1245 Date: Wed, 25 Mar 2026 22:36:10 -0500 Subject: [PATCH] Task: added pagination to records page, removed initial onboarding income from activity chart --- .vscode/settings.json | 4 +++ api/skymoney-api@0.1.0 | 0 api/src/routes/dashboard.ts | 26 +++++++++++++++++--- api/src/routes/transactions.ts | 39 ++++++++++++++++++++++++++++++ api/tests/transactions.test.ts | 37 ++++++++++++++++++++++++++-- api/vitest | 0 web/src/pages/IncomePage.tsx | 3 --- web/src/pages/OnboardingPage.tsx | 6 ++++- web/src/pages/TransactionsPage.tsx | 22 +++++++++-------- 9 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 api/skymoney-api@0.1.0 create mode 100644 api/vitest diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5b72bbf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "atlascode.jira.explorer.enabled": true, + "atlascode.jira.explorer.fetchAllQueryResults": true +} \ No newline at end of file diff --git a/api/skymoney-api@0.1.0 b/api/skymoney-api@0.1.0 new file mode 100644 index 0000000..e69de29 diff --git a/api/src/routes/dashboard.ts b/api/src/routes/dashboard.ts index 5b54e55..770d29d 100644 --- a/api/src/routes/dashboard.ts +++ b/api/src/routes/dashboard.ts @@ -3,6 +3,7 @@ import { getUserMidnightFromDateOnly } from "../allocator.js"; import { getUserTimezone } from "../services/user-context.js"; const DAY_MS = 24 * 60 * 60 * 1000; +const ONBOARDING_SEED_NOTE_MARKER = "[onboarding-seed]"; const monthKey = (date: Date) => `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`; @@ -30,7 +31,7 @@ const dashboardRoutes: FastifyPluginAsync = async (app) => { const now = new Date(); const dashboardTxKinds = ["variable_spend", "fixed_payment"]; - const [cats, plans, recentTxs, agg, allocAgg, incomeEvents, spendTxs, user] = await Promise.all([ + const [cats, plans, recentTxs, agg, allocAgg, incomeEvents, firstIncomeEvent, spendTxs, user] = await Promise.all([ app.prisma.variableCategory.findMany({ where: { userId }, orderBy: [{ priority: "asc" }, { name: "asc" }], @@ -54,8 +55,16 @@ const dashboardRoutes: FastifyPluginAsync = async (app) => { _sum: { amountCents: true }, }), app.prisma.incomeEvent.findMany({ - where: { userId, postedAt: { gte: rangeStart } }, - select: { postedAt: true, amountCents: true }, + where: { + userId, + postedAt: { gte: rangeStart }, + }, + select: { id: true, postedAt: true, amountCents: true, note: true }, + }), + app.prisma.incomeEvent.findFirst({ + where: { userId }, + orderBy: { postedAt: "asc" }, + select: { id: true }, }), app.prisma.transaction.findMany({ where: { @@ -140,8 +149,17 @@ const dashboardRoutes: FastifyPluginAsync = async (app) => { }; const percentTotal = cats.reduce((sum, cat) => sum + cat.percent, 0); + const filteredIncomeEvents = incomeEvents.filter((evt) => { + if (evt.id.startsWith("onboarding-")) return false; + const note = evt.note?.trim().toLowerCase() ?? ""; + if (note.includes(ONBOARDING_SEED_NOTE_MARKER)) return false; + // Legacy onboarding seed for regular users before explicit marker. + if (evt.id === firstIncomeEvent?.id && note.includes("initial budget setup")) return false; + return true; + }); + const incomeByMonth = new Map(); - incomeEvents.forEach((evt) => { + filteredIncomeEvents.forEach((evt) => { const key = monthKey(evt.postedAt); incomeByMonth.set(key, (incomeByMonth.get(key) ?? 0) + Number(evt.amountCents ?? 0n)); }); diff --git a/api/src/routes/transactions.ts b/api/src/routes/transactions.ts index 4dedf39..e15c1ff 100644 --- a/api/src/routes/transactions.ts +++ b/api/src/routes/transactions.ts @@ -34,6 +34,39 @@ type TransactionsRoutesOptions = { }; const isDate = (s?: string) => !!s && /^\d{4}-\d{2}-\d{2}$/.test(s); +const parseDateSearchTerm = (value: string): string | null => { + const trimmed = value.trim(); + if (isDate(trimmed)) return trimmed; + + const match = trimmed.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/); + if (!match) return null; + + const month = Number(match[1]); + const day = Number(match[2]); + const year = Number(match[3]); + if ( + !Number.isInteger(month) || + !Number.isInteger(day) || + !Number.isInteger(year) || + month < 1 || + month > 12 || + day < 1 || + day > 31 + ) { + return null; + } + + const normalized = new Date(Date.UTC(year, month - 1, day)); + if ( + normalized.getUTCFullYear() !== year || + normalized.getUTCMonth() + 1 !== month || + normalized.getUTCDate() !== day + ) { + return null; + } + + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; +}; const transactionsRoutes: FastifyPluginAsync = async ( app, @@ -518,9 +551,15 @@ const transactionsRoutes: FastifyPluginAsync = async if (typeof q === "string" && q.trim() !== "") { const qTrim = q.trim(); const asCents = opts.parseCurrencyToCents(qTrim); + const asDate = parseDateSearchTerm(qTrim); if (asCents > 0) { flexibleOr.push({ amountCents: opts.toBig(asCents) }); } + if (asDate) { + flexibleOr.push({ + occurredAt: getUserDateRangeFromDateOnly(userTimezone, asDate, asDate), + }); + } flexibleOr.push({ note: { contains: qTrim, mode: "insensitive" } }); flexibleOr.push({ category: { name: { contains: qTrim, mode: "insensitive" } } }); flexibleOr.push({ plan: { name: { contains: qTrim, mode: "insensitive" } } }); diff --git a/api/tests/transactions.test.ts b/api/tests/transactions.test.ts index f656187..0576285 100644 --- a/api/tests/transactions.test.ts +++ b/api/tests/transactions.test.ts @@ -21,6 +21,8 @@ afterAll(async () => { describe("GET /transactions", () => { let catId: string; let planId: string; + let tx1Id: string; + let tx2Id: string; beforeEach(async () => { await resetUser(U); @@ -54,23 +56,28 @@ describe("GET /transactions", () => { }); planId = plan.id; + tx1Id = `t_${Date.now()}_1`; + tx2Id = `t_${Date.now()}_2`; + await prisma.transaction.createMany({ data: [ { - id: `t_${Date.now()}_1`, + id: tx1Id, userId: U, occurredAt: new Date("2025-01-03T12:00:00.000Z"), kind: "variable_spend", categoryId: catId, amountCents: 1000n, + note: "Groceries pickup", }, { - id: `t_${Date.now()}_2`, + id: tx2Id, userId: U, occurredAt: new Date("2025-01-10T12:00:00.000Z"), kind: "fixed_payment", planId, amountCents: 2000n, + note: "January rent", }, ], }); @@ -103,6 +110,32 @@ describe("GET /transactions", () => { expect(byPlan.status).toBe(200); expect(byPlan.body.items.every((t: any) => t.planId === planId)).toBe(true); }); + + it("searches by category, note, amount, and date", async () => { + const byCategory = await request(app.server) + .get("/transactions?q=grocer") + .set("x-user-id", U); + expect(byCategory.status).toBe(200); + expect(byCategory.body.items.some((t: any) => t.id === tx1Id)).toBe(true); + + const byNote = await request(app.server) + .get("/transactions?q=pickup") + .set("x-user-id", U); + expect(byNote.status).toBe(200); + expect(byNote.body.items.some((t: any) => t.id === tx1Id)).toBe(true); + + const byAmount = await request(app.server) + .get("/transactions?q=10.00") + .set("x-user-id", U); + expect(byAmount.status).toBe(200); + expect(byAmount.body.items.some((t: any) => t.id === tx1Id)).toBe(true); + + const byDate = await request(app.server) + .get("/transactions?q=2025-01-03") + .set("x-user-id", U); + expect(byDate.status).toBe(200); + expect(byDate.body.items.some((t: any) => t.id === tx1Id)).toBe(true); + }); }); describe("POST /transactions", () => { diff --git a/api/vitest b/api/vitest new file mode 100644 index 0000000..e69de29 diff --git a/web/src/pages/IncomePage.tsx b/web/src/pages/IncomePage.tsx index ef6fe22..db3f0b9 100644 --- a/web/src/pages/IncomePage.tsx +++ b/web/src/pages/IncomePage.tsx @@ -294,9 +294,6 @@ export default function IncomePage() { const payload: CreateIncomeInput = { amountCents, }; - if (debugNowISO) { - payload.occurredAtISO = debugNowISO; - } const trimmedNote = notes.trim(); if (trimmedNote) { diff --git a/web/src/pages/OnboardingPage.tsx b/web/src/pages/OnboardingPage.tsx index 9aef2d2..ad1a853 100644 --- a/web/src/pages/OnboardingPage.tsx +++ b/web/src/pages/OnboardingPage.tsx @@ -74,6 +74,7 @@ const defaultSchedule = (): AutoPaySchedule => ({ }); const DEFAULT_SAVINGS_PERCENT = 20; const MIN_SAVINGS_PERCENT = DEFAULT_SAVINGS_PERCENT; +const ONBOARDING_SEED_NOTE_MARKER = "[onboarding-seed]"; const normalizeCategoryName = (value: string) => value.trim().toLowerCase(); const normalizePercentValue = (value: unknown) => { const parsed = @@ -476,7 +477,10 @@ export default function OnboardingPage() { }); } else { // Use regular income allocation for regular income - await apiPost("/income", { amountCents: budgetCents }); + await apiPost("/income", { + amountCents: budgetCents, + note: `Initial budget setup ${ONBOARDING_SEED_NOTE_MARKER}`, + }); } } diff --git a/web/src/pages/TransactionsPage.tsx b/web/src/pages/TransactionsPage.tsx index 0cd783d..319711b 100644 --- a/web/src/pages/TransactionsPage.tsx +++ b/web/src/pages/TransactionsPage.tsx @@ -15,7 +15,7 @@ export default function TransactionsPage() { const [catFilter, setCatFilter] = useState("all"); const [dateFilter, setDateFilter] = useState<"month" | "30" | "all">("month"); const [page, setPage] = useState(1); - const limit = 100; + const limit = 10; // Get current date in user's timezone const todayStr = getTodayInTimezone(userTimezone); @@ -53,14 +53,16 @@ export default function TransactionsPage() { const total = txQuery.data?.total ?? 0; const catOptions = useMemo(() => { - if (transactions.length === 0) return []; - const buckets = new Map(); - transactions.forEach((t) => { - if (t.categoryId && t.categoryName) buckets.set(t.categoryId, t.categoryName); - if (t.planId && t.planName) buckets.set(t.planId, t.planName); - }); - return Array.from(buckets.entries()).map(([id, name]) => ({ id, name })); - }, [transactions]); + const categoryOptions = (dashboard?.variableCategories ?? []).map((c) => ({ + id: c.id, + name: c.name, + })); + const planOptions = (dashboard?.fixedPlans ?? []).map((p) => ({ + id: p.id, + name: p.name, + })); + return [...categoryOptions, ...planOptions]; + }, [dashboard?.variableCategories, dashboard?.fixedPlans]); @@ -74,7 +76,7 @@ export default function TransactionsPage() {
{ setSearch(e.target.value);