From a1ca96453cde7a8b33e0d7174960992101346257 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:36:52 +0100 Subject: [PATCH] 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 * 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 --------- Co-authored-by: Claude Opus 4.6 --- .claude/skills/mobile-ux-core/SKILL.md | 52 +++ app/(dashboard)/bookkeeping/page.tsx | 6 +- app/(dashboard)/customers/[id]/page.tsx | 2 +- app/(dashboard)/customers/page.tsx | 6 +- app/(dashboard)/deadlines/page.tsx | 32 +- app/(dashboard)/invoices/[id]/page.tsx | 53 +-- app/(dashboard)/invoices/new/page.tsx | 305 +++++++++++++----- app/(dashboard)/invoices/page.tsx | 190 ++++++----- app/(dashboard)/layout.tsx | 25 +- app/(dashboard)/reports/page.tsx | 107 ++++-- app/(dashboard)/settings/page.tsx | 40 ++- .../supplier-invoices/[id]/page.tsx | 161 +++++---- .../supplier-invoices/new/page.tsx | 169 +++++++--- app/(dashboard)/supplier-invoices/page.tsx | 113 ++++--- app/(dashboard)/suppliers/[id]/page.tsx | 97 ++++-- app/(dashboard)/suppliers/page.tsx | 6 +- app/api/transactions/[id]/categorize/route.ts | 4 +- app/globals.css | 5 + app/layout.tsx | 1 + components/bookkeeping/AddAccountDialog.tsx | 2 +- .../bookkeeping/ChartOfAccountsManager.tsx | 4 +- .../bookkeeping/CorrectionEntryDialog.tsx | 142 ++++---- components/bookkeeping/EditAccountDialog.tsx | 2 +- .../bookkeeping/JournalEntryAttachments.tsx | 2 +- components/bookkeeping/JournalEntryForm.tsx | 94 +++++- components/bookkeeping/JournalEntryList.tsx | 171 +++++++--- .../bookkeeping/JournalEntryReviewContent.tsx | 103 +++--- components/customers/CustomerForm.tsx | 4 +- components/dashboard/DashboardNav.tsx | 32 +- components/deadlines/DeadlineForm.tsx | 10 +- .../invoice-inbox/InboxDetailDialog.tsx | 2 +- .../shared/MockDataImportDialog.tsx | 2 +- .../import/BankFileColumnMappingStep.tsx | 6 +- components/invoices/InvoiceReviewContent.tsx | 86 +++-- components/onboarding/Step2CompanyDetails.tsx | 2 +- .../onboarding/Step3TaxRegistration.tsx | 2 +- components/onboarding/Step6ConnectBank.tsx | 2 +- components/suppliers/SupplierForm.tsx | 12 +- .../SupplierInvoiceReviewContent.tsx | 180 +++++++---- .../transactions/TransactionBookingDialog.tsx | 2 +- components/transactions/TransactionForm.tsx | 2 +- .../transactions/TransactionHistoryList.tsx | 2 +- components/ui/confirmation-dialog.tsx | 12 +- components/ui/page-header.tsx | 4 +- components/ui/tabs.tsx | 2 +- lib/import/__tests__/sie-parser.test.ts | 90 ++++++ lib/import/sie-parser.ts | 18 +- 47 files changed, 1628 insertions(+), 738 deletions(-) create mode 100644 .claude/skills/mobile-ux-core/SKILL.md diff --git a/.claude/skills/mobile-ux-core/SKILL.md b/.claude/skills/mobile-ux-core/SKILL.md new file mode 100644 index 00000000..2dc9c019 --- /dev/null +++ b/.claude/skills/mobile-ux-core/SKILL.md @@ -0,0 +1,52 @@ +--- +name: Mobile UX Core +description: Universal mobile UX principles for touch-first interfaces. Enforces touch targets, safe areas, and mobile-specific interaction patterns. +metadata: + labels: [mobile, ux, design, accessibility, cross-platform] + triggers: + files: + [ + '**/*_page.dart', + '**/*_screen.dart', + '**/*_view.dart', + '**/*.swift', + '**/*Activity.kt', + '**/*Screen.tsx', + ] + keywords: [mobile, responsive, SafeArea, touch, gesture, viewport] +--- + +# Mobile UX Core + +## **Priority: P0 (CRITICAL)** + +Universal UX principles for mobile applications. + +## Guidelines + +- **Touch Targets**: Min 44x44pt (iOS) / 48x48dp (Android). Add padding if needed. +- **Safe Areas**: Wrap content in `SafeArea`/`WindowInsets`. Avoid notches. +- **Interactions**: Use active states (no hover). Haptic feedback (short). +- **Typography**: Min 16sp body. Line height 1.5x. +- **Keyboards**: Auto-scroll inputs. Set `InputType` (email/number) & `Action`. + +## Code Examples + +```dart +// ✅ Correct Target +IconButton(icon: Icon(Icons.close), padding: EdgeInsets.all(12)) + +// ❌ Too Small +Icon(Icons.close, size: 16) +``` + +## Anti-Patterns + +- **No Hover Effects**: Mobile has no cursor. Use pressed states. +- **No Tiny Targets**: All clickable elements ≥44pt. +- **No Fixed Bottom**: Account for Home Indicator & Keyboard. +- **No OS Mix**: Use Material (Android) & Cupertino (iOS) conventions. + +## Related Topics + +mobile-accessibility | mobile-performance | flutter-design-system | react-native-dls diff --git a/app/(dashboard)/bookkeeping/page.tsx b/app/(dashboard)/bookkeeping/page.tsx index 0c5c7254..0bfc0e85 100644 --- a/app/(dashboard)/bookkeeping/page.tsx +++ b/app/(dashboard)/bookkeeping/page.tsx @@ -14,14 +14,14 @@ export default function BookkeepingPage() { return (
-
+
-

Bokföring

+

Bokföring

Verifikationer, kontoplan och manuella bokföringsorder

- - + Lägg till kund diff --git a/app/(dashboard)/deadlines/page.tsx b/app/(dashboard)/deadlines/page.tsx index 0e90a0c4..c1ac98f7 100644 --- a/app/(dashboard)/deadlines/page.tsx +++ b/app/(dashboard)/deadlines/page.tsx @@ -190,9 +190,35 @@ export default function DeadlinesPage() {

Deadlines

-
-
-
+ {/* Overdue alert skeleton */} +
+
+
+
+
+
+
+
+
+
+
+
+ {/* Deadline list skeleton */} +
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+ ))}
) diff --git a/app/(dashboard)/invoices/[id]/page.tsx b/app/(dashboard)/invoices/[id]/page.tsx index 6c38db67..d6882dc9 100644 --- a/app/(dashboard)/invoices/[id]/page.tsx +++ b/app/(dashboard)/invoices/[id]/page.tsx @@ -356,14 +356,14 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st return (
{/* Header */} -
+
-
-

{invoice.invoice_number}

+
+

{invoice.invoice_number}

{isProforma && ( Proforma )} @@ -383,7 +383,7 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
{/* Actions */} -
+
{isProforma && invoice.status !== 'cancelled' && ( +
+
+ + {/* Mobile: compact card layout */} +
+
+
+ + + {errors.items?.[index]?.description && ( +

+ {errors.items[index].description?.message} +

+ )} +
+ +
+
+
+ + +
+
+ + ( + + )} + /> +
+
+ + +
+
+ {isRateLocked ? ( +
+ +

+ {availableRates.find(r => r.rate === (watchItems[index]?.vat_rate ?? 25))?.label ?? `${watchItems[index]?.vat_rate ?? 25}%`} +

+
+ ) : ( +
+ + ( + + )} + /> +
)} - /> +
+ Rad {index + 1} + {formatCurrency(lineTotal + lineVat, watchCurrency)} +
+
-
- -
-
- ))} + ) + })} +
+
+ + {/* Mobile sticky total bar */} +
+
+
+

Totalt

+

{formatCurrency(total, watchCurrency)}

+
+
@@ -708,7 +835,7 @@ export default function NewInvoicePage() { {/* Create customer dialog */} - + Lägg till kund diff --git a/app/(dashboard)/invoices/page.tsx b/app/(dashboard)/invoices/page.tsx index 7c3289f1..a6cb9a6d 100644 --- a/app/(dashboard)/invoices/page.tsx +++ b/app/(dashboard)/invoices/page.tsx @@ -119,52 +119,72 @@ export default function InvoicesPage() { {/* Stats */}
- - -
-
- -
-
-

Totalt antal

-

{invoices.length}

-
-
-
-
- - -
-
- -
-
-

Obetalda

-
-

{stats.unpaid}

- {stats.overdue > 0 && ( - - {stats.overdue} förfallna - - )} + {isLoading ? ( + <> + {[1, 2, 3].map((i) => ( + + +
+
+
+
+
+
+
+ + + ))} + + ) : ( + <> + + +
+
+ +
+
+

Totalt antal

+

{invoices.length}

+
-
-
- - - - -
-
- -
-
-

Att få in

-

{formatCurrency(stats.unpaidAmount)}

-
-
-
-
+ + + + +
+
+ +
+
+

Obetalda

+
+

{stats.unpaid}

+ {stats.overdue > 0 && ( + + {stats.overdue} förfallna + + )} +
+
+
+
+
+ + +
+
+ +
+
+

Att få in

+

{formatCurrency(stats.unpaidAmount)}

+
+
+
+
+ + )}
{/* Search and tabs */} @@ -248,51 +268,47 @@ export default function InvoicesPage() { -
-
-
- -
-
-
-

{invoice.invoice_number}

- {isCreditNote && ( - - Kredit - - )} - {isProforma && ( - - Proforma - - )} - {isDeliveryNote && ( - - Följesedel - - )} - - {status.label} - -
-
-

- {(invoice.customer as { name: string })?.name} · {formatDate(invoice.invoice_date)} -

- {relativeTime && ( - - {relativeTime.text} - - )} -
-
+
+
+
-
-

- {formatCurrency(Number(invoice.total), invoice.currency)} +

+
+

{invoice.invoice_number}

+

+ {formatCurrency(Number(invoice.total), invoice.currency)} +

+
+

+ {(invoice.customer as { name: string })?.name} · {formatDate(invoice.invoice_date)}

+
+ {isCreditNote && ( + + Kredit + + )} + {isProforma && ( + + Proforma + + )} + {isDeliveryNote && ( + + Följesedel + + )} + + {status.label} + + {relativeTime && ( + + {relativeTime.text} + + )} +
{invoice.currency !== 'SEK' && invoice.total_sek && ( -

+

{formatCurrency(Number(invoice.total_sek))}

)} diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 19500d94..b1f30cc5 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -18,11 +18,18 @@ export default async function DashboardLayout({ redirect('/login') } - const { data: settings } = await supabase - .from('company_settings') - .select('company_name, onboarding_complete, entity_type, is_sandbox') - .eq('user_id', user.id) - .single() + const [{ data: settings }, { count: uncategorizedCount }] = await Promise.all([ + supabase + .from('company_settings') + .select('company_name, onboarding_complete, entity_type, is_sandbox') + .eq('user_id', user.id) + .single(), + supabase + .from('transactions') + .select('*', { count: 'exact', head: true }) + .eq('user_id', user.id) + .is('is_business', null), + ]) if (!settings?.onboarding_complete) { redirect('/onboarding') @@ -30,12 +37,6 @@ export default async function DashboardLayout({ const entityType = (settings.entity_type as EntityType) || 'enskild_firma' - const { count: uncategorizedCount } = await supabase - .from('transactions') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - .is('is_business', null) - const isSandbox = settings.is_sandbox === true return ( @@ -54,7 +55,7 @@ export default async function DashboardLayout({ uncategorizedTransactionCount={uncategorizedCount ?? 0} isSandbox={isSandbox} /> -
+
{children}
diff --git a/app/(dashboard)/reports/page.tsx b/app/(dashboard)/reports/page.tsx index 646c464d..1e07517c 100644 --- a/app/(dashboard)/reports/page.tsx +++ b/app/(dashboard)/reports/page.tsx @@ -33,6 +33,7 @@ export default function ReportsPage() { const [selectedPeriod, setSelectedPeriod] = useState('') const [activeTab, setActiveTab] = useState('trial-balance') const [entityType, setEntityType] = useState(null) + const [isLoadingInit, setIsLoadingInit] = useState(true) async function fetchPeriods() { const res = await fetch('/api/bookkeeping/fiscal-periods') @@ -56,8 +57,9 @@ export default function ReportsPage() { } useEffect(() => { - fetchPeriods() - fetchEntityType() + Promise.all([fetchPeriods(), fetchEntityType()]).finally(() => { + setIsLoadingInit(false) + }) }, []) const isEnskildFirma = entityType === 'enskild_firma' @@ -65,16 +67,45 @@ export default function ReportsPage() { return (
-
+
-

Rapporter

+

Rapporter

Huvudbok, grundbok, kundreskontra, saldobalans, resultaträkning, balansräkning, momsdeklaration och mer

-
+ {isLoadingInit ? ( +
+
+
+
+
+
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+
+
+
+
+ ))} +
+ + +
+
+ + +
+ ) : ( + <> +
setActiveTab(e.target.value)} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + > + + + + + + + + {isEnskildFirma && } + {isAktiebolag && } + + + + + + + + + + + +
+ + {/* Desktop: grid tab navigation */} +
{/* Bokslut (Financial Statements) */}

@@ -224,6 +285,8 @@ export default function ReportsPage() { )} + + )}

) } @@ -303,7 +366,7 @@ function TrialBalanceView({ periodId }: { periodId: string }) {
- +
@@ -351,7 +414,7 @@ function TrialBalanceView({ periodId }: { periodId: string }) { -
Konto
+
@@ -635,7 +698,7 @@ function ReportSectionTable({ {sections.map((section) => (

{section.title}

- +
{section.rows.map((row) => ( @@ -647,7 +710,7 @@ function ReportSectionTable({ ))} -
+
{section.title} @@ -838,7 +901,7 @@ function VatDeclarationView() { {/* Utgående moms */}

Utgående moms (försäljning)

- +
{data.rutor.ruta05 > 0 && ( @@ -890,13 +953,13 @@ function VatDeclarationView() { -
+
{/* Ingående moms */}

Ingående moms (avdragsgill)

- +
-
@@ -928,7 +991,7 @@ function VatDeclarationView() { {formatAmount(data.rutor.ruta48)} kr
+
@@ -1126,7 +1189,7 @@ function SupplierLedgerView({ periodId }: { periodId: string }) { Ålderfördelning per leverantör - +
@@ -1162,7 +1225,7 @@ function SupplierLedgerView({ periodId }: { periodId: string }) { -
Leverantör{formatAmount(ledger.total_outstanding)}
+
)} @@ -1343,7 +1406,7 @@ function GeneralLedgerView({ periodId }: { periodId: string }) {
- +
@@ -1380,7 +1443,7 @@ function GeneralLedgerView({ periodId }: { periodId: string }) { -
Ver.nr{formatAmount(account.closing_balance)}
+
))} @@ -1498,7 +1561,7 @@ function JournalRegisterView({ periodId }: { periodId: string }) { Grundbok (registreringsordning) - +
@@ -1568,7 +1631,7 @@ function JournalRegisterView({ periodId }: { periodId: string }) { -
{formatAmount(data.total_credit)}
+
@@ -1724,7 +1787,7 @@ function ARLedgerView({ periodId }: { periodId: string }) { Ålderfördelning per kund - +
@@ -1797,7 +1860,7 @@ function ARLedgerView({ periodId }: { periodId: string }) { -
{formatAmount(ledger.total_outstanding)}
+
)} diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index fe9b84d4..8f6fb58e 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -241,8 +241,44 @@ export default function SettingsPage() { if (isLoading) { return ( -
- +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
) } diff --git a/app/(dashboard)/supplier-invoices/[id]/page.tsx b/app/(dashboard)/supplier-invoices/[id]/page.tsx index 960c1f33..b7dd37fd 100644 --- a/app/(dashboard)/supplier-invoices/[id]/page.tsx +++ b/app/(dashboard)/supplier-invoices/[id]/page.tsx @@ -165,28 +165,28 @@ export default function SupplierInvoiceDetailPage() { return (
{/* Header */} -
-
- -
-
-

+
+
+

Ankomst #{invoice.arrival_number}

{statusLabels[invoice.status] || invoice.status}
-

+

{invoice.supplier?.name} | Faktura {invoice.supplier_invoice_number}

{/* Actions */} -
+
{invoice.status === 'registered' && ( <>
+ {/* Mobile cards */} +
+ {items.map((item) => ( +
+
{item.description}
+
+ {item.quantity} {item.unit} × {formatAmount(item.unit_price)} + {formatAmount(item.line_total)} kr +
+
+ · {Math.round(item.vat_rate * 100)}% moms + moms {formatAmount(item.vat_amount)} +
+
+ ))} +
@@ -349,32 +368,54 @@ export default function SupplierInvoiceDetailPage() { Betalningshistorik - - - - - - - - - - - {payments.map((p) => ( - - - - - + {/* Desktop table */} +
+
DatumBeloppVerifikationAnteckning
{p.payment_date}{formatAmount(p.amount)} {p.currency} - {p.journal_entry_id ? ( - - {p.journal_entry_id.substring(0, 8)}... - - ) : '-'} - {p.notes || '-'}
+ + + + + + - ))} - -
DatumBeloppVerifikationAnteckning
+ + + {payments.map((p) => ( + + {p.payment_date} + {formatAmount(p.amount)} {p.currency} + + {p.journal_entry_id ? ( + + {p.journal_entry_id.substring(0, 8)}... + + ) : '-'} + + {p.notes || '-'} + + ))} + + +
+ {/* Mobile cards */} +
+ {payments.map((p) => ( +
+
+ {p.payment_date} + {formatAmount(p.amount)} {p.currency} +
+
+ {p.journal_entry_id ? ( + + {p.journal_entry_id.substring(0, 8)}... + + ) : -} + {p.notes || ''} +
+
+ ))} +
)} diff --git a/app/(dashboard)/supplier-invoices/new/page.tsx b/app/(dashboard)/supplier-invoices/new/page.tsx index 12316736..be7774fd 100644 --- a/app/(dashboard)/supplier-invoices/new/page.tsx +++ b/app/(dashboard)/supplier-invoices/new/page.tsx @@ -287,7 +287,7 @@ export default function NewSupplierInvoicePage() { Datum -
+
@@ -383,21 +383,97 @@ export default function NewSupplierInvoicePage() { - - - - - - - - - - - - - {fields.map((field, index) => ( - - - - - - - - - ))} - -
KontoBeskrivningBelopp (exkl.)MomssatsMoms
+ {/* Desktop table */} +
+ + + + + + + + + + + + + {fields.map((field, index) => ( + + + + + + + + + ))} + +
KontoBeskrivningBelopp (exkl.)MomssatsMoms
+ ( + handleAccountChange(index, val)} + /> + )} + /> + + + + + + ( + + )} + /> + + {formatAmount(itemTotals[index]?.vatAmount || 0)} + + {fields.length > 1 && ( + + )} +
+
+ {/* Mobile cards */} +
+ {fields.map((field, index) => ( +
+
+
)} /> -
- - + + {fields.length > 1 && ( + + )} + + +
+
+ -
+ +
+ )} /> -
- {formatAmount(itemTotals[index]?.vatAmount || 0)} - - {fields.length > 1 && ( - - )} -
+
+
+
+ Moms: {formatAmount(itemTotals[index]?.vatAmount || 0)} kr +
+
+ ))} +
{/* Totals */}
diff --git a/app/(dashboard)/supplier-invoices/page.tsx b/app/(dashboard)/supplier-invoices/page.tsx index fa721a62..a5d73c8c 100644 --- a/app/(dashboard)/supplier-invoices/page.tsx +++ b/app/(dashboard)/supplier-invoices/page.tsx @@ -71,9 +71,9 @@ export default function SupplierInvoicesPage() { return (
-
+
-

Leverantörsfakturor

+

Leverantörsfakturor

Registrera och hantera inkommande fakturor

@@ -88,43 +88,61 @@ export default function SupplierInvoicesPage() { {/* Summary cards */}
- - - - - Totalt obetalt - - - -

{formatAmount(totalUnpaid)} kr

-

- {invoices.filter((i) => !['paid', 'credited'].includes(i.status)).length} fakturor -

-
-
- - - - - Förfallet - - - -

{formatAmount(overdueAmount)} kr

-

{overdueCount} fakturor

-
-
- - - - - Antal fakturor - - - -

{invoices.length}

-
-
+ {isLoading ? ( + <> + {[1, 2, 3].map((i) => ( + + +
+ + +
+
+ + + ))} + + ) : ( + <> + + + + + Totalt obetalt + + + +

{formatAmount(totalUnpaid)} kr

+

+ {invoices.filter((i) => !['paid', 'credited'].includes(i.status)).length} fakturor +

+
+
+ + + + + Förfallet + + + +

{formatAmount(overdueAmount)} kr

+

{overdueCount} fakturor

+
+
+ + + + + Antal fakturor + + + +

{invoices.length}

+
+
+ + )}
{/* Tabs */} @@ -140,8 +158,21 @@ export default function SupplierInvoicesPage() { {isLoading ? ( - - Laddar fakturor... + +
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+
+
+
+
+
+ ))} ) : filteredInvoices.length === 0 ? ( diff --git a/app/(dashboard)/suppliers/[id]/page.tsx b/app/(dashboard)/suppliers/[id]/page.tsx index e2b597fb..2583dafe 100644 --- a/app/(dashboard)/suppliers/[id]/page.tsx +++ b/app/(dashboard)/suppliers/[id]/page.tsx @@ -238,40 +238,69 @@ export default function SupplierDetailPage() { Inga fakturor registrerade för denna leverantör

) : ( - - - - - - - - - - - - - - {invoices.map((inv) => ( - - - - - - - - + <> + {/* Desktop table */} +
+
AnkomstFakturanrDatumFörfallerBeloppKvarStatus
{inv.arrival_number} - - {inv.supplier_invoice_number} - - {inv.invoice_date}{inv.due_date}{formatAmount(inv.total)} kr{formatAmount(inv.remaining_amount)} kr - - {statusLabels[inv.status] || inv.status} - -
+ + + + + + + + + - ))} - -
AnkomstFakturanrDatumFörfallerBeloppKvarStatus
+ + + {invoices.map((inv) => ( + + {inv.arrival_number} + + + {inv.supplier_invoice_number} + + + {inv.invoice_date} + {inv.due_date} + {formatAmount(inv.total)} kr + {formatAmount(inv.remaining_amount)} kr + + + {statusLabels[inv.status] || inv.status} + + + + ))} + + +
+ {/* Mobile cards */} +
+ {invoices.map((inv) => ( +
+
+ + {inv.supplier_invoice_number} + + + {statusLabels[inv.status] || inv.status} + +
+
+ {inv.invoice_date} → {inv.due_date} + {formatAmount(inv.total)} kr +
+ {Number(inv.remaining_amount) > 0 && Number(inv.remaining_amount) !== Number(inv.total) && ( +
+ Kvar: {formatAmount(inv.remaining_amount)} kr +
+ )} +
+ ))} +
+ )} @@ -280,7 +309,7 @@ export default function SupplierDetailPage() { {/* Edit Dialog */} - + Redigera leverantör diff --git a/app/(dashboard)/suppliers/page.tsx b/app/(dashboard)/suppliers/page.tsx index 5a323b88..9509d3b1 100644 --- a/app/(dashboard)/suppliers/page.tsx +++ b/app/(dashboard)/suppliers/page.tsx @@ -95,9 +95,9 @@ export default function SuppliersPage() { return (
-
+
-

Leverantörer

+

Leverantörer

Hantera dina leverantörer och deras betalningsuppgifter

@@ -109,7 +109,7 @@ export default function SuppliersPage() { Ny leverantör - + Lägg till leverantör diff --git a/app/api/transactions/[id]/categorize/route.ts b/app/api/transactions/[id]/categorize/route.ts index 0d3c3b60..7edf3a0b 100644 --- a/app/api/transactions/[id]/categorize/route.ts +++ b/app/api/transactions/[id]/categorize/route.ts @@ -203,8 +203,8 @@ export async function POST( vatLines: mappingResult.vat_lines.map((v) => `${v.account_number} debit=${v.debit_amount} credit=${v.credit_amount}`), }) - // Apply account override if provided (only for business transactions) - if (is_business && body.account_override) { + // Apply account override if provided (only for category-based booking, not templates) + if (is_business && body.account_override && !body.template_id) { // Validate the account exists in the user's chart of accounts const { data: accountExists } = await supabase .from('chart_of_accounts') diff --git a/app/globals.css b/app/globals.css index 7c86a942..060b7096 100644 --- a/app/globals.css +++ b/app/globals.css @@ -194,6 +194,11 @@ h1, h2, h3 { padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); } +/* Main content padding: clears bottom nav + safe area on mobile */ +.safe-area-main-padding { + padding-bottom: calc(5rem + env(safe-area-inset-bottom, 0px)); +} + /* Animation Keyframes */ @keyframes fadeIn { from { opacity: 0; } diff --git a/app/layout.tsx b/app/layout.tsx index 7add81a0..e2bd8884 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -37,6 +37,7 @@ export const viewport: Viewport = { themeColor: "#304D83", width: "device-width", initialScale: 1, + viewportFit: "cover", }; export default function RootLayout({ diff --git a/components/bookkeeping/AddAccountDialog.tsx b/components/bookkeeping/AddAccountDialog.tsx index 88f19bbd..9ae8d025 100644 --- a/components/bookkeeping/AddAccountDialog.tsx +++ b/components/bookkeeping/AddAccountDialog.tsx @@ -126,7 +126,7 @@ export function AddAccountDialog({ open, onOpenChange, onCreated }: AddAccountDi
)} -
+
setEditAccount(account)} > @@ -428,7 +428,7 @@ export default function ChartOfAccountsManager() {

{entry.description}

- - - - - - - - - - - {originalLines.map((line) => ( - - - - - +
+
KontoBeskrivningDebetKredit
{line.line_description || ''} - {Number(line.debit_amount) > 0 - ? Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 }) - : ''} - - {Number(line.credit_amount) > 0 - ? Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 }) - : ''} -
+ + + + + + - ))} - -
KontoBeskrivningDebetKredit
+ + + {originalLines.map((line) => ( + + + {line.line_description || ''} + + {Number(line.debit_amount) > 0 + ? Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 }) + : ''} + + + {Number(line.credit_amount) > 0 + ? Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 }) + : ''} + + + ))} + + +
+
+ {originalLines.map((line) => ( +
+
+ +
+ + {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 })}`} + +
+ ))} +
{/* Divider */} @@ -178,45 +194,49 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
{lines.map((line, index) => ( -
- updateLine(index, 'account_number', v)} - /> +
+
+ updateLine(index, 'account_number', v)} + /> + +
updateLine(index, 'line_description', e.target.value)} placeholder="Beskrivning" className="h-8" /> - updateLine(index, 'debit_amount', e.target.value)} - placeholder="Debet" - className="h-8 text-right" - min={0} - step="0.01" - /> - updateLine(index, 'credit_amount', e.target.value)} - placeholder="Kredit" - className="h-8 text-right" - min={0} - step="0.01" - /> - +
+ updateLine(index, 'debit_amount', e.target.value)} + placeholder="Debet" + className="h-8 text-right" + min={0} + step="0.01" + /> + updateLine(index, 'credit_amount', e.target.value)} + placeholder="Kredit" + className="h-8 text-right" + min={0} + step="0.01" + /> +
))}
diff --git a/components/bookkeeping/EditAccountDialog.tsx b/components/bookkeeping/EditAccountDialog.tsx index f48fee9e..b4aac9ff 100644 --- a/components/bookkeeping/EditAccountDialog.tsx +++ b/components/bookkeeping/EditAccountDialog.tsx @@ -88,7 +88,7 @@ export function EditAccountDialog({ open, onOpenChange, account, onSaved }: Edit />
-
+
handleDownload(doc.id)} title="Ladda ner" > diff --git a/components/bookkeeping/JournalEntryForm.tsx b/components/bookkeeping/JournalEntryForm.tsx index d90f0a52..381682cb 100644 --- a/components/bookkeeping/JournalEntryForm.tsx +++ b/components/bookkeeping/JournalEntryForm.tsx @@ -230,7 +230,7 @@ export default function JournalEntryForm({ const formContent = (
-
+
updateLine(index, 'line_description', e.target.value)} + placeholder="Radtext..." + /> +
+
+ + updateLine(index, 'debit_amount', e.target.value)} + placeholder="0,00" + className="text-right" + inputMode="decimal" + min="0" + step="0.01" + /> +
+
+ + updateLine(index, 'credit_amount', e.target.value)} + placeholder="0,00" + className="text-right" + inputMode="decimal" + min="0" + step="0.01" + /> +
+
+
+ ))} + + {/* Mobile totals */} +
+ Summa +
+ + D: {totalDebit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} + + + K: {totalCredit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} + +
+
+ + +
+ + {/* Entry lines — desktop table */} +
@@ -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]" > - + diff --git a/components/bookkeeping/JournalEntryList.tsx b/components/bookkeeping/JournalEntryList.tsx index d5158e0b..22103fe9 100644 --- a/components/bookkeeping/JournalEntryList.tsx +++ b/components/bookkeeping/JournalEntryList.tsx @@ -138,9 +138,10 @@ export default function JournalEntryList({ periodId }: Props) { {isExpanded && ( @@ -174,59 +205,95 @@ export default function JournalEntryList({ periodId }: Props) { {lines.length === 0 ? (

Inga kontorader hittades för denna verifikation.

) : ( -
- - - - - - - - - + <> + {/* Mobile: stacked cards */} +
{lines .sort((a, b) => a.sort_order - b.sort_order) .map((line) => ( -
- - - - - +
+
+
+ {line.line_description && ( +

{line.line_description}

+ )} +
+
+ {Number(line.debit_amount) > 0 && ( +

{Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} D

+ )} + {Number(line.credit_amount) > 0 && ( +

{Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} K

+ )} +
+
))} - - - - - - - - -
KontoBeskrivningDebetKredit
- {line.line_description || ''} - - {Number(line.debit_amount) > 0 - ? Number(line.debit_amount).toLocaleString('sv-SE', { - minimumFractionDigits: 2, - }) - : ''} - - {Number(line.credit_amount) > 0 - ? Number(line.credit_amount).toLocaleString('sv-SE', { - minimumFractionDigits: 2, - }) - : ''} -
- Summa - - {lines - .reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0) - .toLocaleString('sv-SE', { minimumFractionDigits: 2 })} - - {lines - .reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0) - .toLocaleString('sv-SE', { minimumFractionDigits: 2 })} -
+
+ Summa +
+ D: {lines.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} + K: {lines.reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} +
+
+
+ + {/* Desktop: table */} +
+ + + + + + + + + + + {lines + .sort((a, b) => a.sort_order - b.sort_order) + .map((line) => ( + + + + + + + ))} + + + + + + + + +
KontoBeskrivningDebetKredit
+ {line.line_description || ''} + + {Number(line.debit_amount) > 0 + ? Number(line.debit_amount).toLocaleString('sv-SE', { + minimumFractionDigits: 2, + }) + : ''} + + {Number(line.credit_amount) > 0 + ? Number(line.credit_amount).toLocaleString('sv-SE', { + minimumFractionDigits: 2, + }) + : ''} +
+ Summa + + {lines + .reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0) + .toLocaleString('sv-SE', { minimumFractionDigits: 2 })} + + {lines + .reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0) + .toLocaleString('sv-SE', { minimumFractionDigits: 2 })} +
+
+ )} )} - {/* Debit/Credit table */} - - - - - - - - - - - {activeLines.map((line, index) => ( - - - - - + {/* Debit/Credit — table on desktop, cards on mobile */} +
+
KontoBeskrivningDebetKredit
- - - {line.line_description || ''} - - {parseFloat(line.debit_amount) > 0 - ? formatAmount(parseFloat(line.debit_amount)) - : ''} - - {parseFloat(line.credit_amount) > 0 - ? formatAmount(parseFloat(line.credit_amount)) - : ''} -
+ + + + + + - ))} - - - - - - - - -
KontoBeskrivningDebetKredit
Summa{formatAmount(totalDebit)}{formatAmount(totalCredit)}
+ + + {activeLines.map((line, index) => ( + + + + + + {line.line_description || ''} + + + {parseFloat(line.debit_amount) > 0 + ? formatAmount(parseFloat(line.debit_amount)) + : ''} + + + {parseFloat(line.credit_amount) > 0 + ? formatAmount(parseFloat(line.credit_amount)) + : ''} + + + ))} + + + + Summa + {formatAmount(totalDebit)} + {formatAmount(totalCredit)} + + + +
+
+ {activeLines.map((line, index) => ( +
+
+
+ + {line.line_description && ( + {line.line_description} + )} +
+
+ + {parseFloat(line.debit_amount) > 0 + ? `D ${formatAmount(parseFloat(line.debit_amount))}` + : `K ${formatAmount(parseFloat(line.credit_amount))}`} + +
+ ))} +
+ Summa + D {formatAmount(totalDebit)} / K {formatAmount(totalCredit)} +
+
) } diff --git a/components/customers/CustomerForm.tsx b/components/customers/CustomerForm.tsx index 39e204b8..d1ec7099 100644 --- a/components/customers/CustomerForm.tsx +++ b/components/customers/CustomerForm.tsx @@ -168,7 +168,7 @@ export default function CustomerForm({
{/* Contact */} -
+
-
+
| 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 {/* Mobile bottom navigation */} -