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
@@ -106,6 +106,52 @@ const SIE_WITH_OBJECT_LIST = [
|
||||
'}',
|
||||
].join('\n')
|
||||
|
||||
// SIE file where all VER/TRANS fields are quoted (common from some accounting programs)
|
||||
const SIE_QUOTED_FIELDS = [
|
||||
'#FLAGGA 0',
|
||||
'#SIETYP 4',
|
||||
'#FNAMN "Quoted AB"',
|
||||
'#RAR 0 20240101 20241231',
|
||||
'#KONTO 1510 "Kundfordringar"',
|
||||
'#KONTO 3001 "Försäljning"',
|
||||
'#KONTO 2611 "Utgående moms 25%"',
|
||||
'#VER "A" "1" "20240115" "Faktura 1001"',
|
||||
'{',
|
||||
'#TRANS "1510" {} "12500.00"',
|
||||
'#TRANS "3001" {} "-10000.00"',
|
||||
'#TRANS "2611" {} "-2500.00"',
|
||||
'}',
|
||||
].join('\n')
|
||||
|
||||
// SIE file with empty series (some programs use "" for series)
|
||||
const SIE_EMPTY_SERIES = [
|
||||
'#FLAGGA 0',
|
||||
'#SIETYP 4',
|
||||
'#FNAMN "Empty Series AB"',
|
||||
'#RAR 0 20240101 20241231',
|
||||
'#KONTO 1930 "Företagskonto"',
|
||||
'#KONTO 3001 "Försäljning"',
|
||||
'#VER "" 1 20240115 "No series"',
|
||||
'{',
|
||||
'#TRANS 1930 {} 10000.00',
|
||||
'#TRANS 3001 {} -10000.00',
|
||||
'}',
|
||||
].join('\n')
|
||||
|
||||
// SIE file with { on same line as #VER
|
||||
const SIE_BRACE_ON_VER_LINE = [
|
||||
'#FLAGGA 0',
|
||||
'#SIETYP 4',
|
||||
'#FNAMN "Brace AB"',
|
||||
'#RAR 0 20240101 20241231',
|
||||
'#KONTO 1930 "Företagskonto"',
|
||||
'#KONTO 3001 "Försäljning"',
|
||||
'#VER A 1 20240115 "Inline brace" {',
|
||||
'#TRANS 1930 {} 10000.00',
|
||||
'#TRANS 3001 {} -10000.00',
|
||||
'}',
|
||||
].join('\n')
|
||||
|
||||
// --- parseSIEFile tests ---
|
||||
|
||||
describe('parseSIEFile', () => {
|
||||
@@ -232,6 +278,50 @@ describe('parseSIEFile', () => {
|
||||
expect(v.lines[1]).toMatchObject({ account: '1930', amount: -15000 })
|
||||
})
|
||||
|
||||
it('parses quoted VER fields (series, number, date)', () => {
|
||||
const result = parseSIEFile(SIE_QUOTED_FIELDS)
|
||||
expect(result.vouchers).toHaveLength(1)
|
||||
|
||||
const v = result.vouchers[0]
|
||||
expect(v.series).toBe('A')
|
||||
expect(v.number).toBe(1)
|
||||
expect(v.date).toEqual(new Date(2024, 0, 15))
|
||||
expect(v.description).toBe('Faktura 1001')
|
||||
expect(v.lines).toHaveLength(3)
|
||||
expect(v.lines[0]).toMatchObject({ account: '1510', amount: 12500 })
|
||||
expect(v.lines[1]).toMatchObject({ account: '3001', amount: -10000 })
|
||||
expect(v.lines[2]).toMatchObject({ account: '2611', amount: -2500 })
|
||||
|
||||
const errors = result.issues.filter((i) => i.severity === 'error')
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('allows empty series in VER', () => {
|
||||
const result = parseSIEFile(SIE_EMPTY_SERIES)
|
||||
expect(result.vouchers).toHaveLength(1)
|
||||
|
||||
const v = result.vouchers[0]
|
||||
expect(v.series).toBe('')
|
||||
expect(v.number).toBe(1)
|
||||
expect(v.lines).toHaveLength(2)
|
||||
|
||||
const errors = result.issues.filter((i) => i.severity === 'error')
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles { on same line as #VER', () => {
|
||||
const result = parseSIEFile(SIE_BRACE_ON_VER_LINE)
|
||||
expect(result.vouchers).toHaveLength(1)
|
||||
|
||||
const v = result.vouchers[0]
|
||||
expect(v.series).toBe('A')
|
||||
expect(v.number).toBe(1)
|
||||
expect(v.lines).toHaveLength(2)
|
||||
|
||||
const errors = result.issues.filter((i) => i.severity === 'error')
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('detects unbalanced vouchers as errors', () => {
|
||||
const result = parseSIEFile(SIE_UNBALANCED_VOUCHER)
|
||||
expect(result.vouchers).toHaveLength(1)
|
||||
|
||||
@@ -146,8 +146,9 @@ function parseStringField(field: string): string {
|
||||
*/
|
||||
function parseNumberField(field: string): number {
|
||||
if (!field) return 0
|
||||
// SIE uses dot as decimal separator
|
||||
return parseFloat(field.replace(',', '.')) || 0
|
||||
// Strip quotes and use dot as decimal separator
|
||||
const cleaned = parseStringField(field)
|
||||
return parseFloat(cleaned.replace(',', '.')) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -434,12 +435,13 @@ export function parseSIEFile(content: string): ParsedSIEFile {
|
||||
|
||||
case 'VER': {
|
||||
// #VER series number date "description" [regdate] [signature]
|
||||
// Some programs quote all fields, so strip quotes from number/date too
|
||||
const series = parseStringField(fields[1])
|
||||
const number = parseInt(fields[2], 10)
|
||||
const date = parseSIEDate(fields[3])
|
||||
const number = parseInt(parseStringField(fields[2]), 10)
|
||||
const date = parseSIEDate(parseStringField(fields[3]))
|
||||
const description = parseStringField(fields[4])
|
||||
|
||||
if (series && !isNaN(number) && date) {
|
||||
if (!isNaN(number) && date) {
|
||||
currentVoucher = {
|
||||
series,
|
||||
number,
|
||||
@@ -450,7 +452,7 @@ export function parseSIEFile(content: string): ParsedSIEFile {
|
||||
|
||||
// Optional registration date and signature
|
||||
if (fields[5]) {
|
||||
currentVoucher.registrationDate = parseSIEDate(fields[5]) || undefined
|
||||
currentVoucher.registrationDate = parseSIEDate(parseStringField(fields[5])) || undefined
|
||||
}
|
||||
if (fields[6]) {
|
||||
currentVoucher.signature = parseStringField(fields[6])
|
||||
@@ -470,7 +472,7 @@ export function parseSIEFile(content: string): ParsedSIEFile {
|
||||
|
||||
// Parse account and skip object list (in braces)
|
||||
let fieldIndex = 1
|
||||
const account = fields[fieldIndex++]
|
||||
const account = parseStringField(fields[fieldIndex++])
|
||||
|
||||
// Skip object list if present (now a single field thanks to brace-aware splitting)
|
||||
if (fields[fieldIndex]?.startsWith('{')) {
|
||||
@@ -486,7 +488,7 @@ export function parseSIEFile(content: string): ParsedSIEFile {
|
||||
|
||||
// Optional fields
|
||||
if (fields[fieldIndex]) {
|
||||
transLine.date = parseSIEDate(fields[fieldIndex++]) || undefined
|
||||
transLine.date = parseSIEDate(parseStringField(fields[fieldIndex++])) || undefined
|
||||
}
|
||||
if (fields[fieldIndex]) {
|
||||
transLine.description = parseStringField(fields[fieldIndex++])
|
||||
|
||||
Reference in New Issue
Block a user