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>
501 lines
16 KiB
TypeScript
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}
|
|
>
|
|
Gå 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>
|
|
);
|
|
}
|