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 <noreply@anthropic.com>
This commit is contained in:
Nicolaj Hartmann 2026-01-30 14:18:05 +01:00
parent 1bacbea33b
commit f826794990
12 changed files with 851 additions and 1855 deletions

View file

@ -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"}

View file

@ -8,7 +8,6 @@ import {
TeamOutlined,
SettingOutlined,
FileTextOutlined,
ThunderboltOutlined,
UserOutlined,
BuildOutlined,
ShopOutlined,
@ -44,7 +43,6 @@ const menuItems: MenuItem[] = [
getItem('Dashboard', '/', <DashboardOutlined />),
getItem('Bogfoering', 'accounting', <BookOutlined />, [
getItem('Hurtig Bogfoering', '/hurtig-bogforing', <ThunderboltOutlined />),
getItem('Kassekladde', '/kassekladde', <FileTextOutlined />),
getItem('Kontooversigt', '/kontooversigt', <AccountBookOutlined />),
]),
@ -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') {

View file

@ -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<string, React.ReactNode> = {
goToDashboard: <DashboardOutlined />,
goToKassekladde: <BookOutlined />,
goToKontooversigt: <BookOutlined />,
goToFakturaer: <FileTextOutlined />,
goToKreditnotaer: <FileTextOutlined />,
goToBankafstemning: <BankOutlined />,
goToKunder: <TeamOutlined />,
goToProdukter: <ShopOutlined />,
goToMomsindberetning: <PercentageOutlined />,
goToIndstillinger: <SettingOutlined />,
};
// Action commands (not navigation)
const actionCommands: CommandItem[] = [
{
id: 'newDraft',
label: 'Ny kassekladde',
description: 'Opret en ny kassekladde',
icon: <PlusOutlined />,
shortcut: shortcuts.newDraft?.keys,
category: 'action',
route: '/kassekladde?action=new',
},
{
id: 'newInvoice',
label: 'Ny faktura',
description: 'Opret en ny faktura',
icon: <PlusOutlined />,
shortcut: shortcuts.newInvoice?.keys,
category: 'action',
route: '/fakturaer?action=new',
},
{
id: 'newCustomer',
label: 'Ny kunde',
description: 'Opret en ny kunde',
icon: <PlusOutlined />,
shortcut: shortcuts.newCustomer?.keys,
category: 'action',
route: '/kunder?action=new',
},
{
id: 'newProduct',
label: 'Nyt produkt',
description: 'Opret et nyt produkt',
icon: <PlusOutlined />,
shortcut: shortcuts.newProduct?.keys,
category: 'action',
route: '/produkter?action=new',
},
{
id: 'syncBank',
label: 'Synkroniser bank',
description: 'Hent nye transaktioner fra bank',
icon: <SyncOutlined />,
shortcut: shortcuts.syncBank?.keys,
category: 'action',
route: '/bankafstemning?action=sync',
},
{
id: 'exportSaft',
label: 'Eksporter SAF-T',
description: 'Eksporter regnskabsdata til SAF-T format',
icon: <ExportOutlined />,
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<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(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] || <DashboardOutlined />,
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 || <HistoryOutlined />,
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<string, CommandItem[]> = {};
filteredCommands.forEach((cmd) => {
if (!groups[cmd.category]) {
groups[cmd.category] = [];
}
groups[cmd.category].push(cmd);
});
return groups;
}, [filteredCommands]);
return (
<Modal
open={open}
onCancel={onClose}
footer={null}
closable={false}
centered
width={560}
styles={{
body: { padding: 0 },
content: { borderRadius: 12, overflow: 'hidden' },
}}
maskClosable
>
<div onKeyDown={handleKeyDown}>
{/* Search input */}
<div
style={{
padding: '12px 16px',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
}}
>
<Input
ref={inputRef as React.RefObject<any>}
placeholder="Søg efter sider eller handlinger..."
prefix={<SearchOutlined style={{ color: token.colorTextSecondary }} />}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
variant="borderless"
size="large"
style={{ fontSize: 16 }}
autoComplete="off"
/>
</div>
{/* Results list */}
<div
ref={listRef}
style={{
maxHeight: 400,
overflowY: 'auto',
padding: '8px 0',
}}
>
{filteredCommands.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="Ingen resultater"
style={{ padding: '32px 0' }}
/>
) : (
Object.entries(groupedCommands).map(([category, commands]) => (
<div key={category}>
{/* Category header */}
<div
style={{
padding: '8px 16px 4px',
fontSize: 12,
fontWeight: 500,
color: token.colorTextSecondary,
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
{getCategoryLabel(category)}
</div>
{/* Command items */}
{commands.map((command) => {
const globalIndex = filteredCommands.indexOf(command);
const isSelected = globalIndex === selectedIndex;
const isCurrent = command.route === currentPath;
return (
<div
key={`${command.category}-${command.id}`}
data-index={globalIndex}
onClick={() => 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 */}
<span
style={{
marginRight: 12,
fontSize: 16,
color: isSelected
? token.colorPrimary
: token.colorTextSecondary,
}}
>
{command.icon}
</span>
{/* Label and description */}
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<Text
strong={isSelected}
style={{
color: isSelected
? token.colorText
: token.colorTextSecondary,
}}
>
{command.label}
</Text>
{isCurrent && (
<Tag
color="blue"
style={{ margin: 0, fontSize: 10 }}
>
Aktuel
</Tag>
)}
</div>
{command.description && (
<Text
type="secondary"
style={{ fontSize: 12 }}
>
{command.description}
</Text>
)}
</div>
{/* Shortcut badge */}
{command.shortcut && (
<div
style={{
display: 'flex',
gap: 4,
}}
>
{formatShortcut(command.shortcut)
.split(' ')
.map((key, i) => (
<Tag
key={i}
style={{
margin: 0,
padding: '0 6px',
fontSize: 11,
fontFamily: 'monospace',
backgroundColor: token.colorFillTertiary,
border: `1px solid ${token.colorBorderSecondary}`,
borderRadius: 4,
}}
>
{key}
</Tag>
))}
</div>
)}
</div>
);
})}
</div>
))
)}
</div>
{/* Footer hint */}
<div
style={{
padding: '8px 16px',
borderTop: `1px solid ${token.colorBorderSecondary}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontSize: 12,
color: token.colorTextSecondary,
}}
>
<span>
<Tag style={{ margin: 0, marginRight: 4 }}></Tag>
Navigér
<Tag style={{ margin: '0 4px' }}></Tag>
Vælg
<Tag style={{ margin: '0 4px' }}>Esc</Tag>
Luk
</span>
</div>
</div>
</Modal>
);
}
export default CommandPalette;

View file

@ -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 (
<Button
key={fav.id}
size="small"
type={value === fav.accountId ? 'primary' : 'default'}
onClick={() => handleSelect(fav.accountId)}
style={{ marginRight: 4, marginBottom: 4 }}
>
{account.accountNumber} {account.name.substring(0, 12)}
{account.name.length > 12 ? '...' : ''}
</Button>
);
}).filter(Boolean);
// Select options
const selectOptions = filteredAccounts.map((account) => ({
value: account.id,
label: (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>
<Text strong style={{ marginRight: 8 }}>{account.accountNumber}</Text>
{account.name}
</span>
<span onClick={(e) => handleToggleFavorite(account, e)} style={{ cursor: 'pointer' }}>
{isFavorite(account.id) ? (
<StarFilled style={{ color: '#faad14' }} />
) : (
<StarOutlined style={{ color: '#d9d9d9' }} />
)}
</span>
</div>
),
searchValue: `${account.accountNumber} ${account.name}`,
}));
return (
<div className="account-quick-picker">
{/* Quick favorite buttons */}
{quickButtons.length > 0 && (
<div style={{ marginBottom: 8 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
Hurtige valg:
</Text>
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{quickButtons}
</div>
</div>
)}
{/* Full search select */}
<Select
showSearch
style={{ width: '100%' }}
placeholder={placeholder}
value={value}
onChange={handleSelect}
onSearch={setSearchValue}
filterOption={false}
options={selectOptions}
optionFilterProp="searchValue"
suffixIcon={<SearchOutlined />}
notFoundContent={
<div style={{ padding: '8px', textAlign: 'center' }}>
<Text type="secondary">Ingen konti fundet</Text>
</div>
}
/>
</div>
);
}
interface VATCodePickerProps {
value?: VATCode;
onChange: (code: VATCode) => void;
isExpense?: boolean;
showDescription?: boolean;
}
export function VATCodePicker({
value,
onChange,
isExpense = true,
showDescription = false,
}: VATCodePickerProps) {
// Get relevant VAT codes based on transaction type
const relevantCodes = useMemo(() => {
if (isExpense) {
// Input VAT codes for expenses
return ['K25', 'EU_VARE', 'EU_YDELSE', 'NONE'] as VATCode[];
} else {
// Output VAT codes for income
return ['S25', 'MOMSFRI', 'EKSPORT', 'NONE'] as VATCode[];
}
}, [isExpense]);
const options = relevantCodes.map((code) => {
const config = VAT_CODE_CONFIG[code];
return {
value: code,
label: (
<div>
<Text strong style={{ marginRight: 8 }}>{code}</Text>
{config.nameDanish}
{config.rate > 0 && (
<Tag size="small" style={{ marginLeft: 8 }}>
{(config.rate * 100).toFixed(0)}%
</Tag>
)}
</div>
),
};
});
const selectedConfig = value ? VAT_CODE_CONFIG[value] : null;
return (
<div className="vat-code-picker">
<Select
style={{ width: '100%' }}
placeholder="Vaelg momskode"
value={value}
onChange={onChange}
options={options}
/>
{showDescription && selectedConfig && (
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
{selectedConfig.description}
</Text>
)}
</div>
);
}
export default AccountQuickPicker;

View file

@ -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 (
<Card
size="small"
className={`bank-transaction-card ${isSelected ? 'selected' : ''} ${transaction.isBooked ? 'booked' : ''}`}
style={{
marginBottom: 8,
borderLeft: `3px solid ${isExpense ? accountingColors.debit : accountingColors.credit}`,
opacity: transaction.isBooked ? 0.6 : 1,
background: isSelected ? '#f0f5ff' : transaction.isBooked ? '#fafafa' : '#fff',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
{/* Left: Amount and description */}
<div style={{ flex: 1 }}>
<div style={{ marginBottom: 4 }}>
<Text
strong
style={{
fontSize: 16,
color: amountColor,
}}
>
{formatCurrency(transaction.amount)}
</Text>
</div>
<Text
style={{
display: 'block',
fontSize: 13,
color: '#262626',
lineHeight: 1.4,
}}
>
{transaction.description}
</Text>
<div style={{ marginTop: 6 }}>
<Space size={4}>
<Text type="secondary" style={{ fontSize: 12 }}>
{formatDateShort(transaction.date)}
</Text>
{transaction.counterparty && (
<>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
<Text type="secondary" style={{ fontSize: 12 }}>
{transaction.counterparty}
</Text>
</>
)}
</Space>
</div>
</div>
{/* Right: Actions */}
<div style={{ marginLeft: 16 }}>
{transaction.isBooked ? (
<Tag color="success" icon={<CheckOutlined />}>
Bogfoert
</Tag>
) : (
<Space>
<Tooltip title="Bogfoer">
<Button
type="primary"
size="small"
onClick={() => onBook(transaction)}
disabled={disabled}
>
Bogfoer
</Button>
</Tooltip>
<Tooltip title="Opdel paa flere konti">
<Button
size="small"
icon={<SplitCellsOutlined />}
onClick={() => onSplit(transaction)}
disabled={disabled}
>
Opdel...
</Button>
</Tooltip>
</Space>
)}
</div>
</div>
</Card>
);
}
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 (
<Card style={{ textAlign: 'center', padding: '24px 0' }}>
<BankOutlined style={{ fontSize: 32, color: '#bfbfbf', marginBottom: 8 }} />
<div>
<Text type="secondary">Ingen uboerte banktransaktioner</Text>
</div>
</Card>
);
}
return (
<div className="bank-transaction-list">
{filteredTransactions.map((transaction) => (
<BankTransactionCard
key={transaction.id}
transaction={transaction}
onBook={onBook}
onSplit={onSplit}
isSelected={transaction.id === selectedId}
disabled={disabled}
/>
))}
</div>
);
}
export default BankTransactionCard;

View file

@ -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<typeof generateSimpleDoubleEntry>) => Promise<void>;
}
export function QuickBookModal({ accounts, onSubmit }: QuickBookModalProps) {
const { modal, closeModal, setPreview } = useSimpleBookingStore();
const preview = useBookingPreview();
const isSaving = useIsBookingSaving();
const [selectedAccountId, setSelectedAccountId] = useState<string | undefined>();
const [selectedVATCode, setSelectedVATCode] = useState<VATCode>('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<string | null>(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 }) => (
<span>
<Text strong>{record.accountNumber}</Text>
<Text style={{ marginLeft: 8 }}>{record.accountName}</Text>
</span>
),
},
{
title: 'Debet',
dataIndex: 'debit',
key: 'debit',
align: 'right' as const,
render: (value: number) =>
value > 0 ? (
<Text style={{ color: accountingColors.debit }}>{formatCurrency(value)}</Text>
) : null,
},
{
title: 'Kredit',
dataIndex: 'credit',
key: 'credit',
align: 'right' as const,
render: (value: number) =>
value > 0 ? (
<Text style={{ color: accountingColors.credit }}>{formatCurrency(value)}</Text>
) : null,
},
];
if (!bankTransaction) return null;
return (
<Modal
title="Bogfoer transaktion"
open={isOpen}
onCancel={closeModal}
onOk={handleSubmit}
okText="Bogfoer"
cancelText="Annuller"
okButtonProps={{
disabled: !preview?.isValid || isSaving,
loading: isSaving,
}}
width={600}
destroyOnClose
>
{/* Bank transaction summary */}
<Card
size="small"
style={{
marginBottom: 16,
borderLeft: `3px solid ${isExpense ? accountingColors.debit : accountingColors.credit}`,
}}
>
<div>
<Text
strong
style={{
fontSize: 18,
color: isExpense ? accountingColors.debit : accountingColors.credit,
}}
>
{formatCurrency(bankTransaction.amount)}
</Text>
</div>
<Text>{bankTransaction.description}</Text>
<div style={{ marginTop: 4 }}>
<Text type="secondary">{formatDateShort(bankTransaction.date)}</Text>
{bankTransaction.counterparty && (
<>
<Text type="secondary"> </Text>
<Text type="secondary">{bankTransaction.counterparty}</Text>
</>
)}
</div>
</Card>
{/* Account selection */}
<Form layout="vertical">
<Form.Item label="Vaelg konto" required>
<AccountQuickPicker
accounts={accountOptions}
value={selectedAccountId}
onChange={handleAccountChange}
isExpense={isExpense}
placeholder="Soeg efter konto..."
/>
</Form.Item>
<Form.Item label="Momskode" required>
<VATCodePicker
value={selectedVATCode}
onChange={setSelectedVATCode}
isExpense={isExpense}
showDescription
/>
</Form.Item>
<Form.Item label="Beskrivelse (valgfri)">
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={bankTransaction.description}
/>
</Form.Item>
</Form>
{/* Preview */}
{preview && (
<>
<Divider style={{ margin: '16px 0' }} />
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
<Text strong style={{ marginRight: 8 }}>Forhaandsvisning:</Text>
{preview.isValid ? (
<Tag color="success" icon={<CheckCircleOutlined />}>
Balancerer
</Tag>
) : (
<Tag color="error" icon={<WarningOutlined />}>
Fejl
</Tag>
)}
</div>
{!preview.isValid && preview.validationMessage && (
<Alert
type="error"
message={preview.validationMessage}
style={{ marginBottom: 8 }}
/>
)}
<Table
dataSource={preview.lines.map((line, idx) => ({ ...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 (
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Text strong>Total</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={1} align="right">
<Text strong style={{ color: accountingColors.debit }}>
{formatCurrency(totalDebit)}
</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} align="right">
<Text strong style={{ color: accountingColors.credit }}>
{formatCurrency(totalCredit)}
</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
);
}}
/>
</div>
</>
)}
</Modal>
);
}
export default QuickBookModal;

View file

@ -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<typeof generateSplitDoubleEntry>) => Promise<void>;
}
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<string | undefined>();
const [newLineAmount, setNewLineAmount] = useState<number | null>(null);
const [newLineVATCode, setNewLineVATCode] = useState<VATCode>('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<string | null>(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 }) => (
<span>
<Text strong>{record.accountNumber}</Text>
<Text style={{ marginLeft: 8 }}>{record.accountName}</Text>
</span>
),
},
{
title: 'Debet',
dataIndex: 'debit',
key: 'debit',
align: 'right' as const,
render: (value: number) =>
value > 0 ? (
<Text style={{ color: accountingColors.debit }}>{formatCurrency(value)}</Text>
) : null,
},
{
title: 'Kredit',
dataIndex: 'credit',
key: 'credit',
align: 'right' as const,
render: (value: number) =>
value > 0 ? (
<Text style={{ color: accountingColors.credit }}>{formatCurrency(value)}</Text>
) : 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) => (
<span>
<Text strong>{record.accountNumber}</Text>
<Text style={{ marginLeft: 8 }}>{record.accountName}</Text>
</span>
),
},
{
title: 'Beloeb',
dataIndex: 'amount',
key: 'amount',
align: 'right' as const,
render: (value: number) => formatCurrency(value),
},
{
title: 'Moms',
dataIndex: 'vatCode',
key: 'vatCode',
render: (code: VATCode) => <Tag>{code}</Tag>,
},
{
title: '',
key: 'actions',
width: 50,
render: (_: unknown, __: unknown, index: number) => (
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => removeSplitLine(index)}
/>
),
},
];
if (!bankTransaction) return null;
const canAddLine =
newLineAccountId &&
newLineAmount &&
newLineAmount > 0 &&
newLineAmount <= splitState.remainingAmount + 0.01;
const canSubmit =
preview?.isValid && splitState.lines.length > 0 && splitState.remainingAmount < 0.01;
return (
<Modal
title="Opdel transaktion"
open={isOpen}
onCancel={closeModal}
onOk={handleSubmit}
okText="Bogfoer opdeling"
cancelText="Annuller"
okButtonProps={{
disabled: !canSubmit || isSaving,
loading: isSaving,
}}
width={800}
destroyOnClose
>
{/* Bank transaction summary */}
<Card
size="small"
style={{
marginBottom: 16,
borderLeft: `3px solid ${isExpense ? accountingColors.debit : accountingColors.credit}`,
}}
>
<Row justify="space-between" align="middle">
<Col>
<Text>Banktransaktion:</Text>
<Text
strong
style={{
fontSize: 18,
marginLeft: 8,
color: isExpense ? accountingColors.debit : accountingColors.credit,
}}
>
{formatCurrency(bankTransaction.amount)}
</Text>
<Text style={{ marginLeft: 16 }}>{bankTransaction.description}</Text>
</Col>
<Col>
<Text type="secondary">{formatDateShort(bankTransaction.date)}</Text>
</Col>
</Row>
</Card>
{/* Remaining amount indicator */}
<Alert
type={splitState.remainingAmount < 0.01 ? 'success' : 'warning'}
message={
<span>
Restbeloeb:{' '}
<Text strong style={{ color: splitState.remainingAmount < 0.01 ? accountingColors.credit : accountingColors.warning }}>
{formatCurrency(splitState.remainingAmount)}
</Text>
{splitState.remainingAmount < 0.01 && ' - Fuld fordeling'}
</span>
}
style={{ marginBottom: 16 }}
/>
{/* Existing split lines */}
{splitState.lines.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Text strong style={{ display: 'block', marginBottom: 8 }}>
Fordeling:
</Text>
<Table
dataSource={splitState.lines.map((line, idx) => ({ ...line, key: idx }))}
columns={splitLinesColumns}
pagination={false}
size="small"
/>
</div>
)}
{/* Add new line form */}
{splitState.remainingAmount > 0.01 && (
<Card size="small" title="Tilfoej linje" style={{ marginBottom: 16 }}>
<Row gutter={16}>
<Col span={10}>
<Form.Item label="Konto" style={{ marginBottom: 8 }}>
<AccountQuickPicker
accounts={accountOptions}
value={newLineAccountId}
onChange={setNewLineAccountId}
isExpense={isExpense}
placeholder="Vaelg konto..."
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Beloeb" style={{ marginBottom: 8 }}>
<Space.Compact style={{ width: '100%' }}>
<InputNumber
style={{ width: '100%' }}
value={newLineAmount}
onChange={(value) => setNewLineAmount(value)}
min={0.01}
max={splitState.remainingAmount}
precision={2}
placeholder="0,00"
addonAfter="kr"
/>
<Button onClick={handleFillRemaining} title="Udfyld resten">
Rest
</Button>
</Space.Compact>
</Form.Item>
</Col>
<Col span={5}>
<Form.Item label="Moms" style={{ marginBottom: 8 }}>
<VATCodePicker
value={newLineVATCode}
onChange={setNewLineVATCode}
isExpense={isExpense}
/>
</Form.Item>
</Col>
<Col span={3}>
<Form.Item label=" " style={{ marginBottom: 8 }}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddLine}
disabled={!canAddLine}
>
Tilfoej
</Button>
</Form.Item>
</Col>
</Row>
<Row>
<Col span={24}>
<Form.Item label="Beskrivelse (valgfri)" style={{ marginBottom: 0 }}>
<Input
value={newLineDescription}
onChange={(e) => setNewLineDescription(e.target.value)}
placeholder={bankTransaction.description}
/>
</Form.Item>
</Col>
</Row>
</Card>
)}
{/* Preview */}
{preview && preview.lines.length > 0 && (
<>
<Divider style={{ margin: '16px 0' }} />
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
<Text strong style={{ marginRight: 8 }}>
Forhaandsvisning af bilag:
</Text>
{preview.isValid ? (
<Tag color="success" icon={<CheckCircleOutlined />}>
Balancerer
</Tag>
) : (
<Tag color="error" icon={<WarningOutlined />}>
Fejl
</Tag>
)}
</div>
{!preview.isValid && preview.validationMessage && (
<Alert
type="error"
message={preview.validationMessage}
style={{ marginBottom: 8 }}
/>
)}
<Table
dataSource={preview.lines.map((line, idx) => ({ ...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 (
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Text strong>Total</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={1} align="right">
<Text strong style={{ color: accountingColors.debit }}>
{formatCurrency(totalDebit)}
</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} align="right">
<Text strong style={{ color: accountingColors.credit }}>
{formatCurrency(totalCredit)}
</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
);
}}
/>
<div style={{ marginTop: 8, textAlign: 'right' }}>
<Text type="secondary">
Debet = Kredit{' '}
{preview.isValid ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<WarningOutlined style={{ color: '#ff4d4f' }} />
)}
</Text>
</div>
</div>
</>
)}
</Modal>
);
}
export default SplitBookModal;

View file

@ -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';

View file

@ -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<string, ShortcutDefinition> = {
// 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<ShortcutCategory, ShortcutDefinition[]> {
const grouped: Record<ShortcutCategory, ShortcutDefinition[]> = {
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<ShortcutCategory, string> = {
global: 'Globale',
navigation: 'Navigation',
bogforing: 'Bogforing',
faktura: 'Fakturering',
bank: 'Bank',
kunder: 'Kunder',
produkter: 'Produkter',
};
/**
* Navigation routes mapping
*/
export const navigationRoutes: Record<string, string> = {
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;
}

View file

@ -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<string | undefined>(
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: (
<Space>
<BankOutlined />
<span>{ba.name}</span>
<Text type="secondary">({ba.accountNumber})</Text>
</Space>
),
}));
return (
<div className="hurtig-bogforing-page">
{/* Header */}
<div style={{ marginBottom: 24 }}>
<Title level={3} style={{ marginBottom: 8 }}>
<ThunderboltOutlined style={{ marginRight: 8 }} />
Hurtig Bogfoering
</Title>
<Text type="secondary">
Bogfoer banktransaktioner hurtigt med et enkelt klik
</Text>
</div>
{/* Stats and controls */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card size="small">
<Statistic
title="Bankkonto"
valueRender={() => (
<Select
style={{ width: '100%' }}
value={selectedBankAccountId}
onChange={setSelectedBankAccountId}
options={bankAccountOptions}
/>
)}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="Ubogfoerte transaktioner"
value={unbookedCount}
prefix={<ClockCircleOutlined />}
valueStyle={{ color: unbookedCount > 0 ? accountingColors.warning : accountingColors.credit }}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="Bogfoerte transaktioner"
value={bookedCount}
prefix={<CheckCircleOutlined />}
valueStyle={{ color: accountingColors.credit }}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="Ubogfoert beloeb"
value={formatCurrency(totalUnbookedAmount)}
valueStyle={{ color: totalUnbookedAmount < 0 ? accountingColors.debit : accountingColors.credit }}
/>
</Card>
</Col>
</Row>
{/* Bank account info */}
{selectedBankAccount && (
<Card size="small" style={{ marginBottom: 16 }}>
<Row justify="space-between" align="middle">
<Col>
<Space>
<BankOutlined style={{ fontSize: 20 }} />
<div>
<Text strong>{selectedBankAccount.name}</Text>
<Text type="secondary" style={{ marginLeft: 8 }}>
{selectedBankAccount.bankName} {selectedBankAccount.iban}
</Text>
</div>
</Space>
</Col>
<Col>
<Space>
<Text>Vis boerte:</Text>
<Switch checked={showBooked} onChange={setShowBooked} />
</Space>
</Col>
</Row>
</Card>
)}
{/* Transaction list */}
<Spin spinning={isLoading}>
<BankTransactionList
transactions={filteredTransactions}
onBook={openSimpleBooking}
onSplit={openSplitBooking}
showBooked={showBooked}
/>
</Spin>
{/* Modals */}
<QuickBookModal accounts={MOCK_ACCOUNTS} onSubmit={handleBookingSubmit} />
<SplitBookModal accounts={MOCK_ACCOUNTS} onSubmit={handleBookingSubmit} />
</div>
);
}
export default HurtigBogforing;

View file

@ -5,17 +5,28 @@ import { Spin } from 'antd';
// Lazy load pages for code splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Kassekladde = lazy(() => import('./pages/Kassekladde'));
const HurtigBogforing = lazy(() => import('./pages/HurtigBogforing'));
const Kontooversigt = lazy(() => import('./pages/Kontooversigt'));
const Bankafstemning = lazy(() => import('./pages/Bankafstemning'));
const Momsindberetning = lazy(() => import('./pages/Momsindberetning'));
const Loenforstaelse = lazy(() => import('./pages/Loenforstaelse'));
const Eksport = lazy(() => import('./pages/Eksport'));
const Settings = lazy(() => import('./pages/Settings'));
const UserSettings = lazy(() => import('./pages/UserSettings'));
const Admin = lazy(() => import('./pages/Admin'));
// Invoicing pages
const Kunder = lazy(() => import('./pages/Kunder'));
const Produkter = lazy(() => import('./pages/Produkter'));
const Fakturaer = lazy(() => import('./pages/Fakturaer'));
const Kreditnotaer = lazy(() => import('./pages/Kreditnotaer'));
const Ordrer = lazy(() => import('./pages/Ordrer'));
// Loading fallback component
function PageLoader() {
return (
<div
<Spin
size="large"
tip="Indlæser..."
style={{
display: 'flex',
justifyContent: 'center',
@ -24,8 +35,8 @@ function PageLoader() {
minHeight: 400,
}}
>
<Spin size="large" tip="Indlaeser..." />
</div>
<div style={{ minHeight: 200 }} />
</Spin>
);
}
@ -38,18 +49,29 @@ export default function AppRoutes() {
{/* Accounting */}
<Route path="/kassekladde" element={<Kassekladde />} />
<Route path="/hurtig-bogforing" element={<HurtigBogforing />} />
<Route path="/kontooversigt" element={<Kontooversigt />} />
{/* Bank */}
<Route path="/bankafstemning" element={<Bankafstemning />} />
{/* Invoicing */}
<Route path="/kunder" element={<Kunder />} />
<Route path="/produkter" element={<Produkter />} />
<Route path="/ordrer" element={<Ordrer />} />
<Route path="/fakturaer" element={<Fakturaer />} />
<Route path="/kreditnotaer" element={<Kreditnotaer />} />
{/* Reporting */}
<Route path="/momsindberetning" element={<Momsindberetning />} />
<Route path="/loenforstaelse" element={<Loenforstaelse />} />
<Route path="/eksport" element={<Eksport />} />
{/* Settings */}
<Route path="/indstillinger" element={<Settings />} />
<Route path="/profil" element={<UserSettings />} />
{/* Admin */}
<Route path="/admin" element={<Admin />} />
{/* Fallback redirect */}
<Route path="*" element={<Navigate to="/" replace />} />

View file

@ -1,364 +0,0 @@
// Simple Booking Store - Zustand store for quick/simple booking workflow
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { VATCode } from '@/types/vat';
import type {
BankTransactionInput,
GeneratedTransaction,
SplitBookingLine,
} from '@/lib/accounting';
// =====================================================
// TYPES
// =====================================================
/**
* Bank transaction with booking status
*/
export interface PendingBankTransaction extends BankTransactionInput {
isBooked: boolean;
bookedAt?: string;
transactionId?: string; // Reference to created accounting transaction
}
/**
* Favorite/quick account for fast selection
*/
export interface FavoriteAccount {
id: string;
accountId: string;
accountNumber: string;
accountName: string;
defaultVATCode: VATCode;
usageCount: number;
lastUsed?: string;
}
/**
* Booking modal state
*/
export interface BookingModalState {
isOpen: boolean;
type: 'simple' | 'split' | null;
bankTransaction: PendingBankTransaction | null;
}
/**
* Split booking working state
*/
export interface SplitBookingState {
lines: SplitBookingLine[];
remainingAmount: number;
}
// =====================================================
// STORE STATE
// =====================================================
interface SimpleBookingState {
// Bank transactions
pendingTransactions: PendingBankTransaction[];
selectedBankAccountId: string | null;
// Favorites
favoriteAccounts: FavoriteAccount[];
// Modal state
modal: BookingModalState;
// Split booking state
splitState: SplitBookingState;
// Preview
preview: GeneratedTransaction | null;
// Loading states
isLoading: boolean;
isSaving: boolean;
// Actions - Bank Transactions
setPendingTransactions: (transactions: PendingBankTransaction[]) => void;
markAsBooked: (transactionId: string, accountingTransactionId: string) => void;
setSelectedBankAccount: (accountId: string | null) => void;
// Actions - Favorites
addFavoriteAccount: (account: Omit<FavoriteAccount, 'id' | 'usageCount'>) => void;
removeFavoriteAccount: (accountId: string) => void;
incrementFavoriteUsage: (accountId: string) => void;
updateFavoriteVATCode: (accountId: string, vatCode: VATCode) => void;
// Actions - Modal
openSimpleBooking: (transaction: PendingBankTransaction) => void;
openSplitBooking: (transaction: PendingBankTransaction) => void;
closeModal: () => void;
// Actions - Split Booking
addSplitLine: (line: SplitBookingLine) => void;
updateSplitLine: (index: number, line: Partial<SplitBookingLine>) => void;
removeSplitLine: (index: number) => void;
clearSplitLines: () => void;
// Actions - Preview
setPreview: (preview: GeneratedTransaction | null) => void;
// Actions - Loading
setLoading: (loading: boolean) => void;
setSaving: (saving: boolean) => void;
// Computed
getUnbookedTransactions: () => PendingBankTransaction[];
getTopFavorites: (limit?: number) => FavoriteAccount[];
getSplitRemainingAmount: () => number;
// Reset
resetStore: () => void;
}
// =====================================================
// INITIAL STATE
// =====================================================
const initialModalState: BookingModalState = {
isOpen: false,
type: null,
bankTransaction: null,
};
const initialSplitState: SplitBookingState = {
lines: [],
remainingAmount: 0,
};
const initialState = {
pendingTransactions: [],
selectedBankAccountId: null,
favoriteAccounts: [],
modal: initialModalState,
splitState: initialSplitState,
preview: null,
isLoading: false,
isSaving: false,
};
// =====================================================
// STORE IMPLEMENTATION
// =====================================================
export const useSimpleBookingStore = create<SimpleBookingState>()(
persist(
(set, get) => ({
...initialState,
// Bank Transactions
setPendingTransactions: (transactions) =>
set({ pendingTransactions: transactions }),
markAsBooked: (transactionId, accountingTransactionId) =>
set((state) => ({
pendingTransactions: state.pendingTransactions.map((tx) =>
tx.id === transactionId
? {
...tx,
isBooked: true,
bookedAt: new Date().toISOString(),
transactionId: accountingTransactionId,
}
: tx
),
})),
setSelectedBankAccount: (accountId) =>
set({ selectedBankAccountId: accountId }),
// Favorites
addFavoriteAccount: (account) =>
set((state) => ({
favoriteAccounts: [
...state.favoriteAccounts,
{
...account,
id: `fav-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
usageCount: 0,
},
],
})),
removeFavoriteAccount: (accountId) =>
set((state) => ({
favoriteAccounts: state.favoriteAccounts.filter((f) => f.accountId !== accountId),
})),
incrementFavoriteUsage: (accountId) =>
set((state) => ({
favoriteAccounts: state.favoriteAccounts.map((f) =>
f.accountId === accountId
? {
...f,
usageCount: f.usageCount + 1,
lastUsed: new Date().toISOString(),
}
: f
),
})),
updateFavoriteVATCode: (accountId, vatCode) =>
set((state) => ({
favoriteAccounts: state.favoriteAccounts.map((f) =>
f.accountId === accountId ? { ...f, defaultVATCode: vatCode } : f
),
})),
// Modal
openSimpleBooking: (transaction) =>
set({
modal: {
isOpen: true,
type: 'simple',
bankTransaction: transaction,
},
preview: null,
}),
openSplitBooking: (transaction) =>
set({
modal: {
isOpen: true,
type: 'split',
bankTransaction: transaction,
},
splitState: {
lines: [],
remainingAmount: Math.abs(transaction.amount),
},
preview: null,
}),
closeModal: () =>
set({
modal: initialModalState,
splitState: initialSplitState,
preview: null,
}),
// Split Booking
addSplitLine: (line) =>
set((state) => {
const newLines = [...state.splitState.lines, line];
const totalAllocated = newLines.reduce((sum, l) => sum + Math.abs(l.amount), 0);
const totalAmount = state.modal.bankTransaction
? Math.abs(state.modal.bankTransaction.amount)
: 0;
return {
splitState: {
lines: newLines,
remainingAmount: Math.max(0, totalAmount - totalAllocated),
},
};
}),
updateSplitLine: (index, updates) =>
set((state) => {
const newLines = [...state.splitState.lines];
if (index >= 0 && index < newLines.length) {
newLines[index] = { ...newLines[index], ...updates };
}
const totalAllocated = newLines.reduce((sum, l) => sum + Math.abs(l.amount), 0);
const totalAmount = state.modal.bankTransaction
? Math.abs(state.modal.bankTransaction.amount)
: 0;
return {
splitState: {
lines: newLines,
remainingAmount: Math.max(0, totalAmount - totalAllocated),
},
};
}),
removeSplitLine: (index) =>
set((state) => {
const newLines = state.splitState.lines.filter((_, i) => i !== index);
const totalAllocated = newLines.reduce((sum, l) => sum + Math.abs(l.amount), 0);
const totalAmount = state.modal.bankTransaction
? Math.abs(state.modal.bankTransaction.amount)
: 0;
return {
splitState: {
lines: newLines,
remainingAmount: Math.max(0, totalAmount - totalAllocated),
},
};
}),
clearSplitLines: () =>
set((state) => ({
splitState: {
lines: [],
remainingAmount: state.modal.bankTransaction
? Math.abs(state.modal.bankTransaction.amount)
: 0,
},
})),
// Preview
setPreview: (preview) => set({ preview }),
// Loading
setLoading: (isLoading) => set({ isLoading }),
setSaving: (isSaving) => set({ isSaving }),
// Computed
getUnbookedTransactions: () =>
get().pendingTransactions.filter((tx) => !tx.isBooked),
getTopFavorites: (limit = 5) =>
[...get().favoriteAccounts]
.sort((a, b) => b.usageCount - a.usageCount)
.slice(0, limit),
getSplitRemainingAmount: () => get().splitState.remainingAmount,
// Reset
resetStore: () => set(initialState),
}),
{
name: 'simple-booking-favorites',
// Only persist favoriteAccounts - transient state like modal/preview should not be persisted
partialize: (state) => ({
favoriteAccounts: state.favoriteAccounts,
}),
}
)
);
// =====================================================
// SELECTOR HOOKS
// =====================================================
export const usePendingTransactions = () =>
useSimpleBookingStore((state) => state.pendingTransactions);
export const useUnbookedTransactions = () =>
useSimpleBookingStore((state) => state.getUnbookedTransactions());
export const useFavoriteAccounts = () =>
useSimpleBookingStore((state) => state.favoriteAccounts);
export const useTopFavorites = (limit?: number) =>
useSimpleBookingStore((state) => state.getTopFavorites(limit));
export const useBookingModal = () =>
useSimpleBookingStore((state) => state.modal);
export const useSplitState = () =>
useSimpleBookingStore((state) => state.splitState);
export const useBookingPreview = () =>
useSimpleBookingStore((state) => state.preview);
export const useIsBookingSaving = () =>
useSimpleBookingStore((state) => state.isSaving);