Add frontend components, API mutations, and project config
Frontend:
- API mutations for accounts, bank connections, customers, invoices
- Document processing API
- Shared components (PageHeader, EmptyState, etc.)
- Pages: Admin, Fakturaer, Kunder, Ordrer, Produkter, etc.
- Hooks and stores
Config:
- CLAUDE.md project instructions
- Beads issue tracking config
- Git attributes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:20:03 +01:00
|
|
|
import { useState, useMemo } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Typography,
|
|
|
|
|
Button,
|
|
|
|
|
Card,
|
|
|
|
|
Table,
|
|
|
|
|
Space,
|
|
|
|
|
Tag,
|
|
|
|
|
Modal,
|
|
|
|
|
Form,
|
|
|
|
|
Input,
|
|
|
|
|
Select,
|
|
|
|
|
InputNumber,
|
Audit v2: fix security, data integrity, compliance, bugs, encoding, UX
Backend Security & Data Integrity:
- Block negative debit/credit amounts that bypass balance validation
- Require document date at posting (was optional, bypassing fiscal year checks)
- Fix event sourcing anti-pattern: timestamps now stored in events, not UtcNow in Apply
- Add [Authorize] to BankingController OAuth callback
- Add company access check on attachment downloads
- Validate CVR in CompanyAggregate.Create and CompanyAggregate.Update
- Require company CVR for invoice creation (Momsloven §52)
- Delete leftover WeatherForecastController
- Fix duplicate migration number 007 (renamed to 007b)
- Remove dead code in VatCalculationService (identical if/else branches)
Accounting Compliance:
- Add missing VAT accounts to StandardDanishAccounts (5610, 5611, 5620)
- Populate SAF-T TaxInformation on transaction lines (was always null)
- Add AuditFileCountry and TaxRegistrationNumber to SAF-T header
Critical Frontend Bugs:
- Fix Dashboard <a href> causing full page reloads (now uses React Router Link)
- Wire Kassekladde filters to actual data (account, status, date range)
- Pre-populate form when editing existing Kassekladde drafts
- Add detail drawer for "Vis detaljer" action (was just a toast)
- Toggle advanced filters with "Flere filtre" button
- CloseFiscalYearWizard now actually posts closing entries via mutations
- "Create next year" checkbox now creates the next fiscal year
Danish Character Encoding (~50 fixes):
- Fix ø/æ/å across Momsindberetning, DocumentUploadModal, Bankafstemning,
Kontooversigt, CloseFiscalYearWizard, vatCodes, periodStore, periods,
accounting, types/periods
Dead Buttons & UX:
- Disable Momsindberetning PDF/Export buttons with tooltips
- FiscalYearSelector "Administrer" now navigates to Settings
- Settings bank tab now uses real BankConnectionsTab component
- Bankafstemning save button disabled with development tooltip
- Replace hardcoded account options with real API data (Bankafstemning, Fakturaer)
- Header help button shows info message, notification bell shows popover
Consistency & Quality:
- Remove 7 console.log statements from production code
- Adopt PageHeader on 6 remaining pages (Kreditnotaer, Settings, Admin, etc.)
- Standardize loading states to Skeleton pattern (5 pages)
- Replace deprecated bodyStyle prop on Ant Design Cards
- Standardize date format to DD-MM-YYYY
- Fix sidebar width mismatch in designTokens
- Fix Kontooversigt breadcrumb pointing to non-existent route
Accessibility:
- Add aria-label to sidebar navigation
- Add +/- prefix to AmountText for color-blind users
- Fix CompanySwitcher permanent skeleton when no companies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 00:18:19 +01:00
|
|
|
Skeleton,
|
Add frontend components, API mutations, and project config
Frontend:
- API mutations for accounts, bank connections, customers, invoices
- Document processing API
- Shared components (PageHeader, EmptyState, etc.)
- Pages: Admin, Fakturaer, Kunder, Ordrer, Produkter, etc.
- Hooks and stores
Config:
- CLAUDE.md project instructions
- Beads issue tracking config
- Git attributes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:20:03 +01:00
|
|
|
Alert,
|
|
|
|
|
Drawer,
|
|
|
|
|
Descriptions,
|
|
|
|
|
Popconfirm,
|
|
|
|
|
Row,
|
|
|
|
|
Col,
|
|
|
|
|
Statistic,
|
|
|
|
|
DatePicker,
|
|
|
|
|
Divider,
|
|
|
|
|
List,
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import { showSuccess, showError, showWarning } from '@/lib/errorHandling';
|
|
|
|
|
import {
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
EditOutlined,
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
SendOutlined,
|
|
|
|
|
StopOutlined,
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
LinkOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
|
import { useCompany } from '@/hooks/useCompany';
|
|
|
|
|
import { useCurrentFiscalYear } from '@/stores/periodStore';
|
|
|
|
|
import {
|
|
|
|
|
useCreditNotes,
|
|
|
|
|
useInvoices,
|
|
|
|
|
type Invoice,
|
|
|
|
|
type InvoiceLine,
|
|
|
|
|
type InvoiceStatus,
|
|
|
|
|
} from '@/api/queries/invoiceQueries';
|
|
|
|
|
import { useActiveCustomers, type Customer } from '@/api/queries/customerQueries';
|
|
|
|
|
import {
|
|
|
|
|
useCreateCreditNote,
|
|
|
|
|
useIssueCreditNote,
|
|
|
|
|
useApplyCreditNote,
|
|
|
|
|
useAddInvoiceLine,
|
|
|
|
|
useUpdateInvoiceLine,
|
|
|
|
|
useRemoveInvoiceLine,
|
|
|
|
|
useVoidInvoice,
|
|
|
|
|
type CreateCreditNoteInput,
|
|
|
|
|
type AddInvoiceLineInput,
|
|
|
|
|
type VoidInvoiceInput,
|
|
|
|
|
} from '@/api/mutations/invoiceMutations';
|
|
|
|
|
import { formatCurrency, formatDate } from '@/lib/formatters';
|
|
|
|
|
import { spacing } from '@/styles/designTokens';
|
|
|
|
|
import { accountingColors } from '@/styles/theme';
|
|
|
|
|
import { AmountText } from '@/components/shared/AmountText';
|
|
|
|
|
import { EmptyState } from '@/components/shared/EmptyState';
|
Audit v2: fix security, data integrity, compliance, bugs, encoding, UX
Backend Security & Data Integrity:
- Block negative debit/credit amounts that bypass balance validation
- Require document date at posting (was optional, bypassing fiscal year checks)
- Fix event sourcing anti-pattern: timestamps now stored in events, not UtcNow in Apply
- Add [Authorize] to BankingController OAuth callback
- Add company access check on attachment downloads
- Validate CVR in CompanyAggregate.Create and CompanyAggregate.Update
- Require company CVR for invoice creation (Momsloven §52)
- Delete leftover WeatherForecastController
- Fix duplicate migration number 007 (renamed to 007b)
- Remove dead code in VatCalculationService (identical if/else branches)
Accounting Compliance:
- Add missing VAT accounts to StandardDanishAccounts (5610, 5611, 5620)
- Populate SAF-T TaxInformation on transaction lines (was always null)
- Add AuditFileCountry and TaxRegistrationNumber to SAF-T header
Critical Frontend Bugs:
- Fix Dashboard <a href> causing full page reloads (now uses React Router Link)
- Wire Kassekladde filters to actual data (account, status, date range)
- Pre-populate form when editing existing Kassekladde drafts
- Add detail drawer for "Vis detaljer" action (was just a toast)
- Toggle advanced filters with "Flere filtre" button
- CloseFiscalYearWizard now actually posts closing entries via mutations
- "Create next year" checkbox now creates the next fiscal year
Danish Character Encoding (~50 fixes):
- Fix ø/æ/å across Momsindberetning, DocumentUploadModal, Bankafstemning,
Kontooversigt, CloseFiscalYearWizard, vatCodes, periodStore, periods,
accounting, types/periods
Dead Buttons & UX:
- Disable Momsindberetning PDF/Export buttons with tooltips
- FiscalYearSelector "Administrer" now navigates to Settings
- Settings bank tab now uses real BankConnectionsTab component
- Bankafstemning save button disabled with development tooltip
- Replace hardcoded account options with real API data (Bankafstemning, Fakturaer)
- Header help button shows info message, notification bell shows popover
Consistency & Quality:
- Remove 7 console.log statements from production code
- Adopt PageHeader on 6 remaining pages (Kreditnotaer, Settings, Admin, etc.)
- Standardize loading states to Skeleton pattern (5 pages)
- Replace deprecated bodyStyle prop on Ant Design Cards
- Standardize date format to DD-MM-YYYY
- Fix sidebar width mismatch in designTokens
- Fix Kontooversigt breadcrumb pointing to non-existent route
Accessibility:
- Add aria-label to sidebar navigation
- Add +/- prefix to AmountText for color-blind users
- Fix CompanySwitcher permanent skeleton when no companies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 00:18:19 +01:00
|
|
|
import { PageHeader } from '@/components/shared/PageHeader';
|
Add frontend components, API mutations, and project config
Frontend:
- API mutations for accounts, bank connections, customers, invoices
- Document processing API
- Shared components (PageHeader, EmptyState, etc.)
- Pages: Admin, Fakturaer, Kunder, Ordrer, Produkter, etc.
- Hooks and stores
Config:
- CLAUDE.md project instructions
- Beads issue tracking config
- Git attributes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:20:03 +01:00
|
|
|
|
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
|
|
|
|
// Credit note statuses (using unified Invoice model)
|
|
|
|
|
const statusColors: Record<string, string> = {
|
|
|
|
|
draft: 'default',
|
|
|
|
|
issued: 'processing',
|
|
|
|
|
partially_applied: 'warning',
|
|
|
|
|
fully_applied: 'success',
|
|
|
|
|
voided: 'error',
|
|
|
|
|
// Also include invoice statuses that might appear
|
|
|
|
|
sent: 'processing',
|
|
|
|
|
partially_paid: 'warning',
|
|
|
|
|
paid: 'success',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
|
|
|
draft: 'Kladde',
|
|
|
|
|
issued: 'Udstedt',
|
|
|
|
|
partially_applied: 'Delvist anvendt',
|
|
|
|
|
fully_applied: 'Fuldt anvendt',
|
|
|
|
|
voided: 'Annulleret',
|
|
|
|
|
sent: 'Sendt',
|
|
|
|
|
partially_paid: 'Delvist betalt',
|
|
|
|
|
paid: 'Betalt',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function Kreditnotaer() {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { company } = useCompany();
|
|
|
|
|
const currentFiscalYear = useCurrentFiscalYear();
|
|
|
|
|
const [searchParams] = useSearchParams();
|
|
|
|
|
const customerIdFilter = searchParams.get('customer');
|
|
|
|
|
|
|
|
|
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
|
|
|
const [isLineModalOpen, setIsLineModalOpen] = useState(false);
|
|
|
|
|
const [isApplyModalOpen, setIsApplyModalOpen] = useState(false);
|
|
|
|
|
const [isVoidModalOpen, setIsVoidModalOpen] = useState(false);
|
|
|
|
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
|
|
|
const [selectedCreditNote, setSelectedCreditNote] = useState<Invoice | null>(null);
|
|
|
|
|
const [editingLine, setEditingLine] = useState<InvoiceLine | null>(null);
|
|
|
|
|
const [searchText, setSearchText] = useState('');
|
|
|
|
|
const [statusFilter, setStatusFilter] = useState<InvoiceStatus | 'all'>('all');
|
|
|
|
|
const [createForm] = Form.useForm();
|
|
|
|
|
const [lineForm] = Form.useForm();
|
|
|
|
|
const [applyForm] = Form.useForm();
|
|
|
|
|
const [voidForm] = Form.useForm();
|
|
|
|
|
|
|
|
|
|
// Fetch credit notes
|
|
|
|
|
const {
|
|
|
|
|
data: creditNotes = [],
|
|
|
|
|
isLoading: loading,
|
|
|
|
|
error,
|
|
|
|
|
refetch,
|
|
|
|
|
} = useCreditNotes(company?.id);
|
|
|
|
|
|
|
|
|
|
// Fetch customers for dropdown
|
|
|
|
|
const { data: customers = [] } = useActiveCustomers(company?.id);
|
|
|
|
|
|
2026-02-05 21:35:26 +01:00
|
|
|
// Fetch invoices for applying credit notes and for original invoice selector
|
Add frontend components, API mutations, and project config
Frontend:
- API mutations for accounts, bank connections, customers, invoices
- Document processing API
- Shared components (PageHeader, EmptyState, etc.)
- Pages: Admin, Fakturaer, Kunder, Ordrer, Produkter, etc.
- Hooks and stores
Config:
- CLAUDE.md project instructions
- Beads issue tracking config
- Git attributes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:20:03 +01:00
|
|
|
const { data: allInvoices = [] } = useInvoices(company?.id, undefined, {
|
2026-02-05 21:35:26 +01:00
|
|
|
enabled: !!company?.id && (isApplyModalOpen || isCreateModalOpen),
|
Add frontend components, API mutations, and project config
Frontend:
- API mutations for accounts, bank connections, customers, invoices
- Document processing API
- Shared components (PageHeader, EmptyState, etc.)
- Pages: Admin, Fakturaer, Kunder, Ordrer, Produkter, etc.
- Hooks and stores
Config:
- CLAUDE.md project instructions
- Beads issue tracking config
- Git attributes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:20:03 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const openInvoices: Invoice[] = allInvoices.filter(
|
|
|
|
|
(i: Invoice) => ['sent', 'partially_paid'].includes(i.status) && i.amountRemaining > 0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Mutations
|
|
|
|
|
const createCreditNoteMutation = useCreateCreditNote();
|
|
|
|
|
const addLineMutation = useAddInvoiceLine();
|
|
|
|
|
const updateLineMutation = useUpdateInvoiceLine();
|
|
|
|
|
const removeLineMutation = useRemoveInvoiceLine();
|
|
|
|
|
const issueCreditNoteMutation = useIssueCreditNote();
|
|
|
|
|
const applyCreditNoteMutation = useApplyCreditNote();
|
|
|
|
|
const voidMutation = useVoidInvoice();
|
|
|
|
|
|
|
|
|
|
// Filter credit notes
|
|
|
|
|
const filteredCreditNotes = useMemo(() => {
|
|
|
|
|
return creditNotes.filter((cn) => {
|
|
|
|
|
const matchesSearch =
|
|
|
|
|
searchText === '' ||
|
|
|
|
|
cn.invoiceNumber.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
|
|
|
cn.customerName.toLowerCase().includes(searchText.toLowerCase());
|
|
|
|
|
|
|
|
|
|
const matchesStatus = statusFilter === 'all' || cn.status === statusFilter;
|
|
|
|
|
|
|
|
|
|
const matchesCustomer = !customerIdFilter || cn.customerId === customerIdFilter;
|
|
|
|
|
|
|
|
|
|
return matchesSearch && matchesStatus && matchesCustomer;
|
|
|
|
|
});
|
|
|
|
|
}, [creditNotes, searchText, statusFilter, customerIdFilter]);
|
|
|
|
|
|
|
|
|
|
// Statistics
|
|
|
|
|
const stats = useMemo(() => {
|
|
|
|
|
const total = creditNotes.length;
|
|
|
|
|
const draft = creditNotes.filter((cn) => cn.status === 'draft').length;
|
|
|
|
|
const unapplied = creditNotes
|
|
|
|
|
.filter((cn) => ['issued', 'partially_applied'].includes(cn.status))
|
|
|
|
|
.reduce((sum, cn) => sum + cn.amountRemaining, 0);
|
|
|
|
|
const totalValue = creditNotes
|
|
|
|
|
.filter((cn) => cn.status !== 'voided')
|
|
|
|
|
.reduce((sum, cn) => sum + cn.amountTotal, 0);
|
|
|
|
|
return { total, draft, unapplied, totalValue };
|
|
|
|
|
}, [creditNotes]);
|
|
|
|
|
|
|
|
|
|
const handleCreateCreditNote = () => {
|
|
|
|
|
createForm.resetFields();
|
|
|
|
|
createForm.setFieldsValue({
|
|
|
|
|
creditNoteDate: dayjs(),
|
|
|
|
|
});
|
|
|
|
|
setIsCreateModalOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmitCreate = async () => {
|
|
|
|
|
if (!company || !currentFiscalYear) {
|
|
|
|
|
showError('Virksomhed eller regnskabsår ikke valgt');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const values = await createForm.validateFields();
|
|
|
|
|
const input: CreateCreditNoteInput = {
|
|
|
|
|
companyId: company.id,
|
|
|
|
|
fiscalYearId: currentFiscalYear.id,
|
|
|
|
|
customerId: values.customerId,
|
|
|
|
|
creditNoteDate: values.creditNoteDate?.toISOString(),
|
|
|
|
|
originalInvoiceId: values.originalInvoiceId || undefined,
|
|
|
|
|
creditReason: values.reason || undefined,
|
|
|
|
|
};
|
|
|
|
|
const result = await createCreditNoteMutation.mutateAsync(input);
|
|
|
|
|
showSuccess('Kreditnota oprettet');
|
|
|
|
|
setIsCreateModalOpen(false);
|
|
|
|
|
createForm.resetFields();
|
|
|
|
|
setSelectedCreditNote(result);
|
|
|
|
|
setIsDrawerOpen(true);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof Error) {
|
|
|
|
|
showError(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddLine = () => {
|
|
|
|
|
setEditingLine(null);
|
|
|
|
|
lineForm.resetFields();
|
|
|
|
|
lineForm.setFieldsValue({ quantity: 1, vatCode: 'S25' });
|
|
|
|
|
setIsLineModalOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEditLine = (line: InvoiceLine) => {
|
|
|
|
|
setEditingLine(line);
|
|
|
|
|
lineForm.setFieldsValue({
|
|
|
|
|
description: line.description,
|
|
|
|
|
quantity: line.quantity,
|
|
|
|
|
unitPrice: line.unitPrice,
|
|
|
|
|
vatCode: line.vatCode,
|
|
|
|
|
});
|
|
|
|
|
setIsLineModalOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmitLine = async () => {
|
|
|
|
|
if (!selectedCreditNote) return;
|
|
|
|
|
try {
|
|
|
|
|
const values = await lineForm.validateFields();
|
|
|
|
|
if (editingLine) {
|
|
|
|
|
const result = await updateLineMutation.mutateAsync({
|
|
|
|
|
invoiceId: selectedCreditNote.id,
|
|
|
|
|
lineNumber: editingLine.lineNumber,
|
|
|
|
|
description: values.description,
|
|
|
|
|
quantity: values.quantity,
|
|
|
|
|
unitPrice: values.unitPrice,
|
|
|
|
|
vatCode: values.vatCode,
|
|
|
|
|
});
|
|
|
|
|
showSuccess('Linje opdateret');
|
|
|
|
|
setSelectedCreditNote(result);
|
|
|
|
|
} else {
|
|
|
|
|
const input: AddInvoiceLineInput = {
|
|
|
|
|
invoiceId: selectedCreditNote.id,
|
|
|
|
|
description: values.description,
|
|
|
|
|
quantity: values.quantity,
|
|
|
|
|
unitPrice: values.unitPrice,
|
|
|
|
|
vatCode: values.vatCode,
|
|
|
|
|
};
|
|
|
|
|
const result = await addLineMutation.mutateAsync(input);
|
|
|
|
|
showSuccess('Linje tilføjet');
|
|
|
|
|
setSelectedCreditNote(result);
|
|
|
|
|
}
|
|
|
|
|
setIsLineModalOpen(false);
|
|
|
|
|
setEditingLine(null);
|
|
|
|
|
lineForm.resetFields();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof Error) {
|
|
|
|
|
showError(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRemoveLine = async (lineNumber: number) => {
|
|
|
|
|
if (!selectedCreditNote) return;
|
|
|
|
|
try {
|
|
|
|
|
const result = await removeLineMutation.mutateAsync({
|
|
|
|
|
invoiceId: selectedCreditNote.id,
|
|
|
|
|
lineNumber,
|
|
|
|
|
});
|
|
|
|
|
showSuccess('Linje fjernet');
|
|
|
|
|
setSelectedCreditNote(result);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof Error) {
|
|
|
|
|
showError(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleIssueCreditNote = async () => {
|
|
|
|
|
if (!selectedCreditNote) return;
|
|
|
|
|
if (selectedCreditNote.lines.length === 0) {
|
|
|
|
|
showWarning('Tilføj mindst én linje før udstedelse');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const result = await issueCreditNoteMutation.mutateAsync(selectedCreditNote.id);
|
|
|
|
|
showSuccess('Kreditnota udstedt og bogført');
|
|
|
|
|
setSelectedCreditNote(result);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof Error) {
|
|
|
|
|
showError(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleApplyCreditNote = () => {
|
|
|
|
|
applyForm.resetFields();
|
|
|
|
|
applyForm.setFieldsValue({
|
|
|
|
|
amount: selectedCreditNote?.amountRemaining,
|
|
|
|
|
});
|
|
|
|
|
setIsApplyModalOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmitApply = async () => {
|
|
|
|
|
if (!selectedCreditNote) return;
|
|
|
|
|
try {
|
|
|
|
|
const values = await applyForm.validateFields();
|
|
|
|
|
const result = await applyCreditNoteMutation.mutateAsync({
|
|
|
|
|
creditNoteId: selectedCreditNote.id,
|
|
|
|
|
invoiceId: values.invoiceId,
|
|
|
|
|
amount: values.amount,
|
|
|
|
|
});
|
|
|
|
|
showSuccess('Kreditnota anvendt på faktura');
|
|
|
|
|
setIsApplyModalOpen(false);
|
|
|
|
|
applyForm.resetFields();
|
|
|
|
|
setSelectedCreditNote(result);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof Error) {
|
|
|
|
|
showError(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleVoidCreditNote = () => {
|
|
|
|
|
voidForm.resetFields();
|
|
|
|
|
setIsVoidModalOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmitVoid = async () => {
|
|
|
|
|
if (!selectedCreditNote) return;
|
|
|
|
|
try {
|
|
|
|
|
const values = await voidForm.validateFields();
|
|
|
|
|
const input: VoidInvoiceInput = {
|
|
|
|
|
invoiceId: selectedCreditNote.id,
|
|
|
|
|
reason: values.reason,
|
|
|
|
|
};
|
|
|
|
|
const result = await voidMutation.mutateAsync(input);
|
|
|
|
|
showSuccess('Kreditnota annulleret');
|
|
|
|
|
setIsVoidModalOpen(false);
|
|
|
|
|
voidForm.resetFields();
|
|
|
|
|
setSelectedCreditNote(result);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof Error) {
|
|
|
|
|
showError(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleViewCreditNote = (creditNote: Invoice) => {
|
|
|
|
|
setSelectedCreditNote(creditNote);
|
|
|
|
|
setIsDrawerOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const columns: ColumnsType<Invoice> = [
|
|
|
|
|
{
|
|
|
|
|
title: 'Kreditnotanr.',
|
|
|
|
|
dataIndex: 'invoiceNumber',
|
|
|
|
|
key: 'invoiceNumber',
|
|
|
|
|
width: 130,
|
|
|
|
|
sorter: (a, b) => a.invoiceNumber.localeCompare(b.invoiceNumber),
|
|
|
|
|
render: (value: string) => <Text code>{value}</Text>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Kunde',
|
|
|
|
|
dataIndex: 'customerName',
|
|
|
|
|
key: 'customerName',
|
|
|
|
|
sorter: (a, b) => a.customerName.localeCompare(b.customerName),
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Dato',
|
|
|
|
|
dataIndex: 'invoiceDate',
|
|
|
|
|
key: 'invoiceDate',
|
|
|
|
|
width: 100,
|
|
|
|
|
sorter: (a, b) => (a.invoiceDate ?? '').localeCompare(b.invoiceDate ?? ''),
|
|
|
|
|
render: (value: string | undefined) => value ? formatDate(value) : '-',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Beløb',
|
|
|
|
|
dataIndex: 'amountTotal',
|
|
|
|
|
key: 'amountTotal',
|
|
|
|
|
width: 120,
|
|
|
|
|
align: 'right',
|
|
|
|
|
sorter: (a, b) => a.amountTotal - b.amountTotal,
|
|
|
|
|
render: (value: number) => <AmountText amount={-value} />,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Restbeløb',
|
|
|
|
|
dataIndex: 'amountRemaining',
|
|
|
|
|
key: 'amountRemaining',
|
|
|
|
|
width: 120,
|
|
|
|
|
align: 'right',
|
|
|
|
|
render: (value: number, record: Invoice) =>
|
|
|
|
|
record.status === 'voided' ? '-' : <AmountText amount={-value} />,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Orig. faktura',
|
|
|
|
|
dataIndex: 'originalInvoiceNumber',
|
|
|
|
|
key: 'originalInvoiceNumber',
|
|
|
|
|
width: 120,
|
|
|
|
|
render: (value: string | undefined) => (value ? <Text code>{value}</Text> : '-'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Status',
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
key: 'status',
|
|
|
|
|
width: 130,
|
|
|
|
|
align: 'center',
|
|
|
|
|
filters: [
|
|
|
|
|
{ text: 'Kladde', value: 'draft' },
|
|
|
|
|
{ text: 'Udstedt', value: 'issued' },
|
|
|
|
|
{ text: 'Delvist anvendt', value: 'partially_applied' },
|
|
|
|
|
{ text: 'Fuldt anvendt', value: 'fully_applied' },
|
|
|
|
|
{ text: 'Annulleret', value: 'voided' },
|
|
|
|
|
],
|
|
|
|
|
onFilter: (value, record) => record.status === value,
|
|
|
|
|
render: (value: InvoiceStatus) => (
|
|
|
|
|
<Tag color={statusColors[value]}>{statusLabels[value]}</Tag>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '',
|
|
|
|
|
key: 'actions',
|
|
|
|
|
width: 80,
|
|
|
|
|
align: 'center',
|
|
|
|
|
render: (_: unknown, record: Invoice) => (
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<EyeOutlined />}
|
|
|
|
|
onClick={() => handleViewCreditNote(record)}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
Audit v2: fix security, data integrity, compliance, bugs, encoding, UX
Backend Security & Data Integrity:
- Block negative debit/credit amounts that bypass balance validation
- Require document date at posting (was optional, bypassing fiscal year checks)
- Fix event sourcing anti-pattern: timestamps now stored in events, not UtcNow in Apply
- Add [Authorize] to BankingController OAuth callback
- Add company access check on attachment downloads
- Validate CVR in CompanyAggregate.Create and CompanyAggregate.Update
- Require company CVR for invoice creation (Momsloven §52)
- Delete leftover WeatherForecastController
- Fix duplicate migration number 007 (renamed to 007b)
- Remove dead code in VatCalculationService (identical if/else branches)
Accounting Compliance:
- Add missing VAT accounts to StandardDanishAccounts (5610, 5611, 5620)
- Populate SAF-T TaxInformation on transaction lines (was always null)
- Add AuditFileCountry and TaxRegistrationNumber to SAF-T header
Critical Frontend Bugs:
- Fix Dashboard <a href> causing full page reloads (now uses React Router Link)
- Wire Kassekladde filters to actual data (account, status, date range)
- Pre-populate form when editing existing Kassekladde drafts
- Add detail drawer for "Vis detaljer" action (was just a toast)
- Toggle advanced filters with "Flere filtre" button
- CloseFiscalYearWizard now actually posts closing entries via mutations
- "Create next year" checkbox now creates the next fiscal year
Danish Character Encoding (~50 fixes):
- Fix ø/æ/å across Momsindberetning, DocumentUploadModal, Bankafstemning,
Kontooversigt, CloseFiscalYearWizard, vatCodes, periodStore, periods,
accounting, types/periods
Dead Buttons & UX:
- Disable Momsindberetning PDF/Export buttons with tooltips
- FiscalYearSelector "Administrer" now navigates to Settings
- Settings bank tab now uses real BankConnectionsTab component
- Bankafstemning save button disabled with development tooltip
- Replace hardcoded account options with real API data (Bankafstemning, Fakturaer)
- Header help button shows info message, notification bell shows popover
Consistency & Quality:
- Remove 7 console.log statements from production code
- Adopt PageHeader on 6 remaining pages (Kreditnotaer, Settings, Admin, etc.)
- Standardize loading states to Skeleton pattern (5 pages)
- Replace deprecated bodyStyle prop on Ant Design Cards
- Standardize date format to DD-MM-YYYY
- Fix sidebar width mismatch in designTokens
- Fix Kontooversigt breadcrumb pointing to non-existent route
Accessibility:
- Add aria-label to sidebar navigation
- Add +/- prefix to AmountText for color-blind users
- Fix CompanySwitcher permanent skeleton when no companies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 00:18:19 +01:00
|
|
|
<PageHeader
|
|
|
|
|
title="Kreditnotaer"
|
|
|
|
|
subtitle={company?.name}
|
|
|
|
|
breadcrumbs={[{ title: 'Salg' }, { title: 'Kreditnotaer' }]}
|
|
|
|
|
extra={
|
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateCreditNote}>
|
|
|
|
|
Ny kreditnota
|
|
|
|
|
</Button>
|
|
|
|
|
}
|
|
|
|
|
/>
|
Add frontend components, API mutations, and project config
Frontend:
- API mutations for accounts, bank connections, customers, invoices
- Document processing API
- Shared components (PageHeader, EmptyState, etc.)
- Pages: Admin, Fakturaer, Kunder, Ordrer, Produkter, etc.
- Hooks and stores
Config:
- CLAUDE.md project instructions
- Beads issue tracking config
- Git attributes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:20:03 +01:00
|
|
|
|
|
|
|
|
{/* Error State */}
|
|
|
|
|
{error && (
|
|
|
|
|
<Alert
|
|
|
|
|
message="Fejl ved indlæsning af kreditnotaer"
|
|
|
|
|
description={error.message}
|
|
|
|
|
type="error"
|
|
|
|
|
showIcon
|
|
|
|
|
style={{ marginBottom: spacing.lg }}
|
|
|
|
|
action={
|
|
|
|
|
<Button size="small" onClick={() => refetch()}>
|
|
|
|
|
Prøv igen
|
|
|
|
|
</Button>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Statistics */}
|
|
|
|
|
<Row gutter={[spacing.lg, spacing.lg]} style={{ marginBottom: spacing.lg }}>
|
|
|
|
|
<Col xs={12} sm={6}>
|
|
|
|
|
<Card size="small">
|
|
|
|
|
<Statistic title="Kreditnotaer i alt" value={stats.total} />
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={12} sm={6}>
|
|
|
|
|
<Card size="small">
|
|
|
|
|
<Statistic
|
|
|
|
|
title="Kladder"
|
|
|
|
|
value={stats.draft}
|
|
|
|
|
valueStyle={{ color: '#8c8c8c' }}
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={12} sm={6}>
|
|
|
|
|
<Card size="small">
|
|
|
|
|
<Statistic
|
|
|
|
|
title="Ikke anvendt"
|
|
|
|
|
value={stats.unapplied}
|
|
|
|
|
precision={2}
|
|
|
|
|
valueStyle={{ color: accountingColors.credit }}
|
|
|
|
|
formatter={(value) => formatCurrency(value as number)}
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={12} sm={6}>
|
|
|
|
|
<Card size="small">
|
|
|
|
|
<Statistic
|
|
|
|
|
title="Samlet værdi"
|
|
|
|
|
value={stats.totalValue}
|
|
|
|
|
precision={2}
|
|
|
|
|
valueStyle={{ color: accountingColors.debit }}
|
|
|
|
|
formatter={(value) => formatCurrency(value as number)}
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
{/* Filters */}
|
|
|
|
|
<Card size="small" style={{ marginBottom: spacing.lg }}>
|
|
|
|
|
<Space wrap>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Søg kreditnota..."
|
|
|
|
|
prefix={<SearchOutlined />}
|
|
|
|
|
value={searchText}
|
|
|
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
|
|
|
style={{ width: 250 }}
|
|
|
|
|
allowClear
|
|
|
|
|
/>
|
|
|
|
|
<Select
|
|
|
|
|
value={statusFilter}
|
|
|
|
|
onChange={setStatusFilter}
|
|
|
|
|
style={{ width: 170 }}
|
|
|
|
|
options={[
|
|
|
|
|
{ value: 'all', label: 'Alle status' },
|
|
|
|
|
{ value: 'draft', label: 'Kladde' },
|
|
|
|
|
{ value: 'issued', label: 'Udstedt' },
|
|
|
|
|
{ value: 'partially_applied', label: 'Delvist anvendt' },
|
|
|
|
|
{ value: 'fully_applied', label: 'Fuldt anvendt' },
|
|
|
|
|
{ value: 'voided', label: 'Annulleret' },
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
{customerIdFilter && (
|
|
|
|
|
<Tag closable onClose={() => navigate('/kreditnotaer')}>
|
|
|
|
|
Filtreret på kunde
|
|
|
|
|
</Tag>
|
|
|
|
|
)}
|
|
|
|
|
</Space>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Credit Note Table */}
|
|
|
|
|
<Card size="small">
|
|
|
|
|
{loading ? (
|
Audit v2: fix security, data integrity, compliance, bugs, encoding, UX
Backend Security & Data Integrity:
- Block negative debit/credit amounts that bypass balance validation
- Require document date at posting (was optional, bypassing fiscal year checks)
- Fix event sourcing anti-pattern: timestamps now stored in events, not UtcNow in Apply
- Add [Authorize] to BankingController OAuth callback
- Add company access check on attachment downloads
- Validate CVR in CompanyAggregate.Create and CompanyAggregate.Update
- Require company CVR for invoice creation (Momsloven §52)
- Delete leftover WeatherForecastController
- Fix duplicate migration number 007 (renamed to 007b)
- Remove dead code in VatCalculationService (identical if/else branches)
Accounting Compliance:
- Add missing VAT accounts to StandardDanishAccounts (5610, 5611, 5620)
- Populate SAF-T TaxInformation on transaction lines (was always null)
- Add AuditFileCountry and TaxRegistrationNumber to SAF-T header
Critical Frontend Bugs:
- Fix Dashboard <a href> causing full page reloads (now uses React Router Link)
- Wire Kassekladde filters to actual data (account, status, date range)
- Pre-populate form when editing existing Kassekladde drafts
- Add detail drawer for "Vis detaljer" action (was just a toast)
- Toggle advanced filters with "Flere filtre" button
- CloseFiscalYearWizard now actually posts closing entries via mutations
- "Create next year" checkbox now creates the next fiscal year
Danish Character Encoding (~50 fixes):
- Fix ø/æ/å across Momsindberetning, DocumentUploadModal, Bankafstemning,
Kontooversigt, CloseFiscalYearWizard, vatCodes, periodStore, periods,
accounting, types/periods
Dead Buttons & UX:
- Disable Momsindberetning PDF/Export buttons with tooltips
- FiscalYearSelector "Administrer" now navigates to Settings
- Settings bank tab now uses real BankConnectionsTab component
- Bankafstemning save button disabled with development tooltip
- Replace hardcoded account options with real API data (Bankafstemning, Fakturaer)
- Header help button shows info message, notification bell shows popover
Consistency & Quality:
- Remove 7 console.log statements from production code
- Adopt PageHeader on 6 remaining pages (Kreditnotaer, Settings, Admin, etc.)
- Standardize loading states to Skeleton pattern (5 pages)
- Replace deprecated bodyStyle prop on Ant Design Cards
- Standardize date format to DD-MM-YYYY
- Fix sidebar width mismatch in designTokens
- Fix Kontooversigt breadcrumb pointing to non-existent route
Accessibility:
- Add aria-label to sidebar navigation
- Add +/- prefix to AmountText for color-blind users
- Fix CompanySwitcher permanent skeleton when no companies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 00:18:19 +01:00
|
|
|
<Skeleton active paragraph={{ rows: 8 }} />
|
Add frontend components, API mutations, and project config
Frontend:
- API mutations for accounts, bank connections, customers, invoices
- Document processing API
- Shared components (PageHeader, EmptyState, etc.)
- Pages: Admin, Fakturaer, Kunder, Ordrer, Produkter, etc.
- Hooks and stores
Config:
- CLAUDE.md project instructions
- Beads issue tracking config
- Git attributes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:20:03 +01:00
|
|
|
) : filteredCreditNotes.length > 0 ? (
|
|
|
|
|
<Table
|
|
|
|
|
dataSource={filteredCreditNotes}
|
|
|
|
|
columns={columns}
|
|
|
|
|
rowKey="id"
|
|
|
|
|
size="small"
|
|
|
|
|
pagination={{ pageSize: 20, showSizeChanger: true }}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<EmptyState
|
|
|
|
|
variant="creditNotes"
|
|
|
|
|
title="Ingen kreditnotaer"
|
|
|
|
|
description={
|
|
|
|
|
searchText
|
|
|
|
|
? 'Ingen kreditnotaer matcher din søgning'
|
|
|
|
|
: 'Opret din første kreditnota'
|
|
|
|
|
}
|
|
|
|
|
primaryAction={
|
|
|
|
|
!searchText
|
|
|
|
|
? {
|
|
|
|
|
label: 'Opret kreditnota',
|
|
|
|
|
onClick: handleCreateCreditNote,
|
|
|
|
|
icon: <PlusOutlined />,
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Create Credit Note Modal */}
|
|
|
|
|
<Modal
|
|
|
|
|
title="Opret kreditnota"
|
|
|
|
|
open={isCreateModalOpen}
|
|
|
|
|
onCancel={() => setIsCreateModalOpen(false)}
|
|
|
|
|
onOk={handleSubmitCreate}
|
|
|
|
|
okText="Opret"
|
|
|
|
|
cancelText="Annuller"
|
|
|
|
|
confirmLoading={createCreditNoteMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
<Form form={createForm} layout="vertical">
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="customerId"
|
|
|
|
|
label="Kunde"
|
|
|
|
|
rules={[{ required: true, message: 'Vælg kunde' }]}
|
|
|
|
|
>
|
|
|
|
|
<Select
|
|
|
|
|
showSearch
|
|
|
|
|
placeholder="Vælg kunde"
|
|
|
|
|
optionFilterProp="children"
|
|
|
|
|
filterOption={(input, option) =>
|
|
|
|
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
|
|
|
}
|
|
|
|
|
options={customers.map((c: Customer) => ({
|
|
|
|
|
value: c.id,
|
|
|
|
|
label: `${c.customerNumber} - ${c.name}`,
|
|
|
|
|
}))}
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="creditNoteDate" label="Kreditnotadato">
|
|
|
|
|
<DatePicker style={{ width: '100%' }} format="DD-MM-YYYY" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="originalInvoiceId" label="Original faktura (valgfri)">
|
|
|
|
|
<Select
|
|
|
|
|
showSearch
|
|
|
|
|
allowClear
|
|
|
|
|
placeholder="Vælg faktura der krediteres"
|
|
|
|
|
optionFilterProp="children"
|
2026-02-05 21:35:26 +01:00
|
|
|
filterOption={(input, option) =>
|
|
|
|
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
|
|
|
}
|
|
|
|
|
options={allInvoices
|
|
|
|
|
.filter((i: Invoice) => !i.isCreditNote && i.status !== 'voided')
|
|
|
|
|
.map((i: Invoice) => ({
|
|
|
|
|
value: i.id,
|
|
|
|
|
label: `${i.invoiceNumber} - ${i.customerName} (${formatCurrency(i.amountTotal)})`,
|
|
|
|
|
}))}
|
Add frontend components, API mutations, and project config
Frontend:
- API mutations for accounts, bank connections, customers, invoices
- Document processing API
- Shared components (PageHeader, EmptyState, etc.)
- Pages: Admin, Fakturaer, Kunder, Ordrer, Produkter, etc.
- Hooks and stores
Config:
- CLAUDE.md project instructions
- Beads issue tracking config
- Git attributes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:20:03 +01:00
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="reason" label="Årsag">
|
|
|
|
|
<Input.TextArea rows={2} placeholder="Årsag til kreditering" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
{/* Credit Note Detail Drawer */}
|
|
|
|
|
<Drawer
|
|
|
|
|
title={
|
|
|
|
|
selectedCreditNote && (
|
|
|
|
|
<Space>
|
|
|
|
|
<FileTextOutlined />
|
|
|
|
|
<span>Kreditnota {selectedCreditNote.invoiceNumber}</span>
|
|
|
|
|
<Tag color={statusColors[selectedCreditNote.status] || 'default'}>
|
|
|
|
|
{statusLabels[selectedCreditNote.status] || selectedCreditNote.status}
|
|
|
|
|
</Tag>
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
placement="right"
|
|
|
|
|
width={700}
|
|
|
|
|
open={isDrawerOpen}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setIsDrawerOpen(false);
|
|
|
|
|
setSelectedCreditNote(null);
|
|
|
|
|
}}
|
|
|
|
|
extra={
|
|
|
|
|
selectedCreditNote && (
|
|
|
|
|
<Space>
|
|
|
|
|
{selectedCreditNote.status === 'draft' && (
|
|
|
|
|
<>
|
|
|
|
|
<Button icon={<PlusOutlined />} onClick={handleAddLine}>
|
|
|
|
|
Tilføj linje
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
icon={<SendOutlined />}
|
|
|
|
|
onClick={handleIssueCreditNote}
|
|
|
|
|
loading={issueCreditNoteMutation.isPending}
|
|
|
|
|
disabled={selectedCreditNote.lines.length === 0}
|
|
|
|
|
>
|
|
|
|
|
Udsted
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{['issued', 'partially_applied'].includes(selectedCreditNote.status) && (
|
|
|
|
|
<>
|
|
|
|
|
<Button icon={<LinkOutlined />} onClick={handleApplyCreditNote}>
|
|
|
|
|
Anvend på faktura
|
|
|
|
|
</Button>
|
|
|
|
|
<Button danger icon={<StopOutlined />} onClick={handleVoidCreditNote}>
|
|
|
|
|
Annuller
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{selectedCreditNote && (
|
|
|
|
|
<div>
|
|
|
|
|
<Descriptions column={2} size="small" bordered style={{ marginBottom: spacing.lg }}>
|
|
|
|
|
<Descriptions.Item label="Kunde" span={2}>
|
|
|
|
|
{selectedCreditNote.customerName}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="Kreditnotadato">
|
|
|
|
|
{selectedCreditNote.invoiceDate ? formatDate(selectedCreditNote.invoiceDate) : '-'}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="Original faktura">
|
|
|
|
|
{selectedCreditNote.originalInvoiceNumber || '-'}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
{selectedCreditNote.creditReason && (
|
|
|
|
|
<Descriptions.Item label="Årsag" span={2}>
|
|
|
|
|
{selectedCreditNote.creditReason}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
)}
|
|
|
|
|
</Descriptions>
|
|
|
|
|
|
|
|
|
|
<Title level={5}>Linjer</Title>
|
|
|
|
|
{selectedCreditNote.lines.length > 0 ? (
|
|
|
|
|
<List
|
|
|
|
|
size="small"
|
|
|
|
|
bordered
|
|
|
|
|
dataSource={selectedCreditNote.lines}
|
|
|
|
|
renderItem={(line: InvoiceLine) => (
|
|
|
|
|
<List.Item
|
|
|
|
|
actions={
|
|
|
|
|
selectedCreditNote.status === 'draft'
|
|
|
|
|
? [
|
|
|
|
|
<Button
|
|
|
|
|
key="edit"
|
|
|
|
|
type="text"
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
onClick={() => handleEditLine(line)}
|
|
|
|
|
/>,
|
|
|
|
|
<Popconfirm
|
|
|
|
|
key="delete"
|
|
|
|
|
title="Fjern linje?"
|
|
|
|
|
onConfirm={() => handleRemoveLine(line.lineNumber)}
|
|
|
|
|
okText="Ja"
|
|
|
|
|
cancelText="Nej"
|
|
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
size="small"
|
|
|
|
|
danger
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
/>
|
|
|
|
|
</Popconfirm>,
|
|
|
|
|
]
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<List.Item.Meta
|
|
|
|
|
title={line.description}
|
|
|
|
|
description={
|
|
|
|
|
<Space>
|
|
|
|
|
<span>
|
|
|
|
|
{line.quantity} x {formatCurrency(line.unitPrice)}
|
|
|
|
|
</span>
|
|
|
|
|
<Tag>{line.vatCode}</Tag>
|
|
|
|
|
</Space>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<AmountText amount={-line.amountTotal} style={{ fontWeight: 'bold' }} />
|
|
|
|
|
</List.Item>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<Alert
|
|
|
|
|
message="Ingen linjer endnu"
|
|
|
|
|
description="Tilføj linjer for at kunne udstede kreditnotaen."
|
|
|
|
|
type="info"
|
|
|
|
|
showIcon
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Divider />
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col span={12} />
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<div style={{ textAlign: 'right' }}>
|
|
|
|
|
<div style={{ marginBottom: 4 }}>
|
|
|
|
|
<Text type="secondary">Beløb ex. moms: </Text>
|
|
|
|
|
<Text>{formatCurrency(-selectedCreditNote.amountExVat)}</Text>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ marginBottom: 4 }}>
|
|
|
|
|
<Text type="secondary">Moms: </Text>
|
|
|
|
|
<Text>{formatCurrency(-selectedCreditNote.amountVat)}</Text>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ marginBottom: 4 }}>
|
|
|
|
|
<Text strong>Total: </Text>
|
|
|
|
|
<Text strong style={{ fontSize: 16, color: accountingColors.credit }}>
|
|
|
|
|
{formatCurrency(-selectedCreditNote.amountTotal)}
|
|
|
|
|
</Text>
|
|
|
|
|
</div>
|
|
|
|
|
{selectedCreditNote.amountApplied > 0 && (
|
|
|
|
|
<div style={{ marginBottom: 4 }}>
|
|
|
|
|
<Text type="secondary">Anvendt: </Text>
|
|
|
|
|
<Text>{formatCurrency(-selectedCreditNote.amountApplied)}</Text>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{selectedCreditNote.amountRemaining > 0 &&
|
|
|
|
|
selectedCreditNote.status !== 'voided' && (
|
|
|
|
|
<div>
|
|
|
|
|
<Text type="secondary">Restbeløb: </Text>
|
|
|
|
|
<Text strong style={{ color: accountingColors.credit }}>
|
|
|
|
|
{formatCurrency(-selectedCreditNote.amountRemaining)}
|
|
|
|
|
</Text>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Drawer>
|
|
|
|
|
|
|
|
|
|
{/* Add/Edit Line Modal */}
|
|
|
|
|
<Modal
|
|
|
|
|
title={editingLine ? 'Rediger linje' : 'Tilføj linje'}
|
|
|
|
|
open={isLineModalOpen}
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
setIsLineModalOpen(false);
|
|
|
|
|
setEditingLine(null);
|
|
|
|
|
lineForm.resetFields();
|
|
|
|
|
}}
|
|
|
|
|
onOk={handleSubmitLine}
|
|
|
|
|
okText="Gem"
|
|
|
|
|
cancelText="Annuller"
|
|
|
|
|
confirmLoading={addLineMutation.isPending || updateLineMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
<Form form={lineForm} layout="vertical">
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="description"
|
|
|
|
|
label="Beskrivelse"
|
|
|
|
|
rules={[{ required: true, message: 'Indtast beskrivelse' }]}
|
|
|
|
|
>
|
|
|
|
|
<Input placeholder="Vare eller ydelse der krediteres" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="quantity"
|
|
|
|
|
label="Antal"
|
|
|
|
|
rules={[{ required: true, message: 'Indtast antal' }]}
|
|
|
|
|
>
|
|
|
|
|
<InputNumber min={0.01} step={1} style={{ width: '100%' }} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="unitPrice"
|
|
|
|
|
label="Enhedspris"
|
|
|
|
|
rules={[{ required: true, message: 'Indtast pris' }]}
|
|
|
|
|
>
|
|
|
|
|
<InputNumber
|
|
|
|
|
min={0}
|
|
|
|
|
step={0.01}
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="vatCode"
|
|
|
|
|
label="Momskode"
|
|
|
|
|
rules={[{ required: true, message: 'Vælg momskode' }]}
|
|
|
|
|
>
|
|
|
|
|
<Select
|
|
|
|
|
options={[
|
|
|
|
|
{ value: 'S25', label: 'S25 - 25% moms' },
|
|
|
|
|
{ value: 'U0', label: 'U0 - Momsfrit' },
|
|
|
|
|
{ value: 'UEU', label: 'UEU - EU-salg' },
|
|
|
|
|
{ value: 'UEXP', label: 'UEXP - Eksport' },
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
{/* Apply Credit Note Modal */}
|
|
|
|
|
<Modal
|
|
|
|
|
title="Anvend kreditnota på faktura"
|
|
|
|
|
open={isApplyModalOpen}
|
|
|
|
|
onCancel={() => setIsApplyModalOpen(false)}
|
|
|
|
|
onOk={handleSubmitApply}
|
|
|
|
|
okText="Anvend"
|
|
|
|
|
cancelText="Annuller"
|
|
|
|
|
confirmLoading={applyCreditNoteMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
<Form form={applyForm} layout="vertical">
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="invoiceId"
|
|
|
|
|
label="Faktura"
|
|
|
|
|
rules={[{ required: true, message: 'Vælg faktura' }]}
|
|
|
|
|
>
|
|
|
|
|
<Select
|
|
|
|
|
showSearch
|
|
|
|
|
placeholder="Vælg faktura"
|
|
|
|
|
optionFilterProp="children"
|
|
|
|
|
filterOption={(input, option) =>
|
|
|
|
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
|
|
|
}
|
|
|
|
|
options={openInvoices.map((i) => ({
|
|
|
|
|
value: i.id,
|
|
|
|
|
label: `${i.invoiceNumber} - ${i.customerName} (${formatCurrency(i.amountRemaining)})`,
|
|
|
|
|
}))}
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="amount"
|
|
|
|
|
label="Beløb"
|
|
|
|
|
rules={[{ required: true, message: 'Indtast beløb' }]}
|
|
|
|
|
>
|
|
|
|
|
<InputNumber
|
|
|
|
|
min={0.01}
|
|
|
|
|
max={selectedCreditNote?.amountRemaining}
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, '.')}
|
|
|
|
|
parser={(value) => value!.replace(/\./g, '') as unknown as number}
|
|
|
|
|
addonAfter="DKK"
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
{/* Void Modal */}
|
|
|
|
|
<Modal
|
|
|
|
|
title="Annuller kreditnota"
|
|
|
|
|
open={isVoidModalOpen}
|
|
|
|
|
onCancel={() => setIsVoidModalOpen(false)}
|
|
|
|
|
onOk={handleSubmitVoid}
|
|
|
|
|
okText="Annuller kreditnota"
|
|
|
|
|
okButtonProps={{ danger: true }}
|
|
|
|
|
cancelText="Fortryd"
|
|
|
|
|
confirmLoading={voidMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
<Alert
|
|
|
|
|
message="Advarsel"
|
|
|
|
|
description="At annullere kreditnotaen vil tilbageføre alle bogførte posteringer. Denne handling kan ikke fortrydes."
|
|
|
|
|
type="warning"
|
|
|
|
|
showIcon
|
|
|
|
|
style={{ marginBottom: spacing.lg }}
|
|
|
|
|
/>
|
|
|
|
|
<Form form={voidForm} layout="vertical">
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="reason"
|
|
|
|
|
label="Årsag til annullering"
|
|
|
|
|
rules={[{ required: true, message: 'Angiv årsag' }]}
|
|
|
|
|
>
|
|
|
|
|
<Input.TextArea rows={3} placeholder="Beskriv hvorfor kreditnotaen annulleres" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|