mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01: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:
@@ -90,7 +90,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
{/* Navbar lives outside .trek-dash so it keeps the app-wide font + button
|
||||
styling instead of inheriting the dashboard scope's Geist font and the
|
||||
styling instead of inheriting the dashboard scope's font and the
|
||||
`.trek-dash button` reset (which shifted the bell icon + menu items). */}
|
||||
<Navbar />
|
||||
<div className="trek-dash trek-dash-shell">
|
||||
|
||||
@@ -176,7 +176,7 @@ export default function LoginPage(): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif", position: 'relative' }}>
|
||||
<div style={{ minHeight: '100vh', display: 'flex', fontFamily: "var(--font-system)", position: 'relative' }}>
|
||||
|
||||
{/* Language dropdown */}
|
||||
<div style={{ position: 'absolute', top: 16, right: 16, zIndex: 10 }}>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function SharedTripPage() {
|
||||
const center = mapPlaces.length > 0 ? [mapPlaces[0].lat, mapPlaces[0].lng] : [48.85, 2.35]
|
||||
|
||||
return (
|
||||
<div className="bg-surface-secondary" style={{ minHeight: '100vh', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
|
||||
<div className="bg-surface-secondary" style={{ minHeight: '100vh', fontFamily: "var(--font-system)" }}>
|
||||
{/* Header */}
|
||||
<div className="text-white" style={{ background: 'linear-gradient(135deg, #000 0%, #0f172a 50%, #1e293b 100%)', padding: '32px 20px 28px', textAlign: 'center', position: 'relative' }}>
|
||||
{/* Cover image background */}
|
||||
|
||||
@@ -97,8 +97,8 @@ vi.mock('../components/Files/FileManager', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../components/Budget/BudgetPanel', () => ({
|
||||
default: () => React.createElement('div', { 'data-testid': 'budget-panel' }),
|
||||
vi.mock('../components/Budget/CostsPanel', () => ({
|
||||
default: () => React.createElement('div', { 'data-testid': 'costs-panel' }),
|
||||
}));
|
||||
|
||||
vi.mock('../components/Packing/PackingListPanel', () => ({
|
||||
@@ -436,8 +436,8 @@ describe('TripPlannerPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-PLANNER-012: Budget tab renders BudgetPanel', () => {
|
||||
it('shows BudgetPanel after clicking the Budget tab with budget addon enabled', async () => {
|
||||
describe('FE-PAGE-PLANNER-012: Costs tab renders CostsPanel', () => {
|
||||
it('shows CostsPanel after clicking the Costs tab with budget addon enabled', async () => {
|
||||
server.use(
|
||||
http.get('/api/addons', () =>
|
||||
HttpResponse.json({ addons: [{ id: 'budget', type: 'budget' }] })
|
||||
@@ -454,11 +454,11 @@ describe('TripPlannerPage', () => {
|
||||
|
||||
vi.useRealTimers();
|
||||
|
||||
const budgetTab = await screen.findByTitle('Budget');
|
||||
fireEvent.click(budgetTab);
|
||||
const costsTab = await screen.findByTitle('Costs');
|
||||
fireEvent.click(costsTab);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('budget-panel')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('costs-panel')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import PackingListPanel from '../components/Packing/PackingListPanel'
|
||||
import ApplyTemplateButton from '../components/Packing/ApplyTemplateButton'
|
||||
import TodoListPanel from '../components/Todo/TodoListPanel'
|
||||
import FileManager from '../components/Files/FileManager'
|
||||
import BudgetPanel from '../components/Budget/BudgetPanel'
|
||||
import CostsPanel from '../components/Budget/CostsPanel'
|
||||
import CollabPanel from '../components/Collab/CollabPanel'
|
||||
import Navbar from '../components/Layout/Navbar'
|
||||
import { useToast } from '../components/shared/Toast'
|
||||
@@ -647,7 +647,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
||||
|
||||
{activeTab === 'finanzplan' && (
|
||||
<div style={{ height: '100%', overflowY: 'auto', overscrollBehavior: 'contain', width: '100%', paddingBottom: 'var(--bottom-nav-h)' }}>
|
||||
<BudgetPanel tripId={tripId} tripMembers={tripMembers} />
|
||||
<CostsPanel tripId={tripId} tripMembers={tripMembers} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -604,7 +604,7 @@ export function useTripPlanner() {
|
||||
const defaultCenter = [settings.default_lat || 48.8566, settings.default_lng || 2.3522]
|
||||
const defaultZoom = settings.default_zoom || 10
|
||||
|
||||
const fontStyle = { fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif" }
|
||||
const fontStyle = { fontFamily: "var(--font-system)" }
|
||||
|
||||
// Splash screen — show for initial load + a brief moment for photos to start loading
|
||||
const [splashDone, setSplashDone] = useState(false)
|
||||
|
||||
Reference in New Issue
Block a user