Today was a bugfix marathon on a freelancer invoicing platform. Eight tickets ranging from state management issues to missing admin message visibility. Each fix taught something about React patterns, API design decisions, and the importance of historical data preservation.
The Settings Reset Bug
The bug report: tax and pension settings would revert to previous values after saving. Users could see their changes applied momentarily, then watch them disappear.
The culprit was in the SalarySettings component. The handleUpdate function was dispatching actions with the data directly as the payload. The fix was wrapping that data in a user object before dispatching. The reducer expected the payload to have a user key containing the settings. Without it, the data spread directly into the root state, corrupting the structure. On next render, the component read from the now-malformed state and displayed stale values.
The fix was a single line, but understanding why it failed required tracing the data flow from component through dispatch to reducer.
Invoice Sorting By Wrong Date
Dashboard invoices were supposed to show the most recently sent first. Instead, they were sorted by updatedAt - which changed whenever any field was modified, not when the invoice was actually sent.
The fix was changing the API request parameter from ordering by updatedAt descending to ordering by sentAt descending. The difference matters: an invoice sent January 1st but edited January 8th would appear at the top with updatedAt sorting. Users expected chronological send order.
Historical Subscription Display
This one was subtle. The invoice summary showed "PRO" or "3.95%" based on the user's current subscription status - not what they had when the invoice was created.
The backend already had the answer - a proEligibleAtSend boolean stored on each invoice. The invoice helper was using the isProUser hook which checked current subscription. The fix was replacing that with the proEligibleAtSend field from the invoice data itself, which preserved the historical status. Small change, correct behavior.
Displaying Admin Messages
The big feature request: when an invoice fails to send, users saw "Ej skickad" (Not sent) but no explanation. The backend had both errorCode and adminMessage fields - they just weren't displayed.
A new NotSentDetails component was created. It takes an invoice as a prop, extracts the errorCode and adminMessage fields, and renders them in a styled container with an alert icon. If neither field exists, it returns null. The error code gets translated using the internationalization system with a fallback to "Unknown error" if the code isn't found.
The backend had 19 different error codes that needed translations. These cover scenarios like invalid currency, invalid organization number, missing recipient email, missing payment provider registration, missing recipient name, invalid address, invalid city, invalid invoice email, invalid zip code, recipient name too long, e-invoice required, missing recipient address, invalid VAT number, e-invoice connection pending, customer blacklisted, invalid characters, and invalid attachments.
One refinement: rows with NOT_SENT status now auto-expand to show the error immediately. This required adding a defaultOpen prop to the BaseRow component. The component initializes its open state and chevron icon based on this prop. When an invoice row is rendered, it checks if the status is NOT_SENT and passes that as the defaultOpen value.
Smaller Fixes
Invoice Preview Reference
The preview showed company name instead of contact name for "Er referens" (Your reference). The fix was changing from displaying the customer's name field to displaying the customer's contactName field.
Also changed the button text from "Klicka för att zooma in" (Click to zoom in) to "Se förhandsvisning" (See preview) - clearer intent.
Mobile Menu Referral Card
Added the referral sidebar card to the mobile "More" menu. A simple import and component addition.
Payment Info Removal
Removed a block of payment method text from the settings page that was confusing users.
Salary Widget Updates
Three changes to the salary balance widget. First, changed button text from "Payout" to "Direktutbetalning" (Direct payment). Second, moved the button from the bottom to the header row. Third, fixed the date format - there was a hardcoded Polish locale being used for some reason. Removing that locale parameter let it use the default.
The Pattern
Files Modified
Eleven files were modified in the frontend. The Invoice Row component received the NotSentDetails component and auto-expand logic. BaseRow got the new defaultOpen prop. The Mobile menu component received the referral card. SalarySettings had the state update fix. The Invoice Preview component got the customer reference field fix. The invoice helper started using proEligibleAtSend instead of the current user status. The Balance widget received the button repositioning and date format fix. The dashboard widgets updated the sort order. PlanManagement had the payment info removed. Both Swedish and English translation files got the 19 new error code translations.
Lessons
First, trace the data flow. The settings bug was one line, but finding it required following dispatch to reducer to state to component.
Second, historical data preservation matters. Timestamps and status fields captured at creation time - like sentAt and proEligibleAtSend - exist for a reason. Use them instead of current state.
Third, API fields are cheap to expose. The backend already had adminMessage and errorCode in the serialization groups. Zero backend work needed.
Fourth, consistent component patterns pay off. Adding defaultOpen to BaseRow was the right abstraction. The component didn't need to know why a row should be open - just that it should be.
Fifth, translation files are documentation. The 19 error codes in the Swedish and English JSON files now serve as a reference for what can go wrong with invoice sending.
Eight tickets closed. The platform's a bit less frustrating to use.