A week after implementing the backend for fee-free expense reimbursements, today was about making sure the frontend tells the right story. Numbers need to show up in the right places, warnings need to fire at the right times, and existing flows can't break.
The Problem Space
The reimbursable expenses feature we shipped last week introduced a split: some expenses go through the normal fee calculation, others bypass it entirely. The backend handled this correctly, but the frontend was treating all expenses the same.
Admin Template: Visual Separation
First fix: the admin invoice view lumped all expenses together. Admins need to see which expenses are reimbursable at a glance - they require receipt verification before approval.
The Twig template needed filtering logic. We created two separate sets: regular expenses where reimburseSeparately is false or undefined, and reimbursable expenses where the flag is true. The default-to-false pattern is crucial - existing expenses without the field get treated as regular.
With those two filtered sets, the template renders two side-by-side sections in a grid layout. The left column shows "Utlägg (Regular)" with the standard expense cards. The right column shows "Återbetalbara utlägg" (Reimbursable expenses) with a green accent to visually distinguish them.
The Validation Warning
Here's where business logic meets UX. Reimbursable expenses get paid directly to the freelancer, but what if they exceed what's available after the platform's commission?
The provider calculates this on every change using a memoized function. It extracts the commission amount from the summary data, converts it to a number with a fallback to zero if missing. Then it calculates the available balance by subtracting the commission from the invoice net total. The function returns true if reimbursable expenses exceed that available balance.
The modal uses a two-step confirmation pattern. On first click, if reimbursable expenses exceed the balance and the warning hasn't been shown yet, it sets the warning state to visible and returns early without submitting. The second click proceeds with the actual submission. This isn't a hard block - sometimes freelancers legitimately need to submit expenses that exceed a single invoice. But they need to acknowledge it.
Salary Page: The Calculation Fix
The salary estimation panel was showing wrong numbers for invoices with reimbursable expenses. The issue: payslips have a different data shape than invoice amounts.
The fix in the invoice helper handles the payslip case differently. When processing a payslip, instead of just using the expensesAmount field directly, it now extracts both values separately - the regular expenses amount and the reimbursable expenses amount - converts each to a number with fallbacks to zero, and then adds them together for the total expenses display.
Invoice List: The "Belopp" Column
The invoice list was adding all expenses to the displayed amount. But reimbursable expenses shouldn't be in the invoice total - they're between the platform and freelancer.
The fix calculates two separate sums. The first sum counts all expenses for the dedicated expenses column display. The second sum filters to only include expenses where reimburseSeparately is not true, then sums those for the "Belopp" (Amount) column. The total displayed uses the base amount plus only the regular expenses sum, not the full expenses sum.
Unpaid Invoice Edge Case
One more bug: unpaid invoices weren't showing expenses in the salary estimation panel. The SentDetails component was pulling from the calculated salary amounts, but those don't exist until the invoice is paid.
The fix adds a fallback. It first calculates the total expenses directly from the invoice's expense array by reducing all amounts to a sum. Then when displaying, it uses that invoice-level expenses value as the primary source, falling back to the salary-level expenses value only if the invoice calculation returns zero or undefined.
Backwards Compatibility
The key question: do any of these changes break existing invoices without reimbursable expenses?
All our filters use the pattern "not expense.reimburseSeparately". In JavaScript, negating undefined returns true, so old expenses are treated as regular. Negating false also returns true, so new regular expenses work correctly. Only negating true returns false, which correctly identifies reimbursable expenses. Old invoices work exactly as before.
The Translation Keys
Both English and Swedish translations were added for the warning modal. The English version has a title explaining that reimbursable expenses exceed available balance, content explaining that the excess will need to be covered from other invoices or existing balance, a confirm button saying "Yes, proceed anyway", and a go back button saying "Go back and adjust".
The Swedish version mirrors this: the title "Återbetalbara utlägg överstiger tillgängligt saldo", content explaining the situation, confirm button "Ja, fortsätt ändå", and go back button "Gå tillbaka och justera".
Takeaways
Frontend work on financial features requires tracing every display path:
First, admin views need clear visual distinction for approval workflows.
Second, user-facing totals must match what the business logic actually charges.
Third, estimation panels have different data shapes for different invoice states.
Fourth, list views need careful filtering when calculating aggregate numbers.
Fifth, validation should catch edge cases before they become support tickets.
The reimbursable expenses feature touched invoice creation, list display, detail views, salary estimation, and admin approval. Each had its own assumptions about what "expenses" meant. Unifying that understanding was the real work.