books/frontend/src/pages/CompanySetupWizard.tsx
Nicolaj Hartmann 381156ade7 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

501 lines
16 KiB
TypeScript

import { useState } from 'react';
import {
Card,
Steps,
Form,
Input,
Select,
Button,
Space,
Typography,
Result,
Divider,
Alert,
Row,
Col,
} from 'antd';
import { showError } from '@/lib/errorHandling';
import {
ShopOutlined,
BankOutlined,
CheckCircleOutlined,
ArrowLeftOutlined,
ArrowRightOutlined,
RocketOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useCreateCompany } from '@/api/mutations/companyMutations';
import { useCompanyStore } from '@/stores/companyStore';
import { useMyCompanies } from '@/api/queries/companyQueries';
import { colors } from '@/styles/designTokens';
import { validateCVRModulus11 } from '@/lib/formatters';
const { Title, Text, Paragraph } = Typography;
interface CompanyFormValues {
name: string;
cvr?: string;
country: string;
currency: string;
fiscalYearStartMonth: number;
vatRegistered: boolean;
vatPeriodFrequency?: 'MONTHLY' | 'QUARTERLY' | 'HALFYEARLY';
}
const vatPeriodOptions = [
{ value: 'MONTHLY', label: 'Månedlig' },
{ value: 'QUARTERLY', label: 'Kvartalsvis' },
{ value: 'HALFYEARLY', label: 'Halvårlig' },
];
const monthOptions = [
{ value: 1, label: 'Januar' },
{ value: 2, label: 'Februar' },
{ value: 3, label: 'Marts' },
{ value: 4, label: 'April' },
{ value: 5, label: 'Maj' },
{ value: 6, label: 'Juni' },
{ value: 7, label: 'Juli' },
{ value: 8, label: 'August' },
{ value: 9, label: 'September' },
{ value: 10, label: 'Oktober' },
{ value: 11, label: 'November' },
{ value: 12, label: 'December' },
];
export default function CompanySetupWizard() {
const [currentStep, setCurrentStep] = useState(0);
const [form] = Form.useForm<CompanyFormValues>();
const navigate = useNavigate();
const createCompany = useCreateCompany();
const { setActiveCompany } = useCompanyStore();
const { refetch: refetchCompanies } = useMyCompanies();
const [createdCompanyName, setCreatedCompanyName] = useState('');
// Store form values in state for reliable confirmation display
const [formSnapshot, setFormSnapshot] = useState<Partial<CompanyFormValues>>({});
// Watch vatRegistered for conditional rendering in step 2 (while on that step)
const vatRegistered = Form.useWatch('vatRegistered', form);
const handleNext = async () => {
try {
if (currentStep === 0) {
await form.validateFields(['name', 'cvr']);
} else if (currentStep === 1) {
await form.validateFields(['country', 'currency', 'fiscalYearStartMonth']);
} else if (currentStep === 2) {
await form.validateFields(['vatRegistered', 'vatPeriodFrequency']);
// Backend requires CVR for VAT-registered companies
const currentValues = form.getFieldsValue();
if (currentValues.vatRegistered && !currentValues.cvr?.trim()) {
showError('CVR er påkrævet for momsregistrerede virksomheder');
setCurrentStep(0); // Navigate back to step 0 where CVR is entered
return;
}
}
// Capture current form values before moving to next step
const currentValues = form.getFieldsValue();
setFormSnapshot(prev => ({ ...prev, ...currentValues }));
setCurrentStep(currentStep + 1);
} catch {
// Validation failed, don't proceed
}
};
const handleBack = () => {
setCurrentStep(currentStep - 1);
};
const handleSubmit = async () => {
try {
// Use formSnapshot which was captured during navigation
const values = formSnapshot;
// Defensive validation
if (!values.name || values.name.trim() === '') {
showError('Virksomhedsnavn er påkrævet');
setCurrentStep(0);
return;
}
// Debug logging
console.log('Creating company with values:', values);
const company = await createCompany.mutateAsync({
name: values.name.trim(),
cvr: values.cvr?.trim() || undefined,
country: values.country || 'DK',
currency: values.currency || 'DKK',
fiscalYearStartMonth: values.fiscalYearStartMonth ?? 1,
vatRegistered: values.vatRegistered ?? false,
vatPeriodFrequency: values.vatRegistered ? values.vatPeriodFrequency : undefined,
});
setCreatedCompanyName(values.name);
// Refetch companies to get the new one with role
const { data: companies } = await refetchCompanies();
const newCompany = companies?.find((c) => c.id === company.id);
if (newCompany) {
setActiveCompany(newCompany);
}
setCurrentStep(4); // Success step
} catch (error) {
console.error('Company creation failed:', error);
showError(error, 'Kunne ikke oprette virksomhed');
}
};
const handleGoToDashboard = () => {
navigate('/');
};
const steps = [
{
title: 'Virksomhed',
icon: <ShopOutlined />,
},
{
title: 'Regnskab',
icon: <BankOutlined />,
},
{
title: 'Moms',
icon: <BankOutlined />,
},
{
title: 'Bekræft',
icon: <CheckCircleOutlined />,
},
];
const renderStepContent = () => {
switch (currentStep) {
case 0:
return (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={4}>Fortæl os om din virksomhed</Title>
<Text type="secondary">
Vi bruger disse oplysninger til at oprette din kontoplan og konfigurere systemet.
</Text>
</div>
<Form.Item
name="name"
label="Virksomhedsnavn"
rules={[{ required: true, message: 'Indtast virksomhedsnavn' }]}
>
<Input
size="large"
placeholder="F.eks. Min Virksomhed ApS"
prefix={<ShopOutlined />}
/>
</Form.Item>
<Form.Item
name="cvr"
label="CVR-nummer"
extra="Valgfrit - kan tilføjes senere"
rules={[
{
pattern: /^\d{8}$/,
message: 'CVR-nummer skal være 8 cifre',
},
{
validator: (_, value) => {
if (!value || value.length !== 8) return Promise.resolve();
if (!validateCVRModulus11(value)) {
return Promise.reject('Ugyldigt CVR-nummer (modulus 11 check fejlet)');
}
return Promise.resolve();
},
},
]}
>
<Input
size="large"
placeholder="12345678"
maxLength={8}
/>
</Form.Item>
</Space>
);
case 1:
return (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={4}>Regnskabsindstillinger</Title>
<Text type="secondary">
Konfigurer dine grundlæggende regnskabsindstillinger.
</Text>
</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="country"
label="Land"
rules={[{ required: true, message: 'Vælg land' }]}
>
<Select size="large" placeholder="Vælg land">
<Select.Option value="DK">Danmark</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="currency"
label="Valuta"
rules={[{ required: true, message: 'Vælg valuta' }]}
>
<Select size="large" placeholder="Vælg valuta">
<Select.Option value="DKK">DKK - Danske kroner</Select.Option>
<Select.Option value="EUR">EUR - Euro</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
name="fiscalYearStartMonth"
label="Regnskabsår starter"
rules={[{ required: true, message: 'Vælg startmåned' }]}
extra="De fleste virksomheder bruger januar (kalenderår)"
>
<Select
size="large"
placeholder="Vælg måned"
options={monthOptions}
/>
</Form.Item>
</Space>
);
case 2:
return (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={4}>Momsregistrering</Title>
<Text type="secondary">
Er din virksomhed momsregistreret hos SKAT?
</Text>
</div>
<Form.Item
name="vatRegistered"
label="Momsregistreret"
rules={[{ required: true, message: 'Angiv om virksomheden er momsregistreret' }]}
>
<Select size="large" placeholder="Vælg">
<Select.Option value={true}>Ja, virksomheden er momsregistreret</Select.Option>
<Select.Option value={false}>Nej, ikke momsregistreret</Select.Option>
</Select>
</Form.Item>
{vatRegistered && (
<Form.Item
name="vatPeriodFrequency"
label="Momsperiode"
rules={[{ required: true, message: 'Vælg momsperiode' }]}
>
<Select
size="large"
placeholder="Vælg momsperiode"
options={vatPeriodOptions}
/>
</Form.Item>
)}
{vatRegistered === false && (
<Alert
message="Ikke momsregistreret"
description="Du kan altid registrere virksomheden for moms senere via SKAT's hjemmeside."
type="info"
showIcon
/>
)}
</Space>
);
case 3:
return (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={4}>Bekræft dine oplysninger</Title>
<Text type="secondary">
Gennemgå dine oplysninger før vi opretter virksomheden.
</Text>
</div>
<Card size="small">
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Text type="secondary">Virksomhedsnavn</Text>
<br />
<Text strong style={{ fontSize: 16 }}>{formSnapshot.name || '-'}</Text>
</div>
<Divider style={{ margin: '12px 0' }} />
{formSnapshot.cvr && (
<>
<div>
<Text type="secondary">CVR-nummer</Text>
<br />
<Text strong>{formSnapshot.cvr}</Text>
</div>
<Divider style={{ margin: '12px 0' }} />
</>
)}
<Row gutter={24}>
<Col span={8}>
<Text type="secondary">Land</Text>
<br />
<Text strong>{formSnapshot.country === 'DK' ? 'Danmark' : formSnapshot.country || '-'}</Text>
</Col>
<Col span={8}>
<Text type="secondary">Valuta</Text>
<br />
<Text strong>{formSnapshot.currency || '-'}</Text>
</Col>
<Col span={8}>
<Text type="secondary">Regnskabsår starter</Text>
<br />
<Text strong>
{monthOptions.find((m) => m.value === formSnapshot.fiscalYearStartMonth)?.label || '-'}
</Text>
</Col>
</Row>
<Divider style={{ margin: '12px 0' }} />
<div>
<Text type="secondary">Momsregistrering</Text>
<br />
<Text strong>
{formSnapshot.vatRegistered
? `Ja - ${vatPeriodOptions.find((v) => v.value === formSnapshot.vatPeriodFrequency)?.label || 'Vælg periode'}`
: 'Ikke momsregistreret'}
</Text>
</div>
</Space>
</Card>
<Alert
message="Klar til at starte"
description="Når du opretter virksomheden, genereres automatisk en dansk standardkontoplan med alle nødvendige konti."
type="success"
showIcon
/>
</Space>
);
case 4:
return (
<Result
status="success"
icon={<RocketOutlined style={{ color: colors.primary }} />}
title={`${createdCompanyName} er oprettet!`}
subTitle="Din virksomhed er klar til brug. Vi har oprettet en komplet dansk kontoplan for dig."
extra={[
<Button
type="primary"
size="large"
key="dashboard"
onClick={handleGoToDashboard}
>
til dashboard
</Button>,
]}
/>
);
default:
return null;
}
};
return (
<div
style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 24,
}}
>
<Card
style={{
maxWidth: 600,
width: '100%',
borderRadius: 16,
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
}}
>
{currentStep < 4 && (
<>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<ShopOutlined style={{ fontSize: 48, color: colors.primary }} />
<Title level={2} style={{ marginTop: 16, marginBottom: 8 }}>
Velkommen til Books
</Title>
<Paragraph type="secondary">
Lad os oprette din første virksomhed
</Paragraph>
</div>
<Steps
current={currentStep}
items={steps}
style={{ marginBottom: 32 }}
size="small"
/>
</>
)}
<Form
form={form}
layout="vertical"
preserve={true}
initialValues={{
country: 'DK',
currency: 'DKK',
fiscalYearStartMonth: 1,
vatRegistered: false,
}}
>
{renderStepContent()}
</Form>
{currentStep < 4 && (
<div style={{ marginTop: 32, display: 'flex', justifyContent: 'space-between' }}>
{currentStep > 0 ? (
<Button size="large" onClick={handleBack} icon={<ArrowLeftOutlined />}>
Tilbage
</Button>
) : (
<div />
)}
{currentStep < 3 ? (
<Button type="primary" size="large" onClick={handleNext}>
Næste <ArrowRightOutlined />
</Button>
) : (
<Button
type="primary"
size="large"
onClick={handleSubmit}
loading={createCompany.isPending}
>
Opret virksomhed
</Button>
)}
</div>
)}
</Card>
</div>
);
}