• Claude's avatar
    Give every expense a receipt you can open · cc417ba9
    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: 's avatarClaude Opus 5 <noreply@anthropic.com>
    cc417ba9