feat: mobile UX improvements across dashboard (#9)
* feat: mobile UX improvements across dashboard pages - Responsive layouts with stacked mobile forms and grid desktop views - Safe area handling (viewportFit cover, bottom nav insets, main padding) - Touch-friendly dialogs, tabs, and page headers - Mobile nav drawer open/close animation - Dashboard layout parallel Supabase queries - SIE parser: handle quoted VER/TRANS fields - Skip account override for template-based transaction booking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback from Greptile - Fix nav drawer close timer race condition with useRef + clearTimeout - Show locked VAT rate as read-only label on mobile instead of hiding it - Remove dead bottom-16 class overridden by inline style Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
109f860e22
commit
a1ca96453c
@@ -126,7 +126,7 @@ export function AddAccountDialog({ open, onOpenChange, onCreated }: AddAccountDi
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Kontonummer</Label>
|
||||
<Input
|
||||
|
||||
@@ -419,7 +419,7 @@ export default function ChartOfAccountsManager() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
className="h-7 w-7 min-h-[44px] min-w-[44px]"
|
||||
onClick={() => setEditAccount(account)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
@@ -428,7 +428,7 @@ export default function ChartOfAccountsManager() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:text-destructive"
|
||||
className="h-7 w-7 min-h-[44px] min-w-[44px] text-destructive hover:text-destructive"
|
||||
onClick={() => deleteAccount(account)}
|
||||
disabled={deletingAccount === account.account_number}
|
||||
>
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-3xl max-h-[95dvh] sm:max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Skapa ändringsverifikation</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -139,34 +139,50 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
|
||||
</div>
|
||||
<p className="text-sm">{entry.description}</p>
|
||||
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-1.5 w-48">Konto</th>
|
||||
<th className="py-1.5">Beskrivning</th>
|
||||
<th className="py-1.5 w-28 text-right">Debet</th>
|
||||
<th className="py-1.5 w-28 text-right">Kredit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{originalLines.map((line) => (
|
||||
<tr key={line.id} className="border-b last:border-0">
|
||||
<td className="py-1.5"><AccountNumber number={line.account_number} showName /></td>
|
||||
<td className="py-1.5 text-muted-foreground">{line.line_description || ''}</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{Number(line.debit_amount) > 0
|
||||
? Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{Number(line.credit_amount) > 0
|
||||
? Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
: ''}
|
||||
</td>
|
||||
<div className="hidden sm:block">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-1.5 w-48">Konto</th>
|
||||
<th className="py-1.5">Beskrivning</th>
|
||||
<th className="py-1.5 w-28 text-right">Debet</th>
|
||||
<th className="py-1.5 w-28 text-right">Kredit</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{originalLines.map((line) => (
|
||||
<tr key={line.id} className="border-b last:border-0">
|
||||
<td className="py-1.5"><AccountNumber number={line.account_number} showName /></td>
|
||||
<td className="py-1.5 text-muted-foreground">{line.line_description || ''}</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{Number(line.debit_amount) > 0
|
||||
? Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{Number(line.credit_amount) > 0
|
||||
? Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
: ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sm:hidden space-y-1.5">
|
||||
{originalLines.map((line) => (
|
||||
<div key={line.id} className="flex items-center justify-between py-1.5 border-b last:border-0 text-sm">
|
||||
<div className="min-w-0">
|
||||
<AccountNumber number={line.account_number} showName />
|
||||
</div>
|
||||
<span className="font-mono text-xs shrink-0 ml-2">
|
||||
{Number(line.debit_amount) > 0
|
||||
? `D ${Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}`
|
||||
: `K ${Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
@@ -178,45 +194,49 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
|
||||
|
||||
<div className="space-y-2">
|
||||
{lines.map((line, index) => (
|
||||
<div key={index} className="grid grid-cols-[1fr_1fr_120px_120px_auto] gap-2 items-start">
|
||||
<AccountCombobox
|
||||
value={line.account_number}
|
||||
accounts={accounts}
|
||||
onChange={(v) => updateLine(index, 'account_number', v)}
|
||||
/>
|
||||
<div key={index} className="space-y-2 sm:space-y-0 sm:grid sm:grid-cols-[1fr_1fr_120px_120px_auto] sm:gap-2 sm:items-start border-b sm:border-0 pb-3 sm:pb-0 last:border-0">
|
||||
<div className="grid grid-cols-[1fr_auto] sm:contents gap-2">
|
||||
<AccountCombobox
|
||||
value={line.account_number}
|
||||
accounts={accounts}
|
||||
onChange={(v) => updateLine(index, 'account_number', v)}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 min-h-[44px] min-w-[44px] sm:order-last"
|
||||
onClick={() => removeLine(index)}
|
||||
disabled={lines.length <= 2}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={line.line_description}
|
||||
onChange={(e) => updateLine(index, 'line_description', e.target.value)}
|
||||
placeholder="Beskrivning"
|
||||
className="h-8"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
value={line.debit_amount}
|
||||
onChange={(e) => updateLine(index, 'debit_amount', e.target.value)}
|
||||
placeholder="Debet"
|
||||
className="h-8 text-right"
|
||||
min={0}
|
||||
step="0.01"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
value={line.credit_amount}
|
||||
onChange={(e) => updateLine(index, 'credit_amount', e.target.value)}
|
||||
placeholder="Kredit"
|
||||
className="h-8 text-right"
|
||||
min={0}
|
||||
step="0.01"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => removeLine(index)}
|
||||
disabled={lines.length <= 2}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="grid grid-cols-2 gap-2 sm:contents">
|
||||
<Input
|
||||
type="number"
|
||||
value={line.debit_amount}
|
||||
onChange={(e) => updateLine(index, 'debit_amount', e.target.value)}
|
||||
placeholder="Debet"
|
||||
className="h-8 text-right"
|
||||
min={0}
|
||||
step="0.01"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
value={line.credit_amount}
|
||||
onChange={(e) => updateLine(index, 'credit_amount', e.target.value)}
|
||||
placeholder="Kredit"
|
||||
className="h-8 text-right"
|
||||
min={0}
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ export function EditAccountDialog({ open, onOpenChange, account, onSaved }: Edit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Standard momskod</Label>
|
||||
<Input
|
||||
|
||||
@@ -185,7 +185,7 @@ export default function JournalEntryAttachments({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 shrink-0"
|
||||
className="h-6 w-6 p-0 shrink-0 min-h-[44px] min-w-[44px]"
|
||||
onClick={() => handleDownload(doc.id)}
|
||||
title="Ladda ner"
|
||||
>
|
||||
|
||||
@@ -230,7 +230,7 @@ export default function JournalEntryForm({
|
||||
|
||||
const formContent = (
|
||||
<div className="space-y-4">
|
||||
<div className={`grid gap-4 ${embedded && initialDate ? 'grid-cols-2' : 'grid-cols-3'}`}>
|
||||
<div className={`grid gap-4 grid-cols-1 ${embedded && initialDate ? 'sm:grid-cols-2' : 'sm:grid-cols-3'}`}>
|
||||
<div>
|
||||
<Label>Räkenskapsår</Label>
|
||||
<select
|
||||
@@ -265,8 +265,90 @@ export default function JournalEntryForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entry lines */}
|
||||
<div>
|
||||
{/* Entry lines — mobile cards */}
|
||||
<div className="sm:hidden space-y-3">
|
||||
{lines.map((line, index) => (
|
||||
<div key={index} className="rounded-lg border bg-card p-3 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1">
|
||||
<AccountCombobox
|
||||
value={line.account_number}
|
||||
accounts={accounts}
|
||||
onChange={(num) => updateLine(index, 'account_number', num)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeLine(index)}
|
||||
disabled={lines.length <= 2}
|
||||
className="h-8 w-8 p-0 min-h-[44px] min-w-[44px] shrink-0 -mr-1 -mt-1"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={line.line_description}
|
||||
onChange={(e) => updateLine(index, 'line_description', e.target.value)}
|
||||
placeholder="Radtext..."
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">Debet</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={line.debit_amount}
|
||||
onChange={(e) => updateLine(index, 'debit_amount', e.target.value)}
|
||||
placeholder="0,00"
|
||||
className="text-right"
|
||||
inputMode="decimal"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">Kredit</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={line.credit_amount}
|
||||
onChange={(e) => updateLine(index, 'credit_amount', e.target.value)}
|
||||
placeholder="0,00"
|
||||
className="text-right"
|
||||
inputMode="decimal"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Mobile totals */}
|
||||
<div className="flex justify-between items-center px-1 pt-2 font-semibold text-sm">
|
||||
<span>Summa</span>
|
||||
<div className="flex gap-4">
|
||||
<span className={isBalanced ? 'text-success' : 'text-destructive'}>
|
||||
D: {totalDebit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
<span className={isBalanced ? 'text-success' : 'text-destructive'}>
|
||||
K: {totalCredit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addLine}
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Lägg till rad
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Entry lines — desktop table */}
|
||||
<div className="hidden sm:block">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
@@ -302,6 +384,7 @@ export default function JournalEntryForm({
|
||||
onChange={(e) => updateLine(index, 'debit_amount', e.target.value)}
|
||||
placeholder="0,00"
|
||||
className="text-right h-8"
|
||||
inputMode="decimal"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
@@ -313,6 +396,7 @@ export default function JournalEntryForm({
|
||||
onChange={(e) => updateLine(index, 'credit_amount', e.target.value)}
|
||||
placeholder="0,00"
|
||||
className="text-right h-8"
|
||||
inputMode="decimal"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
@@ -323,9 +407,9 @@ export default function JournalEntryForm({
|
||||
size="sm"
|
||||
onClick={() => removeLine(index)}
|
||||
disabled={lines.length <= 2}
|
||||
className="h-8 w-8 p-0"
|
||||
className="h-8 w-8 p-0 min-h-[44px] min-w-[44px]"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -138,9 +138,10 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
<Card key={entry.id}>
|
||||
<button
|
||||
onClick={() => toggleExpand(entry.id)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors"
|
||||
className="w-full p-4 text-left hover:bg-muted/50 transition-colors min-h-[44px]"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
{/* Desktop: single row */}
|
||||
<div className="hidden sm:flex items-center gap-3 flex-1">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 shrink-0" />
|
||||
) : (
|
||||
@@ -153,7 +154,6 @@ 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" />
|
||||
@@ -167,6 +167,37 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{/* Mobile: two rows */}
|
||||
<div className="sm:hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 shrink-0" />
|
||||
)}
|
||||
<span className="font-mono text-sm text-muted-foreground">
|
||||
{entry.voucher_series}{entry.voucher_number}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{entry.entry_date}
|
||||
</span>
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
{attachmentCounts[entry.id] ? (
|
||||
<span className="flex items-center gap-0.5 text-muted-foreground" 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 title="Underlag saknas">
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 ml-6 text-sm truncate">{entry.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
@@ -174,59 +205,95 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
{lines.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">Inga kontorader hittades för denna verifikation.</p>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2 w-48">Konto</th>
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-28 text-right">Debet</th>
|
||||
<th className="py-2 w-28 text-right">Kredit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<>
|
||||
{/* Mobile: stacked cards */}
|
||||
<div className="sm:hidden space-y-2">
|
||||
{lines
|
||||
.sort((a, b) => a.sort_order - b.sort_order)
|
||||
.map((line) => (
|
||||
<tr key={line.id} className="border-b last:border-0">
|
||||
<td className="py-2"><AccountNumber number={line.account_number} showName /></td>
|
||||
<td className="py-2 text-muted-foreground">
|
||||
{line.line_description || ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{Number(line.debit_amount) > 0
|
||||
? Number(line.debit_amount).toLocaleString('sv-SE', {
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{Number(line.credit_amount) > 0
|
||||
? Number(line.credit_amount).toLocaleString('sv-SE', {
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
: ''}
|
||||
</td>
|
||||
</tr>
|
||||
<div key={line.id} className="flex items-center justify-between py-2 border-b last:border-0 gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm"><AccountNumber number={line.account_number} showName /></div>
|
||||
{line.line_description && (
|
||||
<p className="text-xs text-muted-foreground truncate">{line.line_description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right shrink-0 text-sm tabular-nums">
|
||||
{Number(line.debit_amount) > 0 && (
|
||||
<p>{Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} D</p>
|
||||
)}
|
||||
{Number(line.credit_amount) > 0 && (
|
||||
<p>{Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} K</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="font-semibold">
|
||||
<td colSpan={2} className="py-2">
|
||||
Summa
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{lines
|
||||
.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0)
|
||||
.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{lines
|
||||
.reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0)
|
||||
.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div className="flex justify-between font-semibold text-sm pt-1">
|
||||
<span>Summa</span>
|
||||
<div className="flex gap-3 tabular-nums">
|
||||
<span>D: {lines.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}</span>
|
||||
<span>K: {lines.reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: table */}
|
||||
<div className="hidden sm:block">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2 w-48">Konto</th>
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-28 text-right">Debet</th>
|
||||
<th className="py-2 w-28 text-right">Kredit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lines
|
||||
.sort((a, b) => a.sort_order - b.sort_order)
|
||||
.map((line) => (
|
||||
<tr key={line.id} className="border-b last:border-0">
|
||||
<td className="py-2"><AccountNumber number={line.account_number} showName /></td>
|
||||
<td className="py-2 text-muted-foreground">
|
||||
{line.line_description || ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{Number(line.debit_amount) > 0
|
||||
? Number(line.debit_amount).toLocaleString('sv-SE', {
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{Number(line.credit_amount) > 0
|
||||
? Number(line.credit_amount).toLocaleString('sv-SE', {
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
: ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="font-semibold">
|
||||
<td colSpan={2} className="py-2">
|
||||
Summa
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{lines
|
||||
.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0)
|
||||
.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{lines
|
||||
.reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0)
|
||||
.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<JournalEntryAttachments
|
||||
|
||||
@@ -82,46 +82,71 @@ export function JournalEntryReviewContent({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Debit/Credit table */}
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2 w-24">Konto</th>
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-28 text-right">Debet</th>
|
||||
<th className="py-2 w-28 text-right">Kredit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activeLines.map((line, index) => (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">
|
||||
<AccountNumber number={line.account_number} />
|
||||
</td>
|
||||
<td className="py-2 text-muted-foreground">
|
||||
{line.line_description || ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{parseFloat(line.debit_amount) > 0
|
||||
? formatAmount(parseFloat(line.debit_amount))
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{parseFloat(line.credit_amount) > 0
|
||||
? formatAmount(parseFloat(line.credit_amount))
|
||||
: ''}
|
||||
</td>
|
||||
{/* Debit/Credit — table on desktop, cards on mobile */}
|
||||
<div className="hidden sm:block">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2 w-24">Konto</th>
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-28 text-right">Debet</th>
|
||||
<th className="py-2 w-28 text-right">Kredit</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="font-semibold border-t-2">
|
||||
<td colSpan={2} className="py-2">Summa</td>
|
||||
<td className="py-2 text-right text-success">{formatAmount(totalDebit)}</td>
|
||||
<td className="py-2 text-right text-success">{formatAmount(totalCredit)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activeLines.map((line, index) => (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">
|
||||
<AccountNumber number={line.account_number} />
|
||||
</td>
|
||||
<td className="py-2 text-muted-foreground">
|
||||
{line.line_description || ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{parseFloat(line.debit_amount) > 0
|
||||
? formatAmount(parseFloat(line.debit_amount))
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{parseFloat(line.credit_amount) > 0
|
||||
? formatAmount(parseFloat(line.credit_amount))
|
||||
: ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="font-semibold border-t-2">
|
||||
<td colSpan={2} className="py-2">Summa</td>
|
||||
<td className="py-2 text-right text-success">{formatAmount(totalDebit)}</td>
|
||||
<td className="py-2 text-right text-success">{formatAmount(totalCredit)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sm:hidden space-y-1.5">
|
||||
{activeLines.map((line, index) => (
|
||||
<div key={index} className="flex items-center justify-between py-2 border-b last:border-0 text-sm">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<AccountNumber number={line.account_number} />
|
||||
{line.line_description && (
|
||||
<span className="text-muted-foreground text-xs truncate">{line.line_description}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-mono text-sm shrink-0 ml-2">
|
||||
{parseFloat(line.debit_amount) > 0
|
||||
? `D ${formatAmount(parseFloat(line.debit_amount))}`
|
||||
: `K ${formatAmount(parseFloat(line.credit_amount))}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-between pt-2 border-t-2 font-semibold text-sm">
|
||||
<span>Summa</span>
|
||||
<span className="text-success">D {formatAmount(totalDebit)} / K {formatAmount(totalCredit)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function CustomerForm({
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">E-post</Label>
|
||||
<Input
|
||||
@@ -202,7 +202,7 @@ export default function CustomerForm({
|
||||
{...register('address_line1')}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="postal_code">Postnummer</Label>
|
||||
<Input
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
@@ -72,11 +72,18 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
const router = useRouter()
|
||||
const supabase = createClient()
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const [isClosing, setIsClosing] = useState(false)
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
// Auto-expand Övrigt when the user is on one of its pages, or when manually toggled
|
||||
const isOnOvrigtPage = ['/help', '/settings'].some(p => pathname.startsWith(p))
|
||||
const [manualOvrigtExpanded, setManualOvrigtExpanded] = useState(false)
|
||||
const isOvrigtExpanded = isOnOvrigtPage || manualOvrigtExpanded
|
||||
const openMobileMenu = () => {
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current)
|
||||
closeTimerRef.current = null
|
||||
}
|
||||
setIsClosing(false)
|
||||
setIsMobileMenuOpen(true)
|
||||
}
|
||||
|
||||
@@ -92,7 +99,14 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
const closeMobileMenu = () => setIsMobileMenuOpen(false)
|
||||
const closeMobileMenu = () => {
|
||||
setIsClosing(true)
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
setIsMobileMenuOpen(false)
|
||||
setIsClosing(false)
|
||||
closeTimerRef.current = null
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// Filter nav items by entity type and hidden flag
|
||||
const filteredItems = navItems.filter(item =>
|
||||
@@ -251,7 +265,7 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
</aside>
|
||||
|
||||
{/* Mobile bottom navigation */}
|
||||
<nav className="md:hidden fixed bottom-0 left-0 right-0 z-50 bg-card/98 backdrop-blur-sm border-t border-border/40" aria-label="Mobilnavigation">
|
||||
<nav className="md:hidden fixed bottom-0 left-0 right-0 z-50 bg-card/98 backdrop-blur-sm border-t border-border/40" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }} aria-label="Mobilnavigation">
|
||||
<div className="flex items-center justify-around h-16 px-2">
|
||||
{mobileNavItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
@@ -306,13 +320,21 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="md:hidden fixed inset-0 bg-background/80 backdrop-blur-sm z-50"
|
||||
className={cn(
|
||||
"md:hidden fixed inset-0 bg-background/80 backdrop-blur-sm z-50",
|
||||
isClosing ? "animate-out fade-out duration-200" : "animate-in fade-in duration-300"
|
||||
)}
|
||||
onClick={closeMobileMenu}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{/* Drawer */}
|
||||
<div
|
||||
className="md:hidden fixed right-0 top-0 bottom-0 w-72 bg-card border-l border-border/40 z-50 overflow-y-auto animate-in slide-in-from-right duration-300"
|
||||
className={cn(
|
||||
"md:hidden fixed right-0 top-0 bottom-0 w-72 bg-card border-l border-border/40 z-50 overflow-y-auto",
|
||||
isClosing
|
||||
? "animate-out slide-out-to-right duration-200"
|
||||
: "animate-in slide-in-from-right duration-300"
|
||||
)}
|
||||
role="dialog"
|
||||
aria-label="Navigeringsmeny"
|
||||
>
|
||||
|
||||
@@ -148,7 +148,7 @@ export function DeadlineForm({
|
||||
</div>
|
||||
|
||||
{/* Date and Time */}
|
||||
<div className="grid gap-4 grid-cols-2">
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="due_date">Datum *</Label>
|
||||
<Input
|
||||
@@ -171,7 +171,7 @@ export function DeadlineForm({
|
||||
</div>
|
||||
|
||||
{/* Type and Priority */}
|
||||
<div className="grid gap-4 grid-cols-2">
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Typ *</Label>
|
||||
<Select
|
||||
@@ -215,14 +215,14 @@ export function DeadlineForm({
|
||||
<div className="space-y-2">
|
||||
<Label>Kund (valfritt)</Label>
|
||||
<Select
|
||||
value={formData.customer_id}
|
||||
onValueChange={(v) => updateField('customer_id', v)}
|
||||
value={formData.customer_id || 'none'}
|
||||
onValueChange={(v) => updateField('customer_id', v === 'none' ? '' : v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Välj kund..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">Ingen kund</SelectItem>
|
||||
<SelectItem value="none">Ingen kund</SelectItem>
|
||||
{customers.map((customer) => (
|
||||
<SelectItem key={customer.id} value={customer.id}>
|
||||
{customer.name}
|
||||
|
||||
@@ -150,7 +150,7 @@ export default function InboxDetailDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-2xl max-h-[95dvh] sm:max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<DialogTitle>Granska faktura</DialogTitle>
|
||||
|
||||
@@ -229,7 +229,7 @@ export default function MockDataImportDialog<T>({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-2xl max-h-[95dvh] sm:max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{step === 'upload' && 'Importera testdata'}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function BankFileColumnMappingStep({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Delimiter and decimal settings */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Avgränsare</Label>
|
||||
<Select value={delimiter} onValueChange={setDelimiter}>
|
||||
@@ -110,7 +110,7 @@ export default function BankFileColumnMappingStep({
|
||||
{/* Required column mappings */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-3">Obligatoriska kolumner</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Datum *</Label>
|
||||
<Select
|
||||
@@ -173,7 +173,7 @@ export default function BankFileColumnMappingStep({
|
||||
{/* Optional column mappings */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-3">Valfria kolumner</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Referens/OCR</Label>
|
||||
<Select
|
||||
|
||||
@@ -61,18 +61,18 @@ export function InvoiceReviewContent({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Customer info */}
|
||||
<div className="bg-muted rounded-lg p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-base">{customer.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{customer.email}</p>
|
||||
<div className="bg-muted rounded-lg p-3 sm:p-4 flex flex-col sm:flex-row sm:items-center gap-2 sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-base truncate">{customer.name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{customer.email}</p>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
<Badge variant="outline" className="self-start sm:self-auto shrink-0">
|
||||
{customerTypeLabel[customer.customer_type] || customer.customer_type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Fakturadatum</span>
|
||||
<p className="font-medium">{invoiceDate}</p>
|
||||
@@ -83,35 +83,51 @@ export function InvoiceReviewContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Line items table */}
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-16 text-right">Antal</th>
|
||||
<th className="py-2 w-16 text-center">Enhet</th>
|
||||
<th className="py-2 w-24 text-right">À-pris</th>
|
||||
{showVatColumn && <th className="py-2 w-16 text-right">Moms</th>}
|
||||
<th className="py-2 w-28 text-right">Belopp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">{item.description}</td>
|
||||
<td className="py-2 text-right">{item.quantity}</td>
|
||||
<td className="py-2 text-center">{item.unit}</td>
|
||||
<td className="py-2 text-right">{formatCurrency(item.unit_price, currency)}</td>
|
||||
{showVatColumn && (
|
||||
<td className="py-2 text-right">{item.vat_rate ?? 0}%</td>
|
||||
)}
|
||||
<td className="py-2 text-right">
|
||||
{formatCurrency(item.quantity * item.unit_price, currency)}
|
||||
</td>
|
||||
{/* Line items — table on desktop, cards on mobile */}
|
||||
<div className="hidden sm:block">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-16 text-right">Antal</th>
|
||||
<th className="py-2 w-16 text-center">Enhet</th>
|
||||
<th className="py-2 w-24 text-right">À-pris</th>
|
||||
{showVatColumn && <th className="py-2 w-16 text-right">Moms</th>}
|
||||
<th className="py-2 w-28 text-right">Belopp</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">{item.description}</td>
|
||||
<td className="py-2 text-right">{item.quantity}</td>
|
||||
<td className="py-2 text-center">{item.unit}</td>
|
||||
<td className="py-2 text-right">{formatCurrency(item.unit_price, currency)}</td>
|
||||
{showVatColumn && (
|
||||
<td className="py-2 text-right">{item.vat_rate ?? 0}%</td>
|
||||
)}
|
||||
<td className="py-2 text-right">
|
||||
{formatCurrency(item.quantity * item.unit_price, currency)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sm:hidden space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className="border rounded-lg p-3 text-sm space-y-1.5">
|
||||
<p className="font-medium">{item.description}</p>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span>{item.quantity} {item.unit} × {formatCurrency(item.unit_price, currency)}</span>
|
||||
{showVatColumn && <span className="text-xs">({item.vat_rate ?? 0}% moms)</span>}
|
||||
</div>
|
||||
<p className="text-right font-medium">
|
||||
{formatCurrency(item.quantity * item.unit_price, currency)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2 text-sm">
|
||||
@@ -135,7 +151,7 @@ export function InvoiceReviewContent({
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="flex justify-between font-bold text-2xl">
|
||||
<div className="flex justify-between font-bold text-xl sm:text-2xl">
|
||||
<span>Totalt</span>
|
||||
<span>{formatCurrency(total, currency)}</span>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function Step2CompanyDetails({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="postal_code">Postnummer</Label>
|
||||
<Input
|
||||
|
||||
@@ -328,7 +328,7 @@ export default function Step3TaxRegistration({
|
||||
name="is_first_fiscal_year"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => field.onChange(true)}
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function Step6ConnectBank({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<InfoTooltip
|
||||
content={
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function SupplierForm({
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">E-post</Label>
|
||||
<Input
|
||||
@@ -145,7 +145,7 @@ export default function SupplierForm({
|
||||
{/* Business info */}
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<h3 className="font-medium">Företagsuppgifter</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="org_number">Organisationsnummer</Label>
|
||||
<Input
|
||||
@@ -176,7 +176,7 @@ export default function SupplierForm({
|
||||
{...register('address_line1')}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="postal_code">Postnummer</Label>
|
||||
<Input id="postal_code" placeholder="123 45" {...register('postal_code')} />
|
||||
@@ -195,7 +195,7 @@ export default function SupplierForm({
|
||||
{/* Payment details */}
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<h3 className="font-medium">Betalningsuppgifter</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bankgiro">Bankgiro</Label>
|
||||
<Input id="bankgiro" placeholder="XXX-XXXX" {...register('bankgiro')} />
|
||||
@@ -205,7 +205,7 @@ export default function SupplierForm({
|
||||
<Input id="plusgiro" placeholder="XXXXXXX-X" {...register('plusgiro')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="iban">IBAN</Label>
|
||||
<Input id="iban" placeholder="SE00 0000 0000 0000 0000 0000" {...register('iban')} />
|
||||
@@ -220,7 +220,7 @@ export default function SupplierForm({
|
||||
{/* Defaults */}
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<h3 className="font-medium">Standardvärden</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="default_expense_account">Kostnadskonto</Label>
|
||||
<Input
|
||||
|
||||
@@ -139,12 +139,12 @@ export function SupplierInvoiceReviewContent({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Supplier info */}
|
||||
<div className="bg-muted rounded-lg p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-base">{supplier.name}</p>
|
||||
<div className="bg-muted rounded-lg p-3 sm:p-4 flex flex-col sm:flex-row sm:items-center gap-2 sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-base truncate">{supplier.name}</p>
|
||||
<p className="text-sm text-muted-foreground">Fakturanr: {invoiceNumber}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2 shrink-0">
|
||||
{reverseCharge && (
|
||||
<Badge variant="outline" className="border-orange-300 text-orange-700 dark:text-orange-400">
|
||||
Omvänd skattskyldighet
|
||||
@@ -160,7 +160,7 @@ export function SupplierInvoiceReviewContent({
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Fakturadatum</span>
|
||||
<p className="font-medium">{invoiceDate}</p>
|
||||
@@ -177,34 +177,53 @@ export function SupplierInvoiceReviewContent({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Line items table */}
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2 w-20">Konto</th>
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-28 text-right">Belopp</th>
|
||||
<th className="py-2 w-16 text-right">Moms%</th>
|
||||
<th className="py-2 w-24 text-right">Moms</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
const vatAmount = Math.round(item.amount * item.vat_rate * 100) / 100
|
||||
return (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">
|
||||
<AccountNumber number={item.account_number} size="sm" />
|
||||
</td>
|
||||
<td className="py-2">{item.description}</td>
|
||||
<td className="py-2 text-right font-mono">{formatAmount(item.amount)}</td>
|
||||
<td className="py-2 text-right">{Math.round(item.vat_rate * 100)}%</td>
|
||||
<td className="py-2 text-right font-mono">{formatAmount(vatAmount)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Line items — table on desktop, cards on mobile */}
|
||||
<div className="hidden sm:block">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="py-2 w-20">Konto</th>
|
||||
<th className="py-2">Beskrivning</th>
|
||||
<th className="py-2 w-28 text-right">Belopp</th>
|
||||
<th className="py-2 w-16 text-right">Moms%</th>
|
||||
<th className="py-2 w-24 text-right">Moms</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
const vatAmount = Math.round(item.amount * item.vat_rate * 100) / 100
|
||||
return (
|
||||
<tr key={index} className="border-b last:border-0">
|
||||
<td className="py-2">
|
||||
<AccountNumber number={item.account_number} size="sm" />
|
||||
</td>
|
||||
<td className="py-2">{item.description}</td>
|
||||
<td className="py-2 text-right font-mono">{formatAmount(item.amount)}</td>
|
||||
<td className="py-2 text-right">{Math.round(item.vat_rate * 100)}%</td>
|
||||
<td className="py-2 text-right font-mono">{formatAmount(vatAmount)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sm:hidden space-y-2">
|
||||
{items.map((item, index) => {
|
||||
const vatAmount = Math.round(item.amount * item.vat_rate * 100) / 100
|
||||
return (
|
||||
<div key={index} className="border rounded-lg p-3 text-sm space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-medium">{item.description}</p>
|
||||
<AccountNumber number={item.account_number} size="sm" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span>{formatAmount(item.amount)} kr</span>
|
||||
<span className="text-xs">{Math.round(item.vat_rate * 100)}% moms ({formatAmount(vatAmount)})</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2 text-sm">
|
||||
@@ -217,7 +236,7 @@ export function SupplierInvoiceReviewContent({
|
||||
<span>{formatCurrency(totalVat, currency)}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between font-bold text-2xl">
|
||||
<div className="flex justify-between font-bold text-xl sm:text-2xl">
|
||||
<span>Totalt</span>
|
||||
<span>{formatCurrency(total, currency)}</span>
|
||||
</div>
|
||||
@@ -230,43 +249,66 @@ export function SupplierInvoiceReviewContent({
|
||||
</div>
|
||||
|
||||
{/* Verifikation preview */}
|
||||
<div className="bg-muted/50 border rounded-lg p-4 space-y-2">
|
||||
<div className="bg-muted/50 border rounded-lg p-3 sm:p-4 space-y-2">
|
||||
<p className="text-sm font-semibold text-muted-foreground">Verifikation som bokförs</p>
|
||||
<table className="w-full text-sm font-mono">
|
||||
<thead>
|
||||
<tr className="text-left text-muted-foreground text-xs">
|
||||
<th className="pb-1 w-16">Konto</th>
|
||||
<th className="pb-1">Beskrivning</th>
|
||||
<th className="pb-1 w-24 text-right">Debet</th>
|
||||
<th className="pb-1 w-24 text-right">Kredit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{journalLines.map((line, index) => (
|
||||
<tr key={index} className="border-b border-dashed border-muted-foreground/20 last:border-0">
|
||||
<td className="py-1">
|
||||
<AccountNumber number={line.account_number} size="sm" />
|
||||
</td>
|
||||
<td className="py-1 text-xs">
|
||||
{ACCOUNT_LABELS[line.account_number] || line.description}
|
||||
</td>
|
||||
<td className="py-1 text-right">
|
||||
{line.debit > 0 ? formatAmount(line.debit) : ''}
|
||||
</td>
|
||||
<td className="py-1 text-right">
|
||||
{line.credit > 0 ? formatAmount(line.credit) : ''}
|
||||
</td>
|
||||
<div className="hidden sm:block">
|
||||
<table className="w-full text-sm font-mono">
|
||||
<thead>
|
||||
<tr className="text-left text-muted-foreground text-xs">
|
||||
<th className="pb-1 w-16">Konto</th>
|
||||
<th className="pb-1">Beskrivning</th>
|
||||
<th className="pb-1 w-24 text-right">Debet</th>
|
||||
<th className="pb-1 w-24 text-right">Kredit</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t font-semibold">
|
||||
<td className="pt-1" colSpan={2}>SUMMA</td>
|
||||
<td className="pt-1 text-right">{formatAmount(totalDebit)}</td>
|
||||
<td className="pt-1 text-right">{formatAmount(totalCredit)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{journalLines.map((line, index) => (
|
||||
<tr key={index} className="border-b border-dashed border-muted-foreground/20 last:border-0">
|
||||
<td className="py-1">
|
||||
<AccountNumber number={line.account_number} size="sm" />
|
||||
</td>
|
||||
<td className="py-1 text-xs">
|
||||
{ACCOUNT_LABELS[line.account_number] || line.description}
|
||||
</td>
|
||||
<td className="py-1 text-right">
|
||||
{line.debit > 0 ? formatAmount(line.debit) : ''}
|
||||
</td>
|
||||
<td className="py-1 text-right">
|
||||
{line.credit > 0 ? formatAmount(line.credit) : ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t font-semibold">
|
||||
<td className="pt-1" colSpan={2}>SUMMA</td>
|
||||
<td className="pt-1 text-right">{formatAmount(totalDebit)}</td>
|
||||
<td className="pt-1 text-right">{formatAmount(totalCredit)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sm:hidden space-y-1.5 text-sm">
|
||||
{journalLines.map((line, index) => (
|
||||
<div key={index} className="flex items-center justify-between py-1 border-b border-dashed border-muted-foreground/20 last:border-0">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<AccountNumber number={line.account_number} size="sm" />
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{ACCOUNT_LABELS[line.account_number] || line.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-mono text-xs shrink-0 ml-2">
|
||||
{line.debit > 0 ? `D ${formatAmount(line.debit)}` : `K ${formatAmount(line.credit)}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-between pt-1 border-t font-semibold text-xs font-mono">
|
||||
<span>SUMMA</span>
|
||||
<span>D {formatAmount(totalDebit)} / K {formatAmount(totalCredit)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment reference */}
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function TransactionBookingDialog({
|
||||
}
|
||||
onOpenChange(o)
|
||||
}}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-2xl max-h-[95dvh] sm:max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Bokför transaktion</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function TransactionForm({ onSubmit, isLoading }: TransactionForm
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="date">Datum *</Label>
|
||||
<Input id="date" type="date" {...register('date')} />
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function TransactionHistoryList({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="h-7 text-xs"
|
||||
className="h-7 min-h-[44px] text-xs"
|
||||
onClick={() => onOpenCategoryDialog(transaction)}
|
||||
>
|
||||
Bokför
|
||||
|
||||
@@ -37,24 +37,24 @@ export function ConfirmationDialog({
|
||||
}: ConfirmationDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl border-t-2 border-primary p-0 gap-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-4">
|
||||
<DialogContent className="sm:max-w-2xl border-t-2 border-primary p-0 gap-0 max-h-[95dvh] sm:max-h-[90dvh] flex flex-col">
|
||||
<DialogHeader className="px-4 sm:px-6 pt-4 sm:pt-6 pb-3 sm:pb-4 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 shrink-0">
|
||||
<ClipboardCheck className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle className="text-xl">{title}</DialogTitle>
|
||||
<DialogTitle className="text-lg sm:text-xl">{title}</DialogTitle>
|
||||
<DialogDescription>Granska uppgifterna innan du bekräftar</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-y-auto max-h-[70vh] px-6 pb-4">
|
||||
<div className="overflow-y-auto flex-1 min-h-0 px-4 sm:px-6 pb-4">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="border-t px-6 py-4 space-y-4">
|
||||
<div className="border-t px-4 sm:px-6 py-3 sm:py-4 space-y-3 sm:space-y-4 shrink-0">
|
||||
{warningText && (
|
||||
<div className="flex items-start gap-2 rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 p-3">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
|
||||
|
||||
@@ -8,14 +8,14 @@ interface PageHeaderProps {
|
||||
|
||||
export function PageHeader({ title, description, action }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-medium tracking-tight">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-muted-foreground mt-1 text-balance">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{action && <div>{action}</div>}
|
||||
{action && <div className="w-full sm:w-auto [&>*]:w-full [&>*]:sm:w-auto">{action}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const TabsList = React.forwardRef<
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground max-w-full overflow-x-auto scrollbar-hide",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user