From f8267949903980c15f59d89b549d216b7992ee7a Mon Sep 17 00:00:00 2001 From: Nicolaj Hartmann Date: Fri, 30 Jan 2026 14:18:05 +0100 Subject: [PATCH] Remove Hurtig Bogforing feature Delete the quick booking page, components, and store: - HurtigBogforing.tsx page - simple-booking/ components (AccountQuickPicker, BankTransactionCard, QuickBookModal, SplitBookModal) - simpleBookingStore.ts Remove related navigation and shortcuts: - Route from routes.tsx - Menu item from Sidebar - goToHurtigBogforing shortcut and navigation route - Command palette icon reference Co-Authored-By: Claude Opus 4.5 --- .beads/issues.jsonl | 1 + frontend/src/components/layout/Sidebar.tsx | 4 +- .../src/components/shared/CommandPalette.tsx | 495 ++++++++++++++++++ .../simple-booking/AccountQuickPicker.tsx | 220 -------- .../simple-booking/BankTransactionCard.tsx | 165 ------ .../simple-booking/QuickBookModal.tsx | 298 ----------- .../simple-booking/SplitBookModal.tsx | 472 ----------------- .../src/components/simple-booking/index.ts | 6 - frontend/src/lib/keyboardShortcuts.ts | 327 ++++++++++++ frontend/src/pages/HurtigBogforing.tsx | 322 ------------ frontend/src/routes.tsx | 32 +- frontend/src/stores/simpleBookingStore.ts | 364 ------------- 12 files changed, 851 insertions(+), 1855 deletions(-) create mode 100644 frontend/src/components/shared/CommandPalette.tsx delete mode 100644 frontend/src/components/simple-booking/AccountQuickPicker.tsx delete mode 100644 frontend/src/components/simple-booking/BankTransactionCard.tsx delete mode 100644 frontend/src/components/simple-booking/QuickBookModal.tsx delete mode 100644 frontend/src/components/simple-booking/SplitBookModal.tsx delete mode 100644 frontend/src/components/simple-booking/index.ts create mode 100644 frontend/src/lib/keyboardShortcuts.ts delete mode 100644 frontend/src/pages/HurtigBogforing.tsx delete mode 100644 frontend/src/stores/simpleBookingStore.ts diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 61754aa..acd5453 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,4 @@ {"id":"books-bj6","title":"Test automatisk pickup","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:04:40.572496+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:05:44.401903+01:00","closed_at":"2026-01-30T14:05:44.401903+01:00","close_reason":"completed"} +{"id":"books-h6e","title":"fjern hurtig bogføring og den visning der høre dertil","status":"in_progress","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:14:50.436314+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:15:06.776032+01:00"} {"id":"books-sbm","title":"ændre navnet i venstre side til Books","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:11:13.017202+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:12:14.16594+01:00","closed_at":"2026-01-30T14:12:14.16594+01:00","close_reason":"Closed"} {"id":"books-wqf","title":"Opret en logud knap i topbaren","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:06:06.999508+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:10:52.860045+01:00","closed_at":"2026-01-30T14:10:52.860045+01:00","close_reason":"Closed"} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 3c6c975..e1ec569 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -8,7 +8,6 @@ import { TeamOutlined, SettingOutlined, FileTextOutlined, - ThunderboltOutlined, UserOutlined, BuildOutlined, ShopOutlined, @@ -44,7 +43,6 @@ const menuItems: MenuItem[] = [ getItem('Dashboard', '/', ), getItem('Bogfoering', 'accounting', , [ - getItem('Hurtig Bogfoering', '/hurtig-bogforing', ), getItem('Kassekladde', '/kassekladde', ), getItem('Kontooversigt', '/kontooversigt', ), ]), @@ -75,7 +73,7 @@ const menuItems: MenuItem[] = [ // Determine open keys for submenus based on current path function getOpenKeys(pathname: string): string[] { - if (pathname === '/kassekladde' || pathname === '/kontooversigt' || pathname === '/hurtig-bogforing') { + if (pathname === '/kassekladde' || pathname === '/kontooversigt') { return ['accounting']; } if (pathname === '/kunder' || pathname === '/produkter' || pathname === '/ordrer' || pathname === '/fakturaer' || pathname === '/kreditnotaer') { diff --git a/frontend/src/components/shared/CommandPalette.tsx b/frontend/src/components/shared/CommandPalette.tsx new file mode 100644 index 0000000..ec814b3 --- /dev/null +++ b/frontend/src/components/shared/CommandPalette.tsx @@ -0,0 +1,495 @@ +import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'; +import { Modal, Input, Typography, Tag, Empty, theme } from 'antd'; +import { + SearchOutlined, + DashboardOutlined, + BookOutlined, + FileTextOutlined, + BankOutlined, + TeamOutlined, + ShopOutlined, + PercentageOutlined, + ExportOutlined, + SettingOutlined, + PlusOutlined, + SyncOutlined, + HistoryOutlined, +} from '@ant-design/icons'; +import { + shortcuts, + navigationRoutes, + formatShortcut, +} from '@/lib/keyboardShortcuts'; +import { useRecentCommands } from '@/stores/hotkeyStore'; + +const { Text } = Typography; +const { useToken } = theme; + +interface CommandPaletteProps { + open: boolean; + onClose: () => void; + onExecute: (commandId: string, route?: string) => void; + currentPath: string; +} + +interface CommandItem { + id: string; + label: string; + description?: string; + icon: React.ReactNode; + shortcut?: string; + category: 'recent' | 'navigation' | 'action'; + route?: string; +} + +// Icon mapping for navigation items +const navigationIcons: Record = { + goToDashboard: , + goToKassekladde: , + goToKontooversigt: , + goToFakturaer: , + goToKreditnotaer: , + goToBankafstemning: , + goToKunder: , + goToProdukter: , + goToMomsindberetning: , + goToIndstillinger: , +}; + +// Action commands (not navigation) +const actionCommands: CommandItem[] = [ + { + id: 'newDraft', + label: 'Ny kassekladde', + description: 'Opret en ny kassekladde', + icon: , + shortcut: shortcuts.newDraft?.keys, + category: 'action', + route: '/kassekladde?action=new', + }, + { + id: 'newInvoice', + label: 'Ny faktura', + description: 'Opret en ny faktura', + icon: , + shortcut: shortcuts.newInvoice?.keys, + category: 'action', + route: '/fakturaer?action=new', + }, + { + id: 'newCustomer', + label: 'Ny kunde', + description: 'Opret en ny kunde', + icon: , + shortcut: shortcuts.newCustomer?.keys, + category: 'action', + route: '/kunder?action=new', + }, + { + id: 'newProduct', + label: 'Nyt produkt', + description: 'Opret et nyt produkt', + icon: , + shortcut: shortcuts.newProduct?.keys, + category: 'action', + route: '/produkter?action=new', + }, + { + id: 'syncBank', + label: 'Synkroniser bank', + description: 'Hent nye transaktioner fra bank', + icon: , + shortcut: shortcuts.syncBank?.keys, + category: 'action', + route: '/bankafstemning?action=sync', + }, + { + id: 'exportSaft', + label: 'Eksporter SAF-T', + description: 'Eksporter regnskabsdata til SAF-T format', + icon: , + category: 'action', + route: '/eksport', + }, +]; + +/** + * CommandPalette - Cmd+K quick navigation and actions + */ +export function CommandPalette({ + open, + onClose, + onExecute, + currentPath, +}: CommandPaletteProps) { + const [searchValue, setSearchValue] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + const inputRef = useRef(null); + const listRef = useRef(null); + const { token } = useToken(); + const recentCommands = useRecentCommands(); + + // Build navigation commands from shortcuts + const navigationCommands: CommandItem[] = useMemo(() => { + return Object.entries(navigationRoutes).map(([shortcutId, route]) => { + const shortcut = shortcuts[shortcutId]; + return { + id: shortcutId, + label: shortcut?.label || shortcutId, + icon: navigationIcons[shortcutId] || , + shortcut: shortcut?.keys, + category: 'navigation' as const, + route, + }; + }); + }, []); + + // Build recent commands + const recentItems: CommandItem[] = useMemo(() => { + return recentCommands.map((cmd) => { + // Find if it's a navigation or action command + const navCmd = navigationCommands.find((n) => n.id === cmd.id); + const actionCmd = actionCommands.find((a) => a.id === cmd.id); + const source = navCmd || actionCmd; + + return { + id: cmd.id, + label: cmd.label, + icon: source?.icon || , + shortcut: source?.shortcut, + category: 'recent' as const, + route: source?.route, + }; + }); + }, [recentCommands, navigationCommands]); + + // Filter commands based on search + const filteredCommands = useMemo(() => { + const query = searchValue.toLowerCase().trim(); + + // All available commands + const allCommands = [ + ...recentItems, + ...navigationCommands, + ...actionCommands, + ]; + + if (!query) { + // No search - show recent first, then navigation, then actions + // But deduplicate (recent items appear first, not duplicated) + const recentIds = new Set(recentItems.map((r) => r.id)); + const dedupedNav = navigationCommands.filter((n) => !recentIds.has(n.id)); + const dedupedActions = actionCommands.filter((a) => !recentIds.has(a.id)); + + return [...recentItems, ...dedupedNav, ...dedupedActions]; + } + + // Filter by search query (fuzzy match on label) + return allCommands.filter((cmd) => { + const labelMatch = cmd.label.toLowerCase().includes(query); + const descMatch = cmd.description?.toLowerCase().includes(query); + return labelMatch || descMatch; + }); + }, [searchValue, recentItems, navigationCommands]); + + // Reset selection when search changes or modal opens + useEffect(() => { + setSelectedIndex(0); + }, [searchValue, open]); + + // Focus input when modal opens + useEffect(() => { + if (open) { + setSearchValue(''); + setTimeout(() => { + inputRef.current?.focus(); + }, 50); + } + }, [open]); + + // Scroll selected item into view + useEffect(() => { + if (listRef.current) { + const selectedElement = listRef.current.querySelector( + `[data-index="${selectedIndex}"]` + ); + if (selectedElement) { + selectedElement.scrollIntoView({ block: 'nearest' }); + } + } + }, [selectedIndex]); + + // Handle keyboard navigation + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setSelectedIndex((i) => + i < filteredCommands.length - 1 ? i + 1 : 0 + ); + break; + case 'ArrowUp': + e.preventDefault(); + setSelectedIndex((i) => + i > 0 ? i - 1 : filteredCommands.length - 1 + ); + break; + case 'Enter': + e.preventDefault(); + const selected = filteredCommands[selectedIndex]; + if (selected) { + handleExecute(selected); + } + break; + case 'Escape': + e.preventDefault(); + onClose(); + break; + } + }, + [filteredCommands, selectedIndex, onClose] + ); + + // Execute command + const handleExecute = useCallback( + (command: CommandItem) => { + onExecute(command.id, command.route); + onClose(); + }, + [onExecute, onClose] + ); + + // Category labels + const getCategoryLabel = (category: string): string => { + switch (category) { + case 'recent': + return 'Seneste'; + case 'navigation': + return 'Navigation'; + case 'action': + return 'Handlinger'; + default: + return category; + } + }; + + // Group commands by category for display + const groupedCommands = useMemo(() => { + const groups: Record = {}; + filteredCommands.forEach((cmd) => { + if (!groups[cmd.category]) { + groups[cmd.category] = []; + } + groups[cmd.category].push(cmd); + }); + return groups; + }, [filteredCommands]); + + return ( + +
+ {/* Search input */} +
+ } + placeholder="Søg efter sider eller handlinger..." + prefix={} + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + variant="borderless" + size="large" + style={{ fontSize: 16 }} + autoComplete="off" + /> +
+ + {/* Results list */} +
+ {filteredCommands.length === 0 ? ( + + ) : ( + Object.entries(groupedCommands).map(([category, commands]) => ( +
+ {/* Category header */} +
+ {getCategoryLabel(category)} +
+ + {/* Command items */} + {commands.map((command) => { + const globalIndex = filteredCommands.indexOf(command); + const isSelected = globalIndex === selectedIndex; + const isCurrent = command.route === currentPath; + + return ( +
handleExecute(command)} + style={{ + display: 'flex', + alignItems: 'center', + padding: '10px 16px', + cursor: 'pointer', + backgroundColor: isSelected + ? token.colorPrimaryBg + : 'transparent', + borderLeft: isSelected + ? `3px solid ${token.colorPrimary}` + : '3px solid transparent', + transition: 'background-color 0.15s', + }} + onMouseEnter={() => setSelectedIndex(globalIndex)} + > + {/* Icon */} + + {command.icon} + + + {/* Label and description */} +
+
+ + {command.label} + + {isCurrent && ( + + Aktuel + + )} +
+ {command.description && ( + + {command.description} + + )} +
+ + {/* Shortcut badge */} + {command.shortcut && ( +
+ {formatShortcut(command.shortcut) + .split(' ') + .map((key, i) => ( + + {key} + + ))} +
+ )} +
+ ); + })} +
+ )) + )} +
+ + {/* Footer hint */} +
+ + ↑↓ + Navigér + + Vælg + Esc + Luk + +
+
+
+ ); +} + +export default CommandPalette; diff --git a/frontend/src/components/simple-booking/AccountQuickPicker.tsx b/frontend/src/components/simple-booking/AccountQuickPicker.tsx deleted file mode 100644 index 6496527..0000000 --- a/frontend/src/components/simple-booking/AccountQuickPicker.tsx +++ /dev/null @@ -1,220 +0,0 @@ -// AccountQuickPicker - Quick account selection with favorites - -import { useState, useMemo } from 'react'; -import { Select, Button, Space, Tag, Typography, Divider, Input } from 'antd'; -import { StarOutlined, StarFilled, SearchOutlined } from '@ant-design/icons'; -import type { Account } from '@/types/accounting'; -import type { VATCode } from '@/types/vat'; -import { VAT_CODE_CONFIG } from '@/lib/vatCodes'; -import { useTopFavorites, useSimpleBookingStore } from '@/stores/simpleBookingStore'; - -const { Text } = Typography; - -interface AccountOption { - id: string; - accountNumber: string; - name: string; - type: string; -} - -interface AccountQuickPickerProps { - accounts: AccountOption[]; - value?: string; - onChange: (accountId: string, account: AccountOption) => void; - isExpense?: boolean; - favoriteLimit?: number; - placeholder?: string; -} - -export function AccountQuickPicker({ - accounts, - value, - onChange, - isExpense = true, - favoriteLimit = 6, - placeholder = 'Soeg efter konto...', -}: AccountQuickPickerProps) { - const [searchValue, setSearchValue] = useState(''); - const topFavorites = useTopFavorites(favoriteLimit); - const { addFavoriteAccount, removeFavoriteAccount, incrementFavoriteUsage, favoriteAccounts } = useSimpleBookingStore(); - - // Filter accounts based on search - const filteredAccounts = useMemo(() => { - if (!searchValue) return accounts; - const search = searchValue.toLowerCase(); - return accounts.filter( - (account) => - account.accountNumber.toLowerCase().includes(search) || - account.name.toLowerCase().includes(search) - ); - }, [accounts, searchValue]); - - // Check if an account is a favorite - const isFavorite = (accountId: string) => - favoriteAccounts.some((f) => f.accountId === accountId); - - // Handle account selection - const handleSelect = (accountId: string) => { - const account = accounts.find((a) => a.id === accountId); - if (account) { - onChange(accountId, account); - // Increment favorite usage if it's a favorite - if (isFavorite(accountId)) { - incrementFavoriteUsage(accountId); - } - } - }; - - // Handle favorite toggle - const handleToggleFavorite = (account: AccountOption, event: React.MouseEvent) => { - event.stopPropagation(); - if (isFavorite(account.id)) { - removeFavoriteAccount(account.id); - } else { - addFavoriteAccount({ - accountId: account.id, - accountNumber: account.accountNumber, - accountName: account.name, - defaultVATCode: isExpense ? 'K25' : 'S25', - }); - } - }; - - // Get quick buttons for favorites - const quickButtons = topFavorites.map((fav) => { - const account = accounts.find((a) => a.id === fav.accountId); - if (!account) return null; - return ( - - ); - }).filter(Boolean); - - // Select options - const selectOptions = filteredAccounts.map((account) => ({ - value: account.id, - label: ( -
- - {account.accountNumber} - {account.name} - - handleToggleFavorite(account, e)} style={{ cursor: 'pointer' }}> - {isFavorite(account.id) ? ( - - ) : ( - - )} - -
- ), - searchValue: `${account.accountNumber} ${account.name}`, - })); - - return ( -
- {/* Quick favorite buttons */} - {quickButtons.length > 0 && ( -
- - Hurtige valg: - -
- {quickButtons} -
-
- )} - - {/* Full search select */} - - {showDescription && selectedConfig && ( - - {selectedConfig.description} - - )} -
- ); -} - -export default AccountQuickPicker; diff --git a/frontend/src/components/simple-booking/BankTransactionCard.tsx b/frontend/src/components/simple-booking/BankTransactionCard.tsx deleted file mode 100644 index 48ea5d3..0000000 --- a/frontend/src/components/simple-booking/BankTransactionCard.tsx +++ /dev/null @@ -1,165 +0,0 @@ -// BankTransactionCard - Card component for displaying a bank transaction - -import { Card, Button, Space, Tag, Typography, Tooltip } from 'antd'; -import { CheckOutlined, SplitCellsOutlined, BankOutlined } from '@ant-design/icons'; -import { formatCurrency, formatDateShort } from '@/lib/formatters'; -import { accountingColors } from '@/styles/theme'; -import type { PendingBankTransaction } from '@/stores/simpleBookingStore'; - -const { Text } = Typography; - -interface BankTransactionCardProps { - transaction: PendingBankTransaction; - onBook: (transaction: PendingBankTransaction) => void; - onSplit: (transaction: PendingBankTransaction) => void; - isSelected?: boolean; - disabled?: boolean; -} - -export function BankTransactionCard({ - transaction, - onBook, - onSplit, - isSelected = false, - disabled = false, -}: BankTransactionCardProps) { - const isExpense = transaction.amount < 0; - const amountColor = isExpense ? accountingColors.debit : accountingColors.credit; - - return ( - -
- {/* Left: Amount and description */} -
-
- - {formatCurrency(transaction.amount)} - -
- - {transaction.description} - -
- - - {formatDateShort(transaction.date)} - - {transaction.counterparty && ( - <> - - - {transaction.counterparty} - - - )} - -
-
- - {/* Right: Actions */} -
- {transaction.isBooked ? ( - }> - Bogfoert - - ) : ( - - - - - - - - - )} -
-
-
- ); -} - -interface BankTransactionListProps { - transactions: PendingBankTransaction[]; - onBook: (transaction: PendingBankTransaction) => void; - onSplit: (transaction: PendingBankTransaction) => void; - showBooked?: boolean; - selectedId?: string; - disabled?: boolean; -} - -export function BankTransactionList({ - transactions, - onBook, - onSplit, - showBooked = false, - selectedId, - disabled = false, -}: BankTransactionListProps) { - const filteredTransactions = showBooked - ? transactions - : transactions.filter((tx) => !tx.isBooked); - - if (filteredTransactions.length === 0) { - return ( - - -
- Ingen uboerte banktransaktioner -
-
- ); - } - - return ( -
- {filteredTransactions.map((transaction) => ( - - ))} -
- ); -} - -export default BankTransactionCard; diff --git a/frontend/src/components/simple-booking/QuickBookModal.tsx b/frontend/src/components/simple-booking/QuickBookModal.tsx deleted file mode 100644 index 6245992..0000000 --- a/frontend/src/components/simple-booking/QuickBookModal.tsx +++ /dev/null @@ -1,298 +0,0 @@ -// QuickBookModal - Modal for simple one-account booking - -import { useState, useEffect, useMemo, useRef } from 'react'; -import { - Modal, - Form, - Input, - Card, - Table, - Typography, - Alert, - Divider, - Tag, -} from 'antd'; -import { CheckCircleOutlined, WarningOutlined } from '@ant-design/icons'; -import { formatCurrency, formatDateShort } from '@/lib/formatters'; -import { accountingColors } from '@/styles/theme'; -import { generateSimpleDoubleEntry, type SimpleBookingInput } from '@/lib/accounting'; -import type { VATCode } from '@/types/vat'; -import type { Account } from '@/types/accounting'; -import { AccountQuickPicker, VATCodePicker } from './AccountQuickPicker'; -import { - useBookingPreview, - useIsBookingSaving, - useSimpleBookingStore, -} from '@/stores/simpleBookingStore'; - -const { Text } = Typography; - -interface QuickBookModalProps { - accounts: Account[]; - onSubmit: (transaction: ReturnType) => Promise; -} - -export function QuickBookModal({ accounts, onSubmit }: QuickBookModalProps) { - const { modal, closeModal, setPreview } = useSimpleBookingStore(); - const preview = useBookingPreview(); - const isSaving = useIsBookingSaving(); - - const [selectedAccountId, setSelectedAccountId] = useState(); - const [selectedVATCode, setSelectedVATCode] = useState('K25'); - const [description, setDescription] = useState(''); - - const bankTransaction = modal.bankTransaction; - const isOpen = modal.isOpen && modal.type === 'simple'; - const isExpense = bankTransaction ? bankTransaction.amount < 0 : true; - - // Track previous open state to detect modal opening - const wasOpenRef = useRef(false); - const lastTransactionIdRef = useRef(null); - - // Reset form only when modal opens or transaction changes - useEffect(() => { - const isNewlyOpened = isOpen && !wasOpenRef.current; - const isNewTransaction = bankTransaction && bankTransaction.id !== lastTransactionIdRef.current; - - if (isOpen && bankTransaction && (isNewlyOpened || isNewTransaction)) { - // Calculate isExpense inside effect to avoid dependency - const expense = bankTransaction.amount < 0; - setSelectedAccountId(undefined); - setSelectedVATCode(expense ? 'K25' : 'S25'); - setDescription(bankTransaction.description); - setPreview(null); - lastTransactionIdRef.current = bankTransaction.id; - } - - wasOpenRef.current = isOpen; - }, [isOpen, bankTransaction, setPreview]); - - // Generate preview when inputs change - useEffect(() => { - if (!bankTransaction || !selectedAccountId) { - setPreview(null); - return; - } - - const account = accounts.find((a) => a.id === selectedAccountId); - if (!account) { - setPreview(null); - return; - } - - const input: SimpleBookingInput = { - bankTransaction: { - id: bankTransaction.id, - date: bankTransaction.date, - amount: bankTransaction.amount, - description: description || bankTransaction.description, - counterparty: bankTransaction.counterparty, - bankAccountId: bankTransaction.bankAccountId, - bankAccountNumber: bankTransaction.bankAccountNumber, - }, - contraAccountId: account.id, - contraAccountNumber: account.accountNumber, - contraAccountName: account.name, - vatCode: selectedVATCode, - description: description || undefined, - }; - - const result = generateSimpleDoubleEntry(input); - setPreview(result); - }, [bankTransaction, selectedAccountId, selectedVATCode, description, accounts, setPreview]); - - // Account options for picker - const accountOptions = useMemo( - () => - accounts.map((a) => ({ - id: a.id, - accountNumber: a.accountNumber, - name: a.name, - type: a.type, - })), - [accounts] - ); - - const handleAccountChange = (accountId: string) => { - setSelectedAccountId(accountId); - }; - - const handleSubmit = async () => { - if (!preview || !preview.isValid) return; - await onSubmit(preview); - closeModal(); - }; - - const previewColumns = [ - { - title: 'Konto', - key: 'account', - render: (_: unknown, record: { accountNumber: string; accountName: string }) => ( - - {record.accountNumber} - {record.accountName} - - ), - }, - { - title: 'Debet', - dataIndex: 'debit', - key: 'debit', - align: 'right' as const, - render: (value: number) => - value > 0 ? ( - {formatCurrency(value)} - ) : null, - }, - { - title: 'Kredit', - dataIndex: 'credit', - key: 'credit', - align: 'right' as const, - render: (value: number) => - value > 0 ? ( - {formatCurrency(value)} - ) : null, - }, - ]; - - if (!bankTransaction) return null; - - return ( - - {/* Bank transaction summary */} - -
- - {formatCurrency(bankTransaction.amount)} - -
- {bankTransaction.description} -
- {formatDateShort(bankTransaction.date)} - {bankTransaction.counterparty && ( - <> - - {bankTransaction.counterparty} - - )} -
-
- - {/* Account selection */} -
- - - - - - - - - - setDescription(e.target.value)} - placeholder={bankTransaction.description} - /> - -
- - {/* Preview */} - {preview && ( - <> - -
-
- Forhaandsvisning: - {preview.isValid ? ( - }> - Balancerer - - ) : ( - }> - Fejl - - )} -
- - {!preview.isValid && preview.validationMessage && ( - - )} - - ({ ...line, key: idx }))} - columns={previewColumns} - pagination={false} - size="small" - bordered - summary={() => { - const totalDebit = preview.lines.reduce((sum, l) => sum + l.debit, 0); - const totalCredit = preview.lines.reduce((sum, l) => sum + l.credit, 0); - return ( - - - Total - - - - {formatCurrency(totalDebit)} - - - - - {formatCurrency(totalCredit)} - - - - ); - }} - /> - - - )} - - ); -} - -export default QuickBookModal; diff --git a/frontend/src/components/simple-booking/SplitBookModal.tsx b/frontend/src/components/simple-booking/SplitBookModal.tsx deleted file mode 100644 index 3690f4d..0000000 --- a/frontend/src/components/simple-booking/SplitBookModal.tsx +++ /dev/null @@ -1,472 +0,0 @@ -// SplitBookModal - Modal for splitting one bank transaction to multiple accounts - -import { useState, useEffect, useMemo, useRef } from 'react'; -import { - Modal, - Form, - Input, - InputNumber, - Card, - Table, - Space, - Typography, - Alert, - Divider, - Tag, - Button, - Row, - Col, -} from 'antd'; -import { - CheckCircleOutlined, - WarningOutlined, - PlusOutlined, - DeleteOutlined, -} from '@ant-design/icons'; -import { formatCurrency, formatDateShort } from '@/lib/formatters'; -import { accountingColors } from '@/styles/theme'; -import { generateSplitDoubleEntry, type SplitBookingInput, type SplitBookingLine } from '@/lib/accounting'; -import type { VATCode } from '@/types/vat'; -import type { Account } from '@/types/accounting'; -import { AccountQuickPicker, VATCodePicker } from './AccountQuickPicker'; -import { - useBookingPreview, - useIsBookingSaving, - useSimpleBookingStore, -} from '@/stores/simpleBookingStore'; - -const { Text } = Typography; - -interface SplitBookModalProps { - accounts: Account[]; - onSubmit: (transaction: ReturnType) => Promise; -} - -export function SplitBookModal({ accounts, onSubmit }: SplitBookModalProps) { - const { - modal, - closeModal, - setPreview, - splitState, - addSplitLine, - removeSplitLine, - clearSplitLines, - } = useSimpleBookingStore(); - const preview = useBookingPreview(); - const isSaving = useIsBookingSaving(); - - // New line form state - const [newLineAccountId, setNewLineAccountId] = useState(); - const [newLineAmount, setNewLineAmount] = useState(null); - const [newLineVATCode, setNewLineVATCode] = useState('K25'); - const [newLineDescription, setNewLineDescription] = useState(''); - - const bankTransaction = modal.bankTransaction; - const isOpen = modal.isOpen && modal.type === 'split'; - const isExpense = bankTransaction ? bankTransaction.amount < 0 : true; - - // Track previous open state to detect modal opening - const wasOpenRef = useRef(false); - const lastTransactionIdRef = useRef(null); - - // Reset form only when modal opens or transaction changes - useEffect(() => { - const isNewlyOpened = isOpen && !wasOpenRef.current; - const isNewTransaction = bankTransaction && bankTransaction.id !== lastTransactionIdRef.current; - - if (isOpen && bankTransaction && (isNewlyOpened || isNewTransaction)) { - // Calculate isExpense inside effect to avoid dependency - const expense = bankTransaction.amount < 0; - clearSplitLines(); - setNewLineAccountId(undefined); - setNewLineAmount(null); - setNewLineVATCode(expense ? 'K25' : 'S25'); - setNewLineDescription(''); - setPreview(null); - lastTransactionIdRef.current = bankTransaction.id; - } - - wasOpenRef.current = isOpen; - }, [isOpen, bankTransaction, clearSplitLines, setPreview]); - - // Generate preview when lines change - useEffect(() => { - if (!bankTransaction || splitState.lines.length === 0) { - setPreview(null); - return; - } - - const input: SplitBookingInput = { - bankTransaction: { - id: bankTransaction.id, - date: bankTransaction.date, - amount: bankTransaction.amount, - description: bankTransaction.description, - counterparty: bankTransaction.counterparty, - bankAccountId: bankTransaction.bankAccountId, - bankAccountNumber: bankTransaction.bankAccountNumber, - }, - lines: splitState.lines, - }; - - const result = generateSplitDoubleEntry(input); - setPreview(result); - }, [bankTransaction, splitState.lines, setPreview]); - - // Account options for picker - const accountOptions = useMemo( - () => - accounts.map((a) => ({ - id: a.id, - accountNumber: a.accountNumber, - name: a.name, - type: a.type, - })), - [accounts] - ); - - // Add a new split line - const handleAddLine = () => { - if (!newLineAccountId || !newLineAmount) return; - - const account = accounts.find((a) => a.id === newLineAccountId); - if (!account) return; - - const newLine: SplitBookingLine = { - accountId: account.id, - accountNumber: account.accountNumber, - accountName: account.name, - amount: newLineAmount, - vatCode: newLineVATCode, - description: newLineDescription || undefined, - }; - - addSplitLine(newLine); - - // Reset form for next line - setNewLineAccountId(undefined); - setNewLineAmount(null); - setNewLineDescription(''); - }; - - // Fill remaining amount - const handleFillRemaining = () => { - setNewLineAmount(splitState.remainingAmount); - }; - - const handleSubmit = async () => { - if (!preview || !preview.isValid) return; - await onSubmit(preview); - closeModal(); - }; - - // Preview table columns - const previewColumns = [ - { - title: 'Konto', - key: 'account', - render: (_: unknown, record: { accountNumber: string; accountName: string }) => ( - - {record.accountNumber} - {record.accountName} - - ), - }, - { - title: 'Debet', - dataIndex: 'debit', - key: 'debit', - align: 'right' as const, - render: (value: number) => - value > 0 ? ( - {formatCurrency(value)} - ) : null, - }, - { - title: 'Kredit', - dataIndex: 'credit', - key: 'credit', - align: 'right' as const, - render: (value: number) => - value > 0 ? ( - {formatCurrency(value)} - ) : null, - }, - ]; - - // Split lines table columns - const splitLinesColumns = [ - { - title: '#', - key: 'index', - width: 40, - render: (_: unknown, __: unknown, index: number) => index + 1, - }, - { - title: 'Konto', - key: 'account', - render: (_: unknown, record: SplitBookingLine) => ( - - {record.accountNumber} - {record.accountName} - - ), - }, - { - title: 'Beloeb', - dataIndex: 'amount', - key: 'amount', - align: 'right' as const, - render: (value: number) => formatCurrency(value), - }, - { - title: 'Moms', - dataIndex: 'vatCode', - key: 'vatCode', - render: (code: VATCode) => {code}, - }, - { - title: '', - key: 'actions', - width: 50, - render: (_: unknown, __: unknown, index: number) => ( - - Banktransaktion: - - {formatCurrency(bankTransaction.amount)} - - {bankTransaction.description} - - - {formatDateShort(bankTransaction.date)} - - - - - {/* Remaining amount indicator */} - - Restbeloeb:{' '} - - {formatCurrency(splitState.remainingAmount)} - - {splitState.remainingAmount < 0.01 && ' - Fuld fordeling'} - - } - style={{ marginBottom: 16 }} - /> - - {/* Existing split lines */} - {splitState.lines.length > 0 && ( -
- - Fordeling: - -
({ ...line, key: idx }))} - columns={splitLinesColumns} - pagination={false} - size="small" - /> - - )} - - {/* Add new line form */} - {splitState.remainingAmount > 0.01 && ( - - - - - - - - - - - setNewLineAmount(value)} - min={0.01} - max={splitState.remainingAmount} - precision={2} - placeholder="0,00" - addonAfter="kr" - /> - - - - - - - - - - - - - - - - - - - setNewLineDescription(e.target.value)} - placeholder={bankTransaction.description} - /> - - - - - )} - - {/* Preview */} - {preview && preview.lines.length > 0 && ( - <> - -
-
- - Forhaandsvisning af bilag: - - {preview.isValid ? ( - }> - Balancerer - - ) : ( - }> - Fejl - - )} -
- - {!preview.isValid && preview.validationMessage && ( - - )} - -
({ ...line, key: idx }))} - columns={previewColumns} - pagination={false} - size="small" - bordered - summary={() => { - const totalDebit = preview.lines.reduce((sum, l) => sum + l.debit, 0); - const totalCredit = preview.lines.reduce((sum, l) => sum + l.credit, 0); - return ( - - - Total - - - - {formatCurrency(totalDebit)} - - - - - {formatCurrency(totalCredit)} - - - - ); - }} - /> -
- - Debet = Kredit{' '} - {preview.isValid ? ( - - ) : ( - - )} - -
- - - )} - - ); -} - -export default SplitBookModal; diff --git a/frontend/src/components/simple-booking/index.ts b/frontend/src/components/simple-booking/index.ts deleted file mode 100644 index 500bca2..0000000 --- a/frontend/src/components/simple-booking/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Simple Booking Components - Export barrel - -export { BankTransactionCard, BankTransactionList } from './BankTransactionCard'; -export { AccountQuickPicker, VATCodePicker } from './AccountQuickPicker'; -export { QuickBookModal } from './QuickBookModal'; -export { SplitBookModal } from './SplitBookModal'; diff --git a/frontend/src/lib/keyboardShortcuts.ts b/frontend/src/lib/keyboardShortcuts.ts new file mode 100644 index 0000000..8d1d331 --- /dev/null +++ b/frontend/src/lib/keyboardShortcuts.ts @@ -0,0 +1,327 @@ +/** + * Keyboard Shortcuts Registry and Utilities + * Central configuration for all keyboard shortcuts in the application + */ + +// Platform detection +export const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform); + +/** + * Modifier key display based on platform + */ +export const modifierKey = isMac ? '⌘' : 'Ctrl'; +export const altKey = isMac ? '⌥' : 'Alt'; +export const shiftKey = '⇧'; + +/** + * Shortcut definition + */ +export interface ShortcutDefinition { + id: string; + keys: string; // react-hotkeys-hook format: 'mod+k', 'mod+shift+n' + label: string; // Danish display name + description?: string; + category: ShortcutCategory; + scope?: ShortcutScope; +} + +export type ShortcutCategory = + | 'global' + | 'navigation' + | 'bogforing' + | 'faktura' + | 'bank' + | 'kunder' + | 'produkter'; + +export type ShortcutScope = + | 'global' // Works everywhere + | 'page' // Only on specific page + | 'modal' // Only in modals + | 'table'; // Only when table is focused + +/** + * All keyboard shortcuts in the application + */ +export const shortcuts: Record = { + // Global shortcuts + openCommandPalette: { + id: 'openCommandPalette', + keys: 'mod+k', + label: 'Kommandopalette', + description: 'Hurtig navigation og actions', + category: 'global', + scope: 'global', + }, + showShortcuts: { + id: 'showShortcuts', + keys: 'mod+/', + label: 'Vis genveje', + description: 'Vis alle tastaturgenveje', + category: 'global', + scope: 'global', + }, + closeModal: { + id: 'closeModal', + keys: 'escape', + label: 'Luk', + description: 'Luk aktiv modal eller palette', + category: 'global', + scope: 'global', + }, + + // Navigation shortcuts (G then X pattern) + goToDashboard: { + id: 'goToDashboard', + keys: 'g d', + label: 'Dashboard', + category: 'navigation', + scope: 'global', + }, + goToKassekladde: { + id: 'goToKassekladde', + keys: 'g k', + label: 'Kassekladde', + category: 'navigation', + scope: 'global', + }, + goToKontooversigt: { + id: 'goToKontooversigt', + keys: 'g o', + label: 'Kontooversigt', + category: 'navigation', + scope: 'global', + }, + goToFakturaer: { + id: 'goToFakturaer', + keys: 'g f', + label: 'Fakturaer', + category: 'navigation', + scope: 'global', + }, + goToKreditnotaer: { + id: 'goToKreditnotaer', + keys: 'g c', + label: 'Kreditnotaer', + category: 'navigation', + scope: 'global', + }, + goToBankafstemning: { + id: 'goToBankafstemning', + keys: 'g b', + label: 'Bankafstemning', + category: 'navigation', + scope: 'global', + }, + goToKunder: { + id: 'goToKunder', + keys: 'g u', + label: 'Kunder', + category: 'navigation', + scope: 'global', + }, + goToProdukter: { + id: 'goToProdukter', + keys: 'g p', + label: 'Produkter', + category: 'navigation', + scope: 'global', + }, + goToMomsindberetning: { + id: 'goToMomsindberetning', + keys: 'g m', + label: 'Momsindberetning', + category: 'navigation', + scope: 'global', + }, + goToIndstillinger: { + id: 'goToIndstillinger', + keys: 'g i', + label: 'Indstillinger', + category: 'navigation', + scope: 'global', + }, + + // Bogforing shortcuts (Kassekladde) + newDraft: { + id: 'newDraft', + keys: 'mod+n', + label: 'Ny kassekladde', + category: 'bogforing', + scope: 'page', + }, + postDraft: { + id: 'postDraft', + keys: 'mod+enter', + label: 'Bogfor kladde', + category: 'bogforing', + scope: 'page', + }, + addLine: { + id: 'addLine', + keys: 'mod+shift+l', + label: 'Tilføj linje', + category: 'bogforing', + scope: 'page', + }, + saveDraft: { + id: 'saveDraft', + keys: 'mod+s', + label: 'Gem kladde', + category: 'bogforing', + scope: 'page', + }, + discardDraft: { + id: 'discardDraft', + keys: 'mod+backspace', + label: 'Kasser kladde', + category: 'bogforing', + scope: 'page', + }, + + syncBank: { + id: 'syncBank', + keys: 'mod+r', + label: 'Synkroniser bank', + description: 'Hent nye transaktioner fra bank', + category: 'bank', + scope: 'page', + }, + + // Faktura shortcuts + newInvoice: { + id: 'newInvoice', + keys: 'mod+i', + label: 'Ny faktura', + category: 'faktura', + scope: 'page', + }, + newCreditNote: { + id: 'newCreditNote', + keys: 'mod+shift+i', + label: 'Ny kreditnota', + category: 'faktura', + scope: 'page', + }, + newCustomer: { + id: 'newCustomer', + keys: 'mod+shift+k', + label: 'Ny kunde', + category: 'kunder', + scope: 'page', + }, + newProduct: { + id: 'newProduct', + keys: 'mod+shift+p', + label: 'Nyt produkt', + category: 'produkter', + scope: 'page', + }, + + // Bank shortcuts + matchTransaction: { + id: 'matchTransaction', + keys: 'mod+m', + label: 'Match transaktion', + category: 'bank', + scope: 'page', + }, +}; + +/** + * Get shortcuts by category + */ +export function getShortcutsByCategory(category: ShortcutCategory): ShortcutDefinition[] { + return Object.values(shortcuts).filter(s => s.category === category); +} + +/** + * Get all shortcuts grouped by category + */ +export function getShortcutsGrouped(): Record { + const grouped: Record = { + global: [], + navigation: [], + bogforing: [], + faktura: [], + bank: [], + kunder: [], + produkter: [], + }; + + Object.values(shortcuts).forEach(shortcut => { + grouped[shortcut.category].push(shortcut); + }); + + return grouped; +} + +/** + * Format shortcut keys for display + * Converts 'mod+k' to '⌘K' on Mac or 'Ctrl+K' on Windows + */ +export function formatShortcut(keys: string): string { + return keys + .split(' ') + .map(part => + part + .replace(/mod/g, modifierKey) + .replace(/shift/g, shiftKey) + .replace(/alt/g, altKey) + .replace(/\+/g, '') + .toUpperCase() + ) + .join(' '); +} + +/** + * Format shortcut for tooltip display + * Returns formatted string like "⌘K" or "Ctrl+K" + */ +export function formatShortcutForTooltip(shortcutId: string): string | null { + const shortcut = shortcuts[shortcutId]; + if (!shortcut) return null; + return formatShortcut(shortcut.keys); +} + +/** + * Category display names in Danish + */ +export const categoryNames: Record = { + global: 'Globale', + navigation: 'Navigation', + bogforing: 'Bogforing', + faktura: 'Fakturering', + bank: 'Bank', + kunder: 'Kunder', + produkter: 'Produkter', +}; + +/** + * Navigation routes mapping + */ +export const navigationRoutes: Record = { + goToDashboard: '/', + goToKassekladde: '/kassekladde', + goToKontooversigt: '/kontooversigt', + goToFakturaer: '/fakturaer', + goToKreditnotaer: '/kreditnotaer', + goToBankafstemning: '/bankafstemning', + goToKunder: '/kunder', + goToProdukter: '/produkter', + goToMomsindberetning: '/momsindberetning', + goToIndstillinger: '/indstillinger', +}; + +/** + * Command palette items + */ +export interface CommandItem { + id: string; + label: string; + description?: string; + icon?: string; + shortcut?: string; + category: 'navigation' | 'action' | 'recent'; + action: () => void; +} diff --git a/frontend/src/pages/HurtigBogforing.tsx b/frontend/src/pages/HurtigBogforing.tsx deleted file mode 100644 index 9c0a372..0000000 --- a/frontend/src/pages/HurtigBogforing.tsx +++ /dev/null @@ -1,322 +0,0 @@ -// HurtigBogforing - Quick Booking Page for simple bank transaction processing - -import { useState, useEffect, useMemo } from 'react'; -import { - Card, - Row, - Col, - Select, - Typography, - Space, - Badge, - Statistic, - Divider, - Switch, - message, - Spin, -} from 'antd'; -import { - BankOutlined, - CheckCircleOutlined, - ClockCircleOutlined, - ThunderboltOutlined, -} from '@ant-design/icons'; -import { formatCurrency } from '@/lib/formatters'; -import { accountingColors } from '@/styles/theme'; -import type { Account, BankAccount } from '@/types/accounting'; -import type { GeneratedTransaction } from '@/lib/accounting'; -import { - BankTransactionList, - QuickBookModal, - SplitBookModal, -} from '@/components/simple-booking'; -import { - useSimpleBookingStore, - useUnbookedTransactions, - type PendingBankTransaction, -} from '@/stores/simpleBookingStore'; - -const { Title, Text } = Typography; - -// Mock data for development - replace with actual API calls -const MOCK_BANK_ACCOUNTS: BankAccount[] = [ - { - id: 'bank-1', - name: 'Hovedkonto', - accountNumber: '1000', - bankName: 'Danske Bank', - iban: 'DK12 3456 7890 1234 56', - currency: 'DKK', - balance: 125000, - lastSyncedAt: new Date().toISOString(), - isActive: true, - }, - { - id: 'bank-2', - name: 'Opsparingskonto', - accountNumber: '1010', - bankName: 'Danske Bank', - iban: 'DK12 3456 7890 1234 99', - currency: 'DKK', - balance: 50000, - lastSyncedAt: new Date().toISOString(), - isActive: true, - }, -]; - -const MOCK_ACCOUNTS: Account[] = [ - { id: 'acc-1', accountNumber: '1000', name: 'Bank', type: 'asset', balance: 125000 }, - { id: 'acc-2', accountNumber: '4000', name: 'Salg af varer', type: 'revenue', balance: 0 }, - { id: 'acc-3', accountNumber: '4100', name: 'Salg af ydelser', type: 'revenue', balance: 0 }, - { id: 'acc-4', accountNumber: '5000', name: 'Varekoeb', type: 'cogs', balance: 0 }, - { id: 'acc-5', accountNumber: '6100', name: 'Husleje', type: 'expense', balance: 0 }, - { id: 'acc-6', accountNumber: '6200', name: 'El og varme', type: 'expense', balance: 0 }, - { id: 'acc-7', accountNumber: '6300', name: 'Vedligeholdelse', type: 'expense', balance: 0 }, - { id: 'acc-8', accountNumber: '6400', name: 'Administration', type: 'expense', balance: 0 }, - { id: 'acc-9', accountNumber: '6500', name: 'IT og software', type: 'expense', balance: 0 }, - { id: 'acc-10', accountNumber: '6800', name: 'Kontorartikler', type: 'expense', balance: 0 }, - { id: 'acc-11', accountNumber: '7100', name: 'Loen', type: 'personnel', balance: 0 }, - { id: 'acc-12', accountNumber: '7200', name: 'ATP', type: 'personnel', balance: 0 }, - { id: 'acc-13', accountNumber: '8100', name: 'Renteindtaegter', type: 'financial', balance: 0 }, - { id: 'acc-14', accountNumber: '8200', name: 'Renteudgifter', type: 'financial', balance: 0 }, -]; - -const MOCK_PENDING_TRANSACTIONS: PendingBankTransaction[] = [ - { - id: 'tx-1', - date: '2025-01-15', - amount: -15000, - description: 'HUSLEJE JANUAR EJENDOM APS', - counterparty: 'Ejendom ApS', - bankAccountId: 'bank-1', - bankAccountNumber: '1000', - isBooked: false, - }, - { - id: 'tx-2', - date: '2025-01-14', - amount: -499, - description: 'MOBILEPAY STAPLES', - counterparty: 'Staples', - bankAccountId: 'bank-1', - bankAccountNumber: '1000', - isBooked: false, - }, - { - id: 'tx-3', - date: '2025-01-13', - amount: 25000, - description: 'OVERFOERSEL FRA KUNDE ABC APS', - counterparty: 'ABC ApS', - bankAccountId: 'bank-1', - bankAccountNumber: '1000', - isBooked: false, - }, - { - id: 'tx-4', - date: '2025-01-12', - amount: -10000, - description: 'SAMLET BETALING DIVERSE', - counterparty: 'Diverse leverandoerer', - bankAccountId: 'bank-1', - bankAccountNumber: '1000', - isBooked: false, - }, - { - id: 'tx-5', - date: '2025-01-11', - amount: -2500, - description: 'ELREGNING JANUAR', - counterparty: 'Andel Energi', - bankAccountId: 'bank-1', - bankAccountNumber: '1000', - isBooked: false, - }, - { - id: 'tx-6', - date: '2025-01-10', - amount: 12500, - description: 'FAKTURA 2025-001', - counterparty: 'XYZ Holding A/S', - bankAccountId: 'bank-1', - bankAccountNumber: '1000', - isBooked: true, - bookedAt: '2025-01-10T14:30:00Z', - transactionId: 'booked-tx-1', - }, -]; - -export function HurtigBogforing() { - const [selectedBankAccountId, setSelectedBankAccountId] = useState( - MOCK_BANK_ACCOUNTS[0]?.id - ); - const [showBooked, setShowBooked] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const { - setPendingTransactions, - openSimpleBooking, - openSplitBooking, - markAsBooked, - setSaving, - } = useSimpleBookingStore(); - - const unbookedTransactions = useUnbookedTransactions(); - - // Initialize with mock data - useEffect(() => { - setPendingTransactions(MOCK_PENDING_TRANSACTIONS); - }, [setPendingTransactions]); - - // Filter transactions by selected bank account - const filteredTransactions = useMemo(() => { - if (!selectedBankAccountId) return []; - return MOCK_PENDING_TRANSACTIONS.filter( - (tx) => tx.bankAccountId === selectedBankAccountId - ); - }, [selectedBankAccountId]); - - const selectedBankAccount = MOCK_BANK_ACCOUNTS.find( - (ba) => ba.id === selectedBankAccountId - ); - - // Stats - const unbookedCount = filteredTransactions.filter((tx) => !tx.isBooked).length; - const bookedCount = filteredTransactions.filter((tx) => tx.isBooked).length; - const totalUnbookedAmount = filteredTransactions - .filter((tx) => !tx.isBooked) - .reduce((sum, tx) => sum + tx.amount, 0); - - // Handle booking submission - const handleBookingSubmit = async (transaction: GeneratedTransaction) => { - setSaving(true); - try { - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 500)); - - // Mark as booked - markAsBooked(transaction.bankTransactionId, `tx-${Date.now()}`); - message.success('Transaktion bogfoert'); - } catch (error) { - message.error('Fejl ved bogfoering'); - } finally { - setSaving(false); - } - }; - - // Bank account selector options - const bankAccountOptions = MOCK_BANK_ACCOUNTS.map((ba) => ({ - value: ba.id, - label: ( - - - {ba.name} - ({ba.accountNumber}) - - ), - })); - - return ( -
- {/* Header */} -
- - <ThunderboltOutlined style={{ marginRight: 8 }} /> - Hurtig Bogfoering - - - Bogfoer banktransaktioner hurtigt med et enkelt klik - -
- - {/* Stats and controls */} - -
- - ( -