Mobile view fixes (#188)

* fix: make new user checklist mobile responsive

Adjust spacing, padding, typography, and indentation for small screens
so the onboarding welcome screen works well on mobile devices.

* fix: make expenses and bookkeeping verification views mobile responsive

- Expenses/new: stack form grids on mobile, replace line items table with
  card layout, full-width action buttons
- Expenses/[id]: stack header and actions, card layout for line items and
  payments, single-column info grid on mobile
- JournalEntryList: unify verification lines into card layout for all
  screen sizes, hide redundant line descriptions that duplicate account
  name or entry description, stack filter bar and action buttons on mobile

* fix: remove stray )); in provider_consents migration

Syntax error on line 35 caused MIGRATIONS_FAILED on the Supabase staging branch.

* fix: enhance mobile responsiveness for invoices page and update dialog styles
This commit is contained in:
Mattsson
2026-04-07 15:53:30 +02:00
committed by GitHub
parent a25e75be25
commit d5410a518f
11 changed files with 957 additions and 273 deletions
+111 -60
View File
@@ -170,13 +170,13 @@ export default function ExpenseDetailPage() {
return (
<div className="space-y-6 max-w-4xl">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => router.push('/expenses')}>
<div className="space-y-4">
<div className="flex items-start gap-4">
<Button variant="ghost" size="icon" className="shrink-0 mt-1" onClick={() => router.push('/expenses')}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<div className="flex items-center gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
Utgift #{invoice.arrival_number}
</h1>
@@ -184,39 +184,40 @@ export default function ExpenseDetailPage() {
{status.label}
</Badge>
</div>
<p className="text-muted-foreground">
<p className="text-muted-foreground truncate">
{invoice.supplier?.name} · Faktura {invoice.supplier_invoice_number}
</p>
</div>
</div>
{/* Context-aware actions */}
<div className="flex gap-2">
<div className="flex flex-col sm:flex-row gap-2 sm:justify-end">
{invoice.status === 'registered' && (
<>
<Button onClick={handleApprove} disabled={isProcessing}>
<Button className="w-full sm:w-auto" onClick={handleApprove} disabled={isProcessing}>
<CheckCircle className="mr-2 h-4 w-4" />
Godkänn
</Button>
<Button variant="destructive" size="icon" onClick={handleDelete} disabled={isProcessing}>
<Trash2 className="h-4 w-4" />
<Button variant="destructive" className="w-full sm:w-auto" onClick={handleDelete} disabled={isProcessing}>
<Trash2 className="mr-2 h-4 w-4 sm:mr-0" />
<span className="sm:hidden">Ta bort</span>
</Button>
</>
)}
{['approved', 'overdue'].includes(invoice.status) && (
<>
<Button onClick={() => setIsPayDialogOpen(true)} disabled={isProcessing}>
<Button className="w-full sm:w-auto" onClick={() => setIsPayDialogOpen(true)} disabled={isProcessing}>
<CreditCard className="mr-2 h-4 w-4" />
Markera betald
</Button>
<Button variant="outline" onClick={handleCredit} disabled={isProcessing}>
<Button variant="outline" className="w-full sm:w-auto" onClick={handleCredit} disabled={isProcessing}>
<FileText className="mr-2 h-4 w-4" />
Kreditfaktura
</Button>
</>
)}
{invoice.status === 'partially_paid' && (
<Button onClick={() => setIsPayDialogOpen(true)} disabled={isProcessing}>
<Button className="w-full sm:w-auto" onClick={() => setIsPayDialogOpen(true)} disabled={isProcessing}>
<CreditCard className="mr-2 h-4 w-4" />
Registrera betalning
</Button>
@@ -231,7 +232,7 @@ export default function ExpenseDetailPage() {
</CardHeader>
<CardContent className="space-y-6">
{/* Info grid */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Leverantör</span>
<p className="font-medium">
@@ -282,28 +283,51 @@ export default function ExpenseDetailPage() {
{/* Line items */}
{items.length > 0 && (
<div className="border-t pt-4">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2">Beskrivning</th>
<th className="pb-2 w-20">Konto</th>
<th className="pb-2 w-16 text-right">Moms%</th>
<th className="pb-2 w-28 text-right">Belopp</th>
<th className="pb-2 w-24 text-right">Moms</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id} className="border-b last:border-0">
<td className="py-2">{item.description}</td>
<td className="py-2"><AccountNumber number={item.account_number} /></td>
<td className="py-2 text-right">{Math.round(item.vat_rate * 100)}%</td>
<td className="py-2 text-right font-mono">{formatAmount(item.line_total)}</td>
<td className="py-2 text-right font-mono">{formatAmount(item.vat_amount)}</td>
{/* 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="pb-2">Beskrivning</th>
<th className="pb-2 w-20">Konto</th>
<th className="pb-2 w-16 text-right">Moms%</th>
<th className="pb-2 w-28 text-right">Belopp</th>
<th className="pb-2 w-24 text-right">Moms</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id} className="border-b last:border-0">
<td className="py-2">{item.description}</td>
<td className="py-2"><AccountNumber number={item.account_number} /></td>
<td className="py-2 text-right">{Math.round(item.vat_rate * 100)}%</td>
<td className="py-2 text-right font-mono">{formatAmount(item.line_total)}</td>
<td className="py-2 text-right font-mono">{formatAmount(item.vat_amount)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: stacked cards */}
<div className="sm:hidden space-y-3">
{items.map((item) => (
<div key={item.id} className="border rounded-lg p-3 space-y-2 text-sm">
<div className="flex justify-between items-start gap-2">
<span className="font-medium">{item.description || 'Ingen beskrivning'}</span>
<AccountNumber number={item.account_number} />
</div>
<div className="flex justify-between text-muted-foreground">
<span>Belopp</span>
<span className="font-mono">{formatAmount(item.line_total)}</span>
</div>
<div className="flex justify-between text-muted-foreground">
<span>Moms ({Math.round(item.vat_rate * 100)}%)</span>
<span className="font-mono">{formatAmount(item.vat_amount)}</span>
</div>
</div>
))}
</div>
{/* Amounts summary */}
<div className="mt-4 pt-4 border-t space-y-1 text-sm">
@@ -351,32 +375,59 @@ export default function ExpenseDetailPage() {
{payments.length > 0 && (
<div>
<p className="text-sm font-medium mb-2">Betalningshistorik</p>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2">Datum</th>
<th className="pb-2 text-right">Belopp</th>
<th className="pb-2">Verifikation</th>
<th className="pb-2">Anteckning</th>
</tr>
</thead>
<tbody>
{payments.map((p) => (
<tr key={p.id} className="border-b last:border-0">
<td className="py-2">{p.payment_date}</td>
<td className="py-2 text-right font-mono">{formatAmount(p.amount)} {p.currency}</td>
<td className="py-2">
{p.journal_entry_id ? (
<Link href={`/bookkeeping/${p.journal_entry_id}`} className="text-primary hover:underline font-mono text-xs">
{p.journal_entry_id.substring(0, 8)}...
</Link>
) : '-'}
</td>
<td className="py-2 text-muted-foreground">{p.notes || '-'}</td>
{/* 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="pb-2">Datum</th>
<th className="pb-2 text-right">Belopp</th>
<th className="pb-2">Verifikation</th>
<th className="pb-2">Anteckning</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{payments.map((p) => (
<tr key={p.id} className="border-b last:border-0">
<td className="py-2">{p.payment_date}</td>
<td className="py-2 text-right font-mono">{formatAmount(p.amount)} {p.currency}</td>
<td className="py-2">
{p.journal_entry_id ? (
<Link href={`/bookkeeping/${p.journal_entry_id}`} className="text-primary hover:underline font-mono text-xs">
{p.journal_entry_id.substring(0, 8)}...
</Link>
) : '-'}
</td>
<td className="py-2 text-muted-foreground">{p.notes || '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: stacked cards */}
<div className="sm:hidden space-y-3">
{payments.map((p) => (
<div key={p.id} className="border rounded-lg p-3 space-y-1 text-sm">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{p.payment_date}</span>
<span className="font-mono font-medium">{formatAmount(p.amount)} {p.currency}</span>
</div>
{p.journal_entry_id && (
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Verifikation</span>
<Link href={`/bookkeeping/${p.journal_entry_id}`} className="text-primary hover:underline font-mono text-xs">
{p.journal_entry_id.substring(0, 8)}...
</Link>
</div>
)}
{p.notes && (
<p className="text-muted-foreground text-xs pt-1">{p.notes}</p>
)}
</div>
))}
</div>
</div>
)}
+154 -69
View File
@@ -360,7 +360,7 @@ export default function NewExpensePage() {
<CardTitle className="text-lg">Faktura</CardTitle>
</CardHeader>
<CardContent 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>Leverantör *</Label>
<Controller
@@ -400,7 +400,7 @@ export default function NewExpensePage() {
/>
</div>
</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>Fakturadatum *</Label>
<Input type="date" {...register('invoice_date')} />
@@ -422,12 +422,13 @@ export default function NewExpensePage() {
{/* Section 2: Kontering */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardHeader className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<CardTitle className="text-lg">Kontering</CardTitle>
<Button
type="button"
variant="outline"
size="sm"
className="w-full sm:w-auto"
onClick={() =>
append({ description: '', amount: 0, account_number: '', vat_rate: 0.25 })
}
@@ -437,48 +438,143 @@ export default function NewExpensePage() {
</Button>
</CardHeader>
<CardContent>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 w-28">Konto</th>
<th className="pb-2">Beskrivning</th>
<th className="pb-2 w-32">Belopp (exkl.)</th>
<th className="pb-2 w-24">Momssats</th>
<th className="pb-2 w-24 text-right">Moms</th>
<th className="pb-2 w-8"></th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.id} className="border-b last:border-0 align-top">
<td className="py-2 pr-2">
<Controller
name={`items.${index}.account_number`}
control={control}
render={({ field: f }) => (
<AccountCombobox
value={f.value}
accounts={accounts}
onChange={(val) => handleAccountChange(index, val)}
/>
{/* Desktop: table layout */}
<div className="hidden sm:block">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 w-28">Konto</th>
<th className="pb-2">Beskrivning</th>
<th className="pb-2 w-32">Belopp (exkl.)</th>
<th className="pb-2 w-24">Momssats</th>
<th className="pb-2 w-24 text-right">Moms</th>
<th className="pb-2 w-8"></th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.id} className="border-b last:border-0 align-top">
<td className="py-2 pr-2">
<Controller
name={`items.${index}.account_number`}
control={control}
render={({ field: f }) => (
<AccountCombobox
value={f.value}
accounts={accounts}
onChange={(val) => handleAccountChange(index, val)}
/>
)}
/>
</td>
<td className="py-2 pr-2">
<Input
placeholder="Beskrivning"
{...register(`items.${index}.description`)}
/>
</td>
<td className="py-2 pr-2">
<Input
type="number"
step="0.01"
placeholder="0,00"
{...register(`items.${index}.amount`, { valueAsNumber: true })}
/>
</td>
<td className="py-2 pr-2">
<Controller
name={`items.${index}.vat_rate`}
control={control}
render={({ field: f }) => (
<Select
value={String(f.value)}
onValueChange={(v) => f.onChange(parseFloat(v))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0.25">25%</SelectItem>
<SelectItem value="0.12">12%</SelectItem>
<SelectItem value="0.06">6%</SelectItem>
<SelectItem value="0">0%</SelectItem>
</SelectContent>
</Select>
)}
/>
</td>
<td className="py-2 pr-2 text-right font-mono pt-4">
{formatAmount(itemTotals[index]?.vatAmount || 0)}
</td>
<td className="py-2 pt-3">
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
)}
/>
</td>
<td className="py-2 pr-2">
<Input
placeholder="Beskrivning"
{...register(`items.${index}.description`)}
/>
</td>
<td className="py-2 pr-2">
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: stacked card layout */}
<div className="sm:hidden space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="border rounded-lg p-3 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Rad {index + 1}</span>
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
)}
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Konto</Label>
<Controller
name={`items.${index}.account_number`}
control={control}
render={({ field: f }) => (
<AccountCombobox
value={f.value}
accounts={accounts}
onChange={(val) => handleAccountChange(index, val)}
/>
)}
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Beskrivning</Label>
<Input
placeholder="Beskrivning"
{...register(`items.${index}.description`)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Belopp (exkl.)</Label>
<Input
type="number"
step="0.01"
placeholder="0,00"
{...register(`items.${index}.amount`, { valueAsNumber: true })}
/>
</td>
<td className="py-2 pr-2">
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Momssats</Label>
<Controller
name={`items.${index}.vat_rate`}
control={control}
@@ -499,40 +595,29 @@ export default function NewExpensePage() {
</Select>
)}
/>
</td>
<td className="py-2 pr-2 text-right font-mono pt-4">
{formatAmount(itemTotals[index]?.vatAmount || 0)}
</td>
<td className="py-2 pt-3">
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="flex justify-between items-center pt-1 border-t">
<span className="text-xs text-muted-foreground">Moms</span>
<span className="font-mono text-sm">{formatAmount(itemTotals[index]?.vatAmount || 0)} kr</span>
</div>
</div>
))}
</div>
{/* Totals */}
<div className="mt-4 pt-4 border-t space-y-2 text-right">
<div className="flex justify-end gap-8">
<div className="mt-4 pt-4 border-t space-y-2">
<div className="flex justify-between sm:justify-end sm:gap-8">
<span className="text-muted-foreground">Netto (exkl. moms)</span>
<span className="font-mono w-32">{formatAmount(subtotal)} kr</span>
<span className="font-mono sm:w-32 text-right">{formatAmount(subtotal)} kr</span>
</div>
<div className="flex justify-end gap-8">
<div className="flex justify-between sm:justify-end sm:gap-8">
<span className="text-muted-foreground">Moms</span>
<span className="font-mono w-32">{formatAmount(totalVat)} kr</span>
<span className="font-mono sm:w-32 text-right">{formatAmount(totalVat)} kr</span>
</div>
<div className="flex justify-end gap-8 font-bold text-lg">
<div className="flex justify-between sm:justify-end sm:gap-8 font-bold text-lg">
<span>Totalt</span>
<span className="font-mono w-32">{formatAmount(total)} kr</span>
<span className="font-mono sm:w-32 text-right">{formatAmount(total)} kr</span>
</div>
</div>
</CardContent>
@@ -551,7 +636,7 @@ export default function NewExpensePage() {
</CardHeader>
{advancedOpen && (
<CardContent className="space-y-4 pt-0">
<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>Valuta</Label>
<Controller
@@ -615,11 +700,11 @@ export default function NewExpensePage() {
</Card>
{/* Submit */}
<div className="flex justify-end gap-4">
<Button type="button" variant="outline" onClick={() => router.push('/expenses')}>
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-3 sm:gap-4">
<Button type="button" variant="outline" className="w-full sm:w-auto" onClick={() => router.push('/expenses')}>
Avbryt
</Button>
<Button type="submit" disabled={isSubmitting}>
<Button type="submit" disabled={isSubmitting} className="w-full sm:w-auto">
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
+18 -1
View File
@@ -8,6 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { PageHeader } from '@/components/ui/page-header'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency, formatDate } from '@/lib/utils'
@@ -176,7 +177,23 @@ export default function InvoicesPage() {
className="pl-10"
/>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
{/* Mobile: dropdown select */}
<Select value={activeTab} onValueChange={setActiveTab}>
<SelectTrigger className="sm:hidden w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Alla</SelectItem>
<SelectItem value="unpaid">Obetalda</SelectItem>
<SelectItem value="paid">Betalda</SelectItem>
<SelectItem value="draft">Utkast</SelectItem>
<SelectItem value="proforma">Proforma</SelectItem>
<SelectItem value="delivery_note">Följesedel</SelectItem>
<SelectItem value="credit">Kredit</SelectItem>
</SelectContent>
</Select>
{/* Desktop: tab bar */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="hidden sm:block">
<TabsList>
<TabsTrigger value="all">Alla</TabsTrigger>
<TabsTrigger value="unpaid">Obetalda</TabsTrigger>