feat: UX improvements — nav, reports tabs, dashboard alerts, transaction hints, settings layout

- Move Reports to Finans nav group and auto-expand Övrigt on its pages
- Make report tabs horizontally scrollable with gradient fade on mobile
- Surface deadlines and alerts above the fold on dashboard
- Add dismissible categorization hint card on transactions page
- Split settings company form into 4 separate Cards for scannability
- Add monthly breakdown report, document upload zone, journal entry attachments
- Add batch category selector, receipt document linking, invoice form improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-02-20 17:16:22 +01:00
co-authored by Claude Opus 4.6
parent ba94f60d06
commit d0c0d8a7d2
23 changed files with 2065 additions and 302 deletions
+90 -4
View File
@@ -1,13 +1,25 @@
'use client'
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { ChevronDown, ChevronRight, Paperclip, AlertTriangle } from 'lucide-react'
import { AccountNumber } from '@/components/ui/account-number'
import JournalEntryAttachments from '@/components/bookkeeping/JournalEntryAttachments'
import type { JournalEntry, JournalEntryLine } from '@/types'
const NEEDS_ATTACHMENT = new Set([
'manual',
'bank_transaction',
'supplier_invoice_registered',
'supplier_invoice_paid',
'supplier_invoice_cash_payment',
'import',
])
interface Props {
periodId?: string
}
@@ -18,8 +30,23 @@ export default function JournalEntryList({ periodId }: Props) {
const [expandedId, setExpandedId] = useState<string | null>(null)
const [count, setCount] = useState(0)
const [page, setPage] = useState(0)
const [attachmentCounts, setAttachmentCounts] = useState<Record<string, number>>({})
const [showMissingOnly, setShowMissingOnly] = useState(false)
const pageSize = 20
const fetchAttachmentCounts = useCallback(async (entryIds: string[]) => {
if (entryIds.length === 0) return
try {
const res = await fetch(
`/api/documents/counts?journal_entry_ids=${entryIds.join(',')}`
)
const { data } = await res.json()
setAttachmentCounts(data || {})
} catch {
console.error('Failed to fetch attachment counts')
}
}, [])
useEffect(() => {
fetchEntries()
}, [periodId, page])
@@ -34,11 +61,20 @@ export default function JournalEntryList({ periodId }: Props) {
const res = await fetch(`/api/bookkeeping/journal-entries?${params}`)
const { data, count: total } = await res.json()
setEntries(data || [])
const loadedEntries = data || []
setEntries(loadedEntries)
setCount(total || 0)
setLoading(false)
// Fetch attachment counts for the loaded entries
const ids = loadedEntries.map((e: JournalEntry) => e.id)
fetchAttachmentCounts(ids)
}
const handleAttachmentCountChange = useCallback((entryId: string, count: number) => {
setAttachmentCounts((prev) => ({ ...prev, [entryId]: count }))
}, [])
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : id)
}
@@ -66,6 +102,12 @@ export default function JournalEntryList({ periodId }: Props) {
salary_payment: 'Lön',
opening_balance: 'Ingående balans',
year_end: 'Årsbokslut',
supplier_invoice_registered: 'Leverantörsfaktura',
supplier_invoice_paid: 'Leverantörsbetalning',
supplier_invoice_cash_payment: 'Kontant leverantörsbetalning',
import: 'Import',
storno: 'Storno',
correction: 'Korrigering',
}
return labels[source] || source
}
@@ -90,10 +132,36 @@ export default function JournalEntryList({ periodId }: Props) {
)
}
const filteredEntries = showMissingOnly
? entries.filter(
(e) =>
NEEDS_ATTACHMENT.has(e.source_type) &&
!attachmentCounts[e.id] &&
e.status === 'posted'
)
: entries
return (
<div className="space-y-4">
{/* Missing attachment filter */}
<div className="flex items-center gap-2">
<Switch
id="missing-attachments"
checked={showMissingOnly}
onCheckedChange={setShowMissingOnly}
/>
<Label htmlFor="missing-attachments" className="text-sm cursor-pointer">
Visa saknade underlag
</Label>
{showMissingOnly && (
<Badge variant="secondary" className="text-xs">
{filteredEntries.length}
</Badge>
)}
</div>
<div className="space-y-2">
{entries.map((entry) => {
{filteredEntries.map((entry) => {
const isExpanded = expandedId === entry.id
const lines = (entry.lines || []) as JournalEntryLine[]
@@ -116,6 +184,19 @@ export default function JournalEntryList({ periodId }: Props) {
{entry.entry_date}
</span>
<span className="flex-1 truncate">{entry.description}</span>
{/* Attachment indicator */}
{attachmentCounts[entry.id] ? (
<span className="flex items-center gap-0.5 text-muted-foreground mr-1" title={`${attachmentCounts[entry.id]} underlag`}>
<Paperclip className="h-3.5 w-3.5" />
<span className="text-xs">{attachmentCounts[entry.id]}</span>
</span>
) : (
NEEDS_ATTACHMENT.has(entry.source_type) && entry.status === 'posted' && (
<span className="mr-1" title="Underlag saknas">
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
</span>
)
)}
<Badge variant="outline" className="text-xs mr-2">
{sourceLabel(entry.source_type)}
</Badge>
@@ -178,6 +259,11 @@ export default function JournalEntryList({ periodId }: Props) {
</tr>
</tfoot>
</table>
<JournalEntryAttachments
journalEntryId={entry.id}
onCountChange={(c) => handleAttachmentCountChange(entry.id, c)}
/>
</CardContent>
)}
</Card>