mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
feat(costs): rework Budget into Costs — Splitwise-style, multi-currency, mobile (#1106)
* fix(journey): authorize reads of the journey share link GET /api/journeys/:id/share-link now requires journey access (canAccessJourney), matching the create/delete share-link routes and the get_journey_share_link MCP tool. Returns no link when the caller lacks access to the journey. * feat(costs): rework Budget into Costs — Splitwise-style, multi-currency, mobile Renames the Budget addon to "Costs" (UI only) and reworks it into a Tricount/ Splitwise-style cost tracker: multiple payers per expense, equal split across chosen members, settle-up with persisted history + undo, 12 fixed categories, per-expense currency with live FX conversion to a user-set display currency (Settings -> Display), and locale-correct money formatting. Adds a desktop and a dedicated mobile layout. A migration backfills existing budget items (single payer, split members, currency). Closes #551 (per-expense currency). Also switches the app font to self-hosted Poppins (Geist for secondary subtext), replacing the Google Fonts CDN dependency. * fix(costs): neutral dashboard dark palette + liquid glass, full page width, entry-count badge - Dark mode used a warm oklch palette that read brownish; switch to the neutral zinc tokens used by the dashboard (#121215 bg, #f4f4f5 ink) and add a subtle backdrop-blur glass on cards. - Costs now uses the full available page width on desktop instead of a 1280px cap. - Render the expense count next to the Expenses title as a badge. - Adapt budget/journey unit tests to the new payer-based settlement model and the Costs rename (category default 'other', Costs tab/CostsPanel). * fix(costs): drop the entry-count badge, always show row edit/delete actions Removes the count badge next to the Expenses title and makes the per-row edit/delete actions permanently visible (no longer hover-only) on desktop too. * feat(costs): currency-native money formatting, custom select/date, rename addon to Costs - Format every amount in its own currency convention (symbol position, grouping and decimal separators) regardless of app language, via a currency->locale map (EUR -> '12,00 €', USD -> '$12.00', JPY -> '¥12', ...). Previously Intl used the app locale, so EUR showed the symbol in front under an English UI. - Use TREK's CustomSelect (searchable, with symbols) and CustomDatePicker in the add/edit expense modal instead of the native <select>/<input type=date>. - Rename the 'Budget Planner' add-on to 'Costs' in the admin list (display only; id/tables/permissions/MCP stay 'budget') via seed + a migration for existing DBs. * feat(auth): configurable session duration via SESSION_DURATION Adds a SESSION_DURATION env var (ms-style strings: 1h, 7d, 30d, ...) controlling how long a session stays valid before re-login. It drives both the trek_session JWT exp claim and the cookie maxAge from one source, so they never drift. Invalid values warn at startup and fall back to the default (24h — unchanged). The MFA challenge token and MCP OAuth tokens keep their own TTL. Implements the request from discussion #946. Documented in the env-var wiki page, .env.example and docker-compose.yml.
This commit is contained in:
@@ -2278,6 +2278,68 @@ function runMigrations(db: Database.Database): void {
|
||||
if (!err.message?.includes('no such table')) throw err;
|
||||
}
|
||||
},
|
||||
// Costs rework (budget → "Costs", Tricount/Splitwise style). Adds, additively
|
||||
// and without touching existing rows:
|
||||
// - per-expense currency + exchange_rate, so an expense can be entered in a
|
||||
// foreign currency and converted to the trip base currency (NULL currency =
|
||||
// base currency; rate 1.0). Closes the multi-currency request (#551).
|
||||
// - budget_item_payers: several people can each have paid part of one expense
|
||||
// (amounts in the expense currency), replacing the single paid_by_user_id.
|
||||
// - budget_settlements: persisted "X paid Y" transfers so the settle-up
|
||||
// history (with undo) is shared across all trip members.
|
||||
// The equal-split participants stay in budget_item_members. The single legacy
|
||||
// payer is backfilled into budget_item_payers as one payer covering the total.
|
||||
() => {
|
||||
try { db.exec('ALTER TABLE budget_items ADD COLUMN currency TEXT'); }
|
||||
catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
|
||||
try { db.exec('ALTER TABLE budget_items ADD COLUMN exchange_rate REAL NOT NULL DEFAULT 1'); }
|
||||
catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS budget_item_payers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
budget_item_id INTEGER NOT NULL REFERENCES budget_items(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
amount REAL NOT NULL DEFAULT 0,
|
||||
UNIQUE(budget_item_id, user_id)
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_budget_item_payers_item ON budget_item_payers(budget_item_id)');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS budget_settlements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||
from_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
to_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
amount REAL NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by_user_id INTEGER REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_budget_settlements_trip ON budget_settlements(trip_id)');
|
||||
|
||||
// Backfill the legacy single payer: that person paid the full total of the
|
||||
// expense, in the (base) currency the existing amount was already stored in.
|
||||
try {
|
||||
db.exec(`
|
||||
INSERT OR IGNORE INTO budget_item_payers (budget_item_id, user_id, amount)
|
||||
SELECT id, paid_by_user_id, total_price
|
||||
FROM budget_items
|
||||
WHERE paid_by_user_id IS NOT NULL
|
||||
`);
|
||||
} catch (err: any) {
|
||||
if (!err.message?.includes('no such column')) throw err;
|
||||
}
|
||||
},
|
||||
// Rename the "Budget Planner" addon to "Costs" in the admin add-on list. This
|
||||
// is a display rename only — the addon id, tables, permissions and MCP tools
|
||||
// all stay 'budget'. Scoped to the default name so a customised one is kept.
|
||||
() => {
|
||||
db.prepare(
|
||||
"UPDATE addons SET name = 'Costs', description = 'Track and split trip expenses' WHERE id = 'budget' AND name = 'Budget Planner'",
|
||||
).run();
|
||||
},
|
||||
];
|
||||
|
||||
if (currentVersion < migrations.length) {
|
||||
|
||||
@@ -90,7 +90,7 @@ function seedAddons(db: Database.Database): void {
|
||||
try {
|
||||
const defaultAddons = [
|
||||
{ id: 'packing', name: 'Lists', description: 'Packing lists and to-do tasks for your trips', type: 'trip', icon: 'ListChecks', enabled: 1, sort_order: 0 },
|
||||
{ id: 'budget', name: 'Budget Planner', description: 'Track expenses and plan your travel budget', type: 'trip', icon: 'Wallet', enabled: 1, sort_order: 1 },
|
||||
{ id: 'budget', name: 'Costs', description: 'Track and split trip expenses', type: 'trip', icon: 'Wallet', enabled: 1, sort_order: 1 },
|
||||
{ id: 'documents', name: 'Documents', description: 'Store and manage travel documents', type: 'trip', icon: 'FileText', enabled: 1, sort_order: 2 },
|
||||
{ id: 'vacay', name: 'Vacay', description: 'Personal vacation day planner with calendar view', type: 'global', icon: 'CalendarDays', enabled: 1, sort_order: 10 },
|
||||
{ id: 'atlas', name: 'Atlas', description: 'World map of your visited countries with travel stats', type: 'global', icon: 'Globe', enabled: 1, sort_order: 11 },
|
||||
|
||||
Reference in New Issue
Block a user