Add daily expense tracking with sort/split and CSV export
Standalone expenses (date, description, category, amount) with the same equal/own/payer split machinery as entry costs, merged into the costs panel and settle-up as a single expense bucket. Self-fetching card with per-day grouping, client-side sorting, and quick-add. CSV export interleaves expenses with priced entries, one share column per member (UTF-8 BOM, RFC 4180, formula-injection guard on text cells). Also fixes trip deletion, which hit a foreign-key violation and rolled back for any trip with checklist items.
This commit is contained in:
@@ -10,6 +10,7 @@ import { renderCalendar } from './calendar.js';
|
||||
import { renderMap } from './map.js';
|
||||
import { renderSummary } from './summary.js';
|
||||
import { renderCosts } from './costs.js';
|
||||
import { renderExpenses } from './expenses.js';
|
||||
import { renderChecklist } from './checklist.js';
|
||||
import { renderCountdown } from './flipclock.js';
|
||||
import { openDayEditor } from './dayEditor.js';
|
||||
@@ -24,6 +25,11 @@ export function renderTripDetail(container, ctx, id) {
|
||||
_onModalRefresh: null,
|
||||
};
|
||||
|
||||
// The mounted Costs section node, so refreshCosts() can swap it in place
|
||||
// without redrawing the whole page (self-fetching cards like Expenses
|
||||
// trigger this instead of tctx.refreshTrip()).
|
||||
let costsSection = null;
|
||||
|
||||
mount(container, loading('Loading trip…'));
|
||||
init();
|
||||
|
||||
@@ -48,6 +54,23 @@ export function renderTripDetail(container, ctx, id) {
|
||||
}
|
||||
};
|
||||
|
||||
// Lighter-weight than refreshTrip(): re-fetches only /costs and swaps the
|
||||
// Costs section in place, so a self-fetching card (Expenses) can keep the
|
||||
// settle-up numbers current without re-fetching the trip/route or
|
||||
// re-mounting every other panel.
|
||||
tctx.refreshCosts = async () => {
|
||||
try {
|
||||
tctx.costs = await api.trips.costs(id);
|
||||
if (costsSection && costsSection.isConnected) {
|
||||
const next = renderCosts(tctx);
|
||||
costsSection.replaceWith(next);
|
||||
costsSection = next;
|
||||
}
|
||||
} catch (err) {
|
||||
toast(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
tctx.openDay = (date) => openDayEditor(tctx, date);
|
||||
|
||||
async function init() {
|
||||
@@ -70,12 +93,20 @@ export function renderTripDetail(container, ctx, id) {
|
||||
page.appendChild(renderHeader());
|
||||
page.appendChild(renderCountdown(tctx));
|
||||
page.appendChild(renderCalendar(tctx));
|
||||
costsSection = renderCosts(tctx);
|
||||
page.appendChild(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'detail-grid' },
|
||||
renderMap(tctx),
|
||||
el('div', { class: 'detail-side' }, renderSummary(tctx), renderCosts(tctx), renderChecklist(tctx)),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'detail-side' },
|
||||
renderSummary(tctx),
|
||||
costsSection,
|
||||
renderExpenses(tctx),
|
||||
renderChecklist(tctx),
|
||||
),
|
||||
),
|
||||
);
|
||||
mount(container, page);
|
||||
|
||||
Reference in New Issue
Block a user