import { useState, useMemo } from 'react'; import { Typography, Button, Card, Table, Space, Tag, Modal, Form, Input, Select, Spin, Alert, Drawer, Descriptions, Row, Col, Statistic, DatePicker, Divider, List, Checkbox, Radio, Tooltip, } from 'antd'; import { showSuccess, showError, showWarning } from '@/lib/errorHandling'; import { PlusOutlined, SearchOutlined, EyeOutlined, CheckOutlined, StopOutlined, FileTextOutlined, ShoppingCartOutlined, FileDoneOutlined, BarcodeOutlined, } from '@ant-design/icons'; import dayjs from 'dayjs'; import { useCompany } from '@/hooks/useCompany'; import { useCurrentFiscalYear } from '@/stores/periodStore'; import { useOrders } from '@/api/queries/orderQueries'; import { useActiveCustomers, type Customer } from '@/api/queries/customerQueries'; import { useActiveProducts, type Product } from '@/api/queries/productQueries'; import { useCreateOrder, useAddOrderLine, useConfirmOrder, useCancelOrder, useConvertOrderToInvoice, type CreateOrderInput, type AddOrderLineInput, type CancelOrderInput, type InvoiceOrderLinesInput, } from '@/api/mutations/orderMutations'; 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'; import { PageHeader } from '@/components/shared/PageHeader'; import type { ColumnsType } from 'antd/es/table'; import type { Order, OrderLine, OrderStatus } from '@/types/order'; import { ORDER_STATUS_LABELS, ORDER_STATUS_COLORS } from '@/types/order'; const { Title, Text } = Typography; export default function Ordrer() { const { company } = useCompany(); const currentFiscalYear = useCurrentFiscalYear(); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isAddLineModalOpen, setIsAddLineModalOpen] = useState(false); const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); const [isConvertModalOpen, setIsConvertModalOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [selectedOrder, setSelectedOrder] = useState(null); const [selectedLinesToInvoice, setSelectedLinesToInvoice] = useState([]); const [searchText, setSearchText] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [addLineMode, setAddLineMode] = useState<'product' | 'freetext'>('product'); const [selectedProductId, setSelectedProductId] = useState(null); const [createForm] = Form.useForm(); const [addLineForm] = Form.useForm(); const [cancelForm] = Form.useForm(); // Fetch orders const { data: orders = [], isLoading: loading, error, refetch, } = useOrders(company?.id); // Fetch customers for dropdown const { data: customers = [] } = useActiveCustomers(company?.id); // Fetch products for dropdown const { data: products = [] } = useActiveProducts(company?.id); // Mutations const createOrderMutation = useCreateOrder(); const addOrderLineMutation = useAddOrderLine(); const confirmOrderMutation = useConfirmOrder(); const cancelOrderMutation = useCancelOrder(); const convertToInvoiceMutation = useConvertOrderToInvoice(); // Filter orders const filteredOrders = useMemo(() => { return orders.filter((order) => { const matchesSearch = searchText === '' || order.orderNumber.toLowerCase().includes(searchText.toLowerCase()) || order.customerName.toLowerCase().includes(searchText.toLowerCase()); const matchesStatus = statusFilter === 'all' || order.status === statusFilter; return matchesSearch && matchesStatus; }); }, [orders, searchText, statusFilter]); // Statistics const stats = useMemo(() => { const total = orders.length; const drafts = orders.filter((o) => o.status === 'draft').length; const confirmed = orders.filter((o) => o.status === 'confirmed').length; const totalValue = orders .filter((o) => o.status !== 'cancelled') .reduce((sum, o) => sum + o.amountTotal, 0); const invoicedValue = orders.reduce((sum, o) => sum + (o.amountTotal - (o.uninvoicedAmount ?? 0)), 0); return { total, drafts, confirmed, totalValue, invoicedValue }; }, [orders]); const handleCreateOrder = () => { createForm.resetFields(); createForm.setFieldsValue({ orderDate: 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: CreateOrderInput = { companyId: company.id, fiscalYearId: currentFiscalYear.id, customerId: values.customerId, orderDate: values.orderDate?.toISOString(), expectedDeliveryDate: values.expectedDeliveryDate?.toISOString(), notes: values.notes || undefined, reference: values.reference || undefined, }; const result = await createOrderMutation.mutateAsync(input); showSuccess('Ordre oprettet'); setIsCreateModalOpen(false); createForm.resetFields(); setSelectedOrder(result); setIsDrawerOpen(true); } catch (err) { if (err instanceof Error) { showError(err); } } }; const handleViewOrder = (order: Order) => { setSelectedOrder(order); setIsDrawerOpen(true); }; const handleOpenAddLineModal = () => { addLineForm.resetFields(); addLineForm.setFieldsValue({ quantity: 1, vatCode: 'S25', }); setAddLineMode('product'); setSelectedProductId(null); setIsAddLineModalOpen(true); }; const handleProductSelect = (productId: string) => { setSelectedProductId(productId); const product = products.find((p: Product) => p.id === productId); if (product) { addLineForm.setFieldsValue({ description: product.name, unitPrice: product.unitPrice, unit: product.unit || 'stk', vatCode: product.vatCode || 'S25', }); } }; const handleSubmitAddLine = async () => { if (!selectedOrder) return; try { const values = await addLineForm.validateFields(); const input: AddOrderLineInput = { orderId: selectedOrder.id, productId: addLineMode === 'product' && selectedProductId ? selectedProductId : undefined, description: values.description, quantity: Number(values.quantity), unitPrice: Number(values.unitPrice), unit: values.unit || undefined, discountPercent: values.discountPercent ? Number(values.discountPercent) : undefined, vatCode: values.vatCode, }; const updatedOrder = await addOrderLineMutation.mutateAsync(input); showSuccess('Linje tilføjet'); setIsAddLineModalOpen(false); addLineForm.resetFields(); setSelectedProductId(null); setSelectedOrder(updatedOrder); } catch (err) { if (err instanceof Error) { showError(err); } } }; const handleConfirmOrder = async () => { if (!selectedOrder) return; if (selectedOrder.lines.length === 0) { showWarning('Tilføj mindst en linje før bekræftelse'); return; } try { await confirmOrderMutation.mutateAsync(selectedOrder.id); showSuccess('Ordre bekræftet'); // Refresh would happen via query invalidation } catch (err) { if (err instanceof Error) { showError(err); } } }; const handleOpenCancelModal = () => { cancelForm.resetFields(); setIsCancelModalOpen(true); }; const handleSubmitCancel = async () => { if (!selectedOrder) return; try { const values = await cancelForm.validateFields(); const input: CancelOrderInput = { orderId: selectedOrder.id, reason: values.reason, }; await cancelOrderMutation.mutateAsync(input); showSuccess('Ordre annulleret'); setIsCancelModalOpen(false); cancelForm.resetFields(); } catch (err) { if (err instanceof Error) { showError(err); } } }; const handleOpenConvertModal = () => { if (!selectedOrder) return; // Pre-select uninvoiced lines const uninvoicedLines = selectedOrder.lines .filter((line) => !line.isInvoiced) .map((line) => line.lineNumber); setSelectedLinesToInvoice(uninvoicedLines); setIsConvertModalOpen(true); }; const handleSubmitConvert = async () => { if (!selectedOrder || selectedLinesToInvoice.length === 0) { showWarning('Vælg mindst en linje at fakturere'); return; } try { const input: InvoiceOrderLinesInput = { orderId: selectedOrder.id, lineNumbers: selectedLinesToInvoice, }; const invoice = await convertToInvoiceMutation.mutateAsync(input); showSuccess(`Faktura ${invoice.invoiceNumber} oprettet fra ordre`); setIsConvertModalOpen(false); setSelectedLinesToInvoice([]); // Refresh the selected order to show updated invoice tracking refetch(); } catch (err) { if (err instanceof Error) { showError(err); } } }; // Check if order can show the convert to invoice button const canShowConvertToInvoice = (order: Order): boolean => { if (order.status === 'cancelled' || order.status === 'fully_invoiced') { return false; } // Show for draft (disabled) and confirmed/partially_invoiced (enabled) return order.status === 'draft' || order.lines.some((line) => !line.isInvoiced); }; // Check if convert to invoice button should be disabled const isConvertToInvoiceDisabled = (order: Order): boolean => { if (order.status === 'draft') { return true; // Must confirm order first } return !order.lines.some((line) => !line.isInvoiced); }; const columns: ColumnsType = [ { title: 'Ordrenr.', dataIndex: 'orderNumber', key: 'orderNumber', width: 140, sorter: (a, b) => a.orderNumber.localeCompare(b.orderNumber), render: (value: string) => {value}, }, { title: 'Kunde', dataIndex: 'customerName', key: 'customerName', sorter: (a, b) => a.customerName.localeCompare(b.customerName), ellipsis: true, }, { title: 'Dato', dataIndex: 'orderDate', key: 'orderDate', width: 100, sorter: (a, b) => (a.orderDate || '').localeCompare(b.orderDate || ''), render: (value: string | undefined) => (value ? formatDate(value) : '-'), }, { title: 'Forventet levering', dataIndex: 'expectedDeliveryDate', key: 'expectedDeliveryDate', width: 130, 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) => , }, { title: 'Faktureret', dataIndex: 'amountInvoiced', key: 'amountInvoiced', width: 120, align: 'right', render: (value: number, record: Order) => record.status === 'cancelled' ? '-' : , }, { title: 'Status', dataIndex: 'status', key: 'status', width: 140, align: 'center', filters: [ { text: 'Kladde', value: 'draft' }, { text: 'Bekræftet', value: 'confirmed' }, { text: 'Delvist faktureret', value: 'partially_invoiced' }, { text: 'Fuldt faktureret', value: 'fully_invoiced' }, { text: 'Annulleret', value: 'cancelled' }, ], onFilter: (value, record) => record.status === value, render: (value: OrderStatus) => ( {ORDER_STATUS_LABELS[value]} ), }, { title: '', key: 'actions', width: 80, align: 'center', render: (_: unknown, record: Order) => ( } /> {/* Error State */} {error && ( refetch()}> Prøv igen } /> )} {/* Statistics */} formatCurrency(value as number)} /> {/* Filters */} } value={searchText} onChange={(e) => setSearchText(e.target.value)} style={{ width: 250 }} allowClear /> (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) } options={customers.map((c: Customer) => ({ value: c.id, label: `${c.customerNumber} - ${c.name}`, }))} /> {/* Order Detail Drawer */} Ordre {selectedOrder.orderNumber} {ORDER_STATUS_LABELS[selectedOrder.status]} ) } placement="right" width={700} open={isDrawerOpen} onClose={() => { setIsDrawerOpen(false); setSelectedOrder(null); }} extra={ selectedOrder && ( {selectedOrder.status === 'draft' && ( <> )} {canShowConvertToInvoice(selectedOrder) && ( )} {selectedOrder.status !== 'cancelled' && selectedOrder.status !== 'fully_invoiced' && ( )} ) } > {selectedOrder && (
{selectedOrder.customerName} {selectedOrder.orderDate ? formatDate(selectedOrder.orderDate) : '-'} {selectedOrder.expectedDeliveryDate ? formatDate(selectedOrder.expectedDeliveryDate) : '-'} {selectedOrder.reference && ( {selectedOrder.reference} )} Linjer {selectedOrder.lines.length > 0 ? ( { const linkedProduct = line.productId ? products.find((p: Product) => p.id === line.productId) : null; return ( {line.description} {line.productId && ( }> {linkedProduct?.productNumber || 'Produkt'} )} {line.isInvoiced && ( }> Faktureret )} } description={ {line.quantity} {line.unit || 'stk'} x {formatCurrency(line.unitPrice)} {line.discountPercent > 0 && ( -{line.discountPercent}% )} {line.vatCode} {line.isInvoiced && line.invoicedAt && ( Faktureret: {dayjs(line.invoicedAt).format('DD/MM/YYYY')} )} } /> ); }} /> ) : ( )} {selectedOrder.notes && ( <> Bemærkninger:

{selectedOrder.notes}

)} {selectedOrder.cancelledReason && ( <> Annulleringsårsag:

{selectedOrder.cancelledReason}

)}
Beløb ex. moms: {formatCurrency(selectedOrder.amountExVat)}
Moms: {formatCurrency(selectedOrder.amountVat)}
Total: {formatCurrency(selectedOrder.amountTotal)}
{(selectedOrder.uninvoicedAmount ?? selectedOrder.amountTotal) < selectedOrder.amountTotal && (
Faktureret: {formatCurrency(selectedOrder.amountTotal - (selectedOrder.uninvoicedAmount ?? 0))}
)} {(selectedOrder.uninvoicedAmount ?? 0) > 0 && selectedOrder.status !== 'cancelled' && (
Resterende: {formatCurrency(selectedOrder.uninvoicedAmount ?? 0)}
)}
)} {/* Cancel Order Modal */} setIsCancelModalOpen(false)} onOk={handleSubmitCancel} okText="Annuller ordre" okButtonProps={{ danger: true }} cancelText="Fortryd" confirmLoading={cancelOrderMutation.isPending} >
{/* Add Line Modal */} { setIsAddLineModalOpen(false); setSelectedProductId(null); }} onOk={handleSubmitAddLine} okText="Tilføj" cancelText="Annuller" confirmLoading={addOrderLineMutation.isPending} width={550} >
{ setAddLineMode(e.target.value); setSelectedProductId(null); addLineForm.resetFields(); addLineForm.setFieldsValue({ quantity: 1, vatCode: 'S25', }); }} optionType="button" buttonStyle="solid" > Vælg produkt Fritekst {addLineMode === 'product' && (