-
Claude authored
An expense recorded with a scan attached arrived in the database with no scan at all. ExpenseForm uploaded the file and passed the path to ExpenseService::recordExpense(), which builds its Expense::create() array by hand and never copied the two attachment keys across — so the file landed on disk and the row forgot about it. It landed on the `public` disk too, which needs a storage symlink the containers never create, so even a persisted path would have 404'd. Receipts now go to the private disk and are read back through ExpenseAttachmentController, which checks the permission, the academy and the active branch before streaming a byte. The list was also a dead end: a row showed a number and a description and offered nothing but "cancel". Rows are now clickable and carry a view button, with a paperclip marking the ones that have evidence behind them. The new detail page is where the expense explains itself — amount, category, recipient, method, receipt reference, branch, notes, who recorded it and when, and, if it was cancelled, by whom and why. Below that sit the journal entries it produced, the original debit/credit pair and any reversing entry, so the accounting effect is visible rather than implied. The receipt itself previews inline: images as images, PDFs in a frame, with download beside them. An expense recorded without a scan is no longer stuck that way — attach one from the detail page, replace it (the displaced file is deleted), or remove it. Every attachment records who uploaded it and when. A cancelled expense refuses all three: its evidence is frozen with its journal. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
cc417ba9