From cb27e8d4762ff66bbe3d8fe1d72904b8ea87d2a8 Mon Sep 17 00:00:00 2001 From: Thiago Macedo <1750447+macedot@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:34:37 +0200 Subject: [PATCH 1/3] feat: add fullscreen and Export CSV options to results table --- index.html | 85 ++++++++++++++++++++++++++++++++++- tests/integration/app.spec.js | 30 ++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 994a834..ffc2c27 100644 --- a/index.html +++ b/index.html @@ -70,7 +70,10 @@

Cashflow Simulator

-
+
@@ -481,6 +484,67 @@

+ + +
+
+
+

+ Results ({{ filteredResults.length }} entries) +

+ +
+
+ + + + + + + + + + + + + + + + + +
Date + Cashflow + BalanceItems
+ {{ formatDate(row.date) }} + + {{ row.cashflow >= 0 ? '+' : '' }}{{ row.cashflow.toFixed(2) }} + + {{ row.balance.toFixed(2) }} + + {{ row.items.join(', ') || '—' }} +
+
+
+
+
+
@@ -527,6 +591,20 @@

Results ({{ filteredResults.length }} entries)

+
+ + +
@@ -782,6 +860,8 @@

// Chart fullscreen const chartFullscreen = ref(false); + // Results table fullscreen + const resultsFullscreen = ref(false); // --- Auto-run on mount --- onMounted(() => { @@ -801,6 +881,8 @@

cancelEditEvent(); } else if (chartFullscreen.value) { chartFullscreen.value = false; + } else if (resultsFullscreen.value) { + resultsFullscreen.value = false; } } }); @@ -1452,6 +1534,7 @@

toggleEvents, portfolioCurrency, chartFullscreen, + resultsFullscreen, }; }, }).mount('#app'); diff --git a/tests/integration/app.spec.js b/tests/integration/app.spec.js index 67ed78a..59a43af 100644 --- a/tests/integration/app.spec.js +++ b/tests/integration/app.spec.js @@ -59,7 +59,35 @@ test.describe('Cashflow Simulator App', () => { }); test('CSV export functionality exists', async ({ page }) => { - await expect(page.locator('text=Export CSV')).toBeVisible(); + await expect(page.locator('text=Export CSV').first()).toBeVisible(); + }); + + test('results table has its own Export CSV button that downloads results', async ({ page }) => { + const resultsSection = page.locator('div.bg-white:has(h2:has-text("Results ("))').first(); + const exportButton = resultsSection.getByRole('button', { name: 'Export CSV' }); + await expect(exportButton).toBeVisible(); + + const [download] = await Promise.all([page.waitForEvent('download'), exportButton.click()]); + expect(download.suggestedFilename()).toBe('cashflow-results.csv'); + }); + + test('results table fullscreen toggle opens and closes overlay', async ({ page }) => { + const resultsSection = page.locator('div.bg-white:has(h2:has-text("Results ("))').first(); + await resultsSection.getByRole('button', { name: '⛶' }).click(); + + const overlay = page.locator('.fixed.inset-0.z-50:has-text("Results (")'); + await expect(overlay).toBeVisible(); + await expect(overlay.getByRole('columnheader', { name: 'Date' })).toBeVisible(); + + // Close via ✕ button + await overlay.getByRole('button', { name: '✕' }).click(); + await expect(overlay).toBeHidden(); + + // Reopen and close via Escape + await resultsSection.getByRole('button', { name: '⛶' }).click(); + await expect(overlay).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(overlay).toBeHidden(); }); test('CSV import functionality exists', async ({ page }) => { From 4e5fb348ef912d5e30954a6c0c90c59056cd12e5 Mon Sep 17 00:00:00 2001 From: Thiago Macedo <1750447+macedot@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:59:47 +0200 Subject: [PATCH 2/3] fix: extend sim horizon past bounded endDate so open-ended events continue Importing events where a bounded event (e.g. a loan) ended later than the default period set the simulation end exactly at that endDate, silently truncating open-ended events: an open-ended salary lost its payment in the month after the loan's last payment, and the whole result stopped there. When open-ended events exist, the auto-expanded period now extends one month past the last bounded endDate (clamped to month length) so their recurrence stays visible beyond it. Bounded-only and open-ended-only imports behave exactly as before. --- index.html | 35 +++++++++++++++++++++++++++++++---- tests/integration/app.spec.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index ffc2c27..7713191 100644 --- a/index.html +++ b/index.html @@ -730,6 +730,24 @@

simEnd.value = formatLocalDate(end); } + /** + * Add one month to a YYYY-MM-DD string, clamping the day to the + * target month's length (2027-01-31 → 2027-02-28). + * @param {string} iso - Date in YYYY-MM-DD format + * @returns {string} + */ + function addOneMonthISO(iso) { + const parts = iso.split('-').map(Number); + const y = /** @type {number} */ (parts[0]); + const m = /** @type {number} */ (parts[1]); + const d = /** @type {number} */ (parts[2]); + const nextYear = m === 12 ? y + 1 : y; + const nextMonth = m === 12 ? 1 : m + 1; + const daysInMonth = new Date(Date.UTC(nextYear, nextMonth, 0)).getUTCDate(); + const nextDay = String(Math.min(d, daysInMonth)).padStart(2, '0'); + return `${nextYear}-${String(nextMonth).padStart(2, '0')}-${nextDay}`; + } + // Expand simulation period to cover all current events function expandSimPeriodForEvents() { if (events.value.length === 0) return; @@ -756,10 +774,19 @@

if (earliestStart && earliestStart < simStart.value) { simStart.value = earliestStart; } - // Expand end: prefer latestEnd, fallback to today+1year if events have no endDate - if (latestEnd && latestEnd > simEnd.value) { - simEnd.value = latestEnd; - } else if (!latestEnd || latestEnd < todayStr) { + // Expand end to cover bounded events. When open-ended events + // exist, extend one month past the last bounded end so their + // recurrence stays visible beyond it — otherwise the whole + // simulation would stop at that event's endDate (e.g. a loan + // ending mid-salary-cycle would hide the next salary payment). + const hasOpenEnded = events.value.some(ev => ev.startDate && !ev.endDate); + let desiredEnd = latestEnd; + if (hasOpenEnded && desiredEnd) { + desiredEnd = addOneMonthISO(desiredEnd); + } + if (desiredEnd && desiredEnd > simEnd.value) { + simEnd.value = desiredEnd; + } else if (!desiredEnd || desiredEnd < todayStr) { simEnd.value = oneYearLater; } simPeriodPreset.value = 'custom'; diff --git a/tests/integration/app.spec.js b/tests/integration/app.spec.js index 59a43af..cb4ebce 100644 --- a/tests/integration/app.spec.js +++ b/tests/integration/app.spec.js @@ -219,6 +219,36 @@ test.describe('Cashflow Simulator App', () => { await expect(page.getByText('Ghost', { exact: true })).toHaveCount(0); }); + test('CSV import keeps open-ended events running past a bounded event endDate', async ({ + page, + }) => { + await page.evaluate(() => localStorage.clear()); + await page.reload(); + + await page.locator('input[type="date"]').nth(0).fill('2026-01-01'); // sim start + await page.locator('input[type="date"]').nth(1).fill('2026-03-01'); // sim end + + const csv = [ + 'name,startDate,endDate,frequency,value,currency', + 'Salary,2026-08-28,,monthly,500,USD', + 'Loan,2026-08-12,2027-12-12,monthly,-470,USD', + ].join('\n'); + await page.setInputFiles('input[type="file"]', { + name: 'events.csv', + mimeType: 'text/csv', + buffer: Buffer.from(csv, 'utf8'), + }); + + await expect(page.locator('text=Imported 2 events')).toBeVisible(); + + const table = resultsTable(page); + // The whole horizon must not collapse onto the loan's endDate: + // loan's last payment is on its endDate (inclusive)... + await expect(table.locator('tr', { hasText: '2027-12-12' })).toHaveCount(1); + // ...and the open-ended salary still pays on 2027-12-28, past the loan's end + await expect(table.locator('tr', { hasText: '2027-12-28' })).toHaveCount(1); + }); + test('Fork me on GitHub ribbon links to the repo', async ({ page }) => { const ribbon = page.locator('a.github-fork-ribbon'); await expect(ribbon).toBeVisible(); From f4a9bc90f5b9ebf949dc64210331268cc847ab2b Mon Sep 17 00:00:00 2001 From: Thiago Macedo <1750447+macedot@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:00:01 +0200 Subject: [PATCH 3/3] chore: bump version to 0.6.3 --- index.html | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 7713191..c87f626 100644 --- a/index.html +++ b/index.html @@ -57,7 +57,7 @@

Cashflow Simulator

- v0.6.2 + v0.6.3