c2a5e509-7a51-4eb1-8d84-a68d55cde77a
This commit is contained in:
1 parent
85c4006803
commit
9c58e29472
11 files changed
+493
-44
No files matched your search
@@ -86,6 +86,7 @@ interface AccountingData {
|
||||
const VAT_STATUS_META: Record<string, { label: string; cls: string }> = {
|
||||
draft: { label: 'Entwurf', cls: 'bg-muted text-foreground' },
|
||||
computed: { label: 'Berechnet', cls: 'bg-foreground/50 text-background' },
|
||||
prepared: { label: 'XML erstellt', cls: 'bg-foreground/70 text-background' },
|
||||
submitted: { label: 'Übermittelt', cls: 'bg-foreground/70 text-background' },
|
||||
accepted: { label: 'Akzeptiert', cls: 'bg-foreground text-background' },
|
||||
corrected: { label: 'Korrigiert', cls: 'bg-muted text-muted-foreground' },
|
||||
@@ -100,6 +101,7 @@ const CATEGORY_LABELS: Record<string, string> = {
|
||||
export function AccountingView() {
|
||||
const { data, loading, refetch } = useFetch<AccountingData>('/api/aos/accounting')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [preparedXml, setPreparedXml] = useState<string | null>(null)
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
@@ -114,15 +116,24 @@ export function AccountingView() {
|
||||
|
||||
const submitVat = async (vatId: string) => {
|
||||
setSubmitting(true)
|
||||
const t = toast.loading('Voranmeldung wird an ELSTER übermittelt (ERiC)…')
|
||||
setPreparedXml(null)
|
||||
const t = toast.loading('UStVA-XML wird erstellt und validiert…')
|
||||
try {
|
||||
const r = await fetch(`/api/aos/accounting/vat/${vatId}/submit`, { method: 'PATCH' })
|
||||
if (!r.ok) {
|
||||
const err = await r.json().catch(() => ({}))
|
||||
throw new Error(err.error || 'submit failed')
|
||||
}
|
||||
const res = await r.json()
|
||||
toast.success(`Akzeptiert — ELSTER-Ref: ${res.elsterRef}`, { id: t })
|
||||
if (!r.ok) {
|
||||
if (res.details && Array.isArray(res.details)) {
|
||||
throw new Error('Validierung fehlgeschlagen: ' + res.details.join('; '))
|
||||
}
|
||||
throw new Error(res.error || 'submit failed')
|
||||
}
|
||||
if (res.transmitted === false) {
|
||||
// Honest path: XML prepared, transmission requires ERiC certificate
|
||||
setPreparedXml(res.xml)
|
||||
toast.success(`UStVA-XML erstellt (${res.xmlSize} Bytes) — Übermittlung an ERiC ausstehend`, { id: t, duration: 6000 })
|
||||
} else {
|
||||
toast.success('An ELSTER übermittelt', { id: t })
|
||||
}
|
||||
refetch()
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Übermittlung fehlgeschlagen', { id: t })
|
||||
@@ -217,10 +228,10 @@ export function AccountingView() {
|
||||
</Badge>
|
||||
</p>
|
||||
</div>
|
||||
{currentVat.status === 'computed' && (
|
||||
{(currentVat.status === 'computed' || currentVat.status === 'prepared') && (
|
||||
<Button size="sm" className="gap-1.5" onClick={() => submitVat(currentVat.id)} disabled={submitting}>
|
||||
{submitting ? <Loader2 className="size-3.5 animate-spin" /> : <Send className="size-3.5" />}
|
||||
An ELSTER übermitteln
|
||||
{currentVat.status === 'prepared' ? 'UStVA-XML neu erstellen' : 'UStVA-XML erstellen'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -254,8 +265,41 @@ export function AccountingView() {
|
||||
)}
|
||||
<div className="mt-3 rounded-md border border-dashed border-border p-3 text-[11px] text-muted-foreground">
|
||||
<p className="font-medium text-foreground">ELSTER-Übermittlung (ERiC)</p>
|
||||
<p className="mt-1">Die Kennzahlen werden aus den gebuchten Rechnungen und Ausgaben berechnet (Ist-Versteuerung). Übermittlung via ERiC-Schnittstelle. Vor dem Absenden: Vorprüfung jeder Kennzahl mit Herkunftsnachweis.</p>
|
||||
<p className="mt-1">Die Kennzahlen werden aus den gebuchten Rechnungen und Ausgaben berechnet (Ist-Versteuerung). Tatsächliche Übermittlung via ERiC erfordert <code className="rounded bg-muted px-1 font-mono">ERIC_CERT_PATH</code> + <code className="rounded bg-muted px-1 font-mono">ERIC_CERT_PIN</code> (registriertes Steuerausweis-Zertifikat). Ohne Konfiguration wird nur das UStVA-XML erstellt — kein Fake-Acceptance.</p>
|
||||
</div>
|
||||
|
||||
{/* Prepared XML preview (real, not mock) */}
|
||||
{preparedXml && (
|
||||
<div className="mt-3 rounded-md border border-foreground/30 bg-foreground/5 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
UStVA-XML erstellt ({preparedXml.length} Bytes)
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 gap-1 text-[10px]"
|
||||
onClick={() => {
|
||||
const blob = new Blob([preparedXml], { type: 'application/xml' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `UStVA-${currentVat.periodYear}-${String(currentVat.periodMonth).padStart(2, '0')}.xml`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}}
|
||||
>
|
||||
XML herunterladen
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="mt-2 max-h-40 overflow-auto rounded bg-background p-2 text-[10px] leading-relaxed [scrollbar-width:thin]">
|
||||
{preparedXml.slice(0, 800)}{preparedXml.length > 800 ? '\n… (gekürzt)' : ''}
|
||||
</pre>
|
||||
<p className="mt-2 text-[10px] text-muted-foreground">
|
||||
Status: <span className="font-medium">prepared</span> — XML kann an Steuerberater (DATEV) oder manuell an das ELSTER-Portal übergeben werden. Kein automatischer Versand ohne ERiC-Zertifikat.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -428,7 +472,39 @@ export function AccountingView() {
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm">Bankbewegungen (CAMT.053 Import)</CardTitle>
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1.5 text-xs">
|
||||
<input
|
||||
type="file"
|
||||
accept=".xml,application/xml,text/xml"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const t = toast.loading(`CAMT.053 wird geparst: ${file.name}…`)
|
||||
try {
|
||||
const xml = await file.text()
|
||||
const r = await fetch('/api/aos/accounting/bank/import', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ xml }),
|
||||
})
|
||||
const res = await r.json()
|
||||
if (!r.ok) throw new Error(res.error || 'import failed')
|
||||
toast.success(`${res.imported} Bewegungen importiert (Batch ${res.batchId.slice(0, 12)}…)`, { id: t })
|
||||
refetch()
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Import fehlgeschlagen', { id: t })
|
||||
} finally {
|
||||
e.target.value = ''
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
id="camt-import"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 gap-1.5 text-xs"
|
||||
onClick={() => document.getElementById('camt-import')?.click()}
|
||||
>
|
||||
<Banknote className="size-3.5" />
|
||||
CAMT.053 importieren
|
||||
</Button>
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { PageHeader } from '../primitives'
|
||||
import { formatEuro, formatDate, daysUntil } from '@/lib/format'
|
||||
import { formatEuro, formatDate, formatDateTime, daysUntil } from '@/lib/format'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
@@ -280,17 +280,23 @@ export function ContractsView() {
|
||||
<div className="flex items-center gap-2">
|
||||
<PenLine className="size-4 text-muted-foreground" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wide">E-Signatur</span>
|
||||
{selected.signedAt && (
|
||||
<Badge variant="outline" className="ml-auto text-[9px]">AdES (eIDAS Art. 25 Abs. 2)</Badge>
|
||||
)}
|
||||
</div>
|
||||
{selected.signedAt ? (
|
||||
<div className="mt-2 space-y-1 text-[11px]">
|
||||
<SigRow label="Unterzeichnet am" value={formatDate(selected.signedAt)} />
|
||||
<SigRow label="Unterzeichnet am" value={formatDateTime(selected.signedAt)} />
|
||||
<SigRow label="Unterzeichner" value={selected.signedBy ?? '—'} />
|
||||
<SigRow label="IP (Portal)" value={selected.signedIp ?? '—'} mono />
|
||||
<SigRow label="Signatur-Token" value={selected.signToken ?? '—'} mono />
|
||||
<SigRow label="Client-IP" value={selected.signedIp ?? '—'} mono />
|
||||
<SigRow label="Akzeptanz-Token" value={selected.signToken ?? '—'} mono />
|
||||
<p className="pt-1 text-[10px] text-muted-foreground">
|
||||
Fortgeschrittene elektronische Signatur (AdES). Keine qualifizierte Signatur (QES, eIDAS Art. 26) — für Verträge mit gesetzlicher QES-Pflicht (z. B. Grundstückskauf) einen qualifizierten VDU einbinden.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">
|
||||
Noch nicht unterzeichnet. Versand an Kunden-Portal zur E-Signatur möglich.
|
||||
Noch nicht unterzeichnet. Versand an Kunden-Portal zur AdES-Signatur (eIDAS Art. 25 Abs. 2) möglich.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,10 @@ export function SetupWizardView() {
|
||||
const [step, setStep] = useState(1)
|
||||
const [completed, setCompleted] = useState<Set<number>>(new Set())
|
||||
const [mailTesting, setMailTesting] = useState(false)
|
||||
const [mailResult, setMailResult] = useState<{ smtp: { ok: boolean; message: string }; imap: { ok: boolean; message: string } } | null>(null)
|
||||
const [mailResult, setMailResult] = useState<{
|
||||
smtp: { ok: boolean; host: string | null; port: number | null; message: string; banner?: string; error?: string }
|
||||
imap: { ok: boolean; host: string | null; port: number | null; message: string; banner?: string; error?: string }
|
||||
} | null>(null)
|
||||
const [modules, setModules] = useState<Record<string, boolean>>({
|
||||
crm: true, leads: true, projects: true, licenses: true, contracts: true, expenses: true, estimates: true, support: true, finance: true,
|
||||
})
|
||||
@@ -299,12 +302,20 @@ export function SetupWizardView() {
|
||||
<span className={cn('size-1.5 rounded-full', mailResult.smtp.ok ? 'bg-foreground' : 'bg-muted-foreground/40')} />
|
||||
<span className="text-muted-foreground">SMTP:</span>
|
||||
<span>{mailResult.smtp.message}</span>
|
||||
{mailResult.smtp.banner && <code className="text-[9px] font-mono text-muted-foreground">[{mailResult.smtp.banner.slice(0, 50)}]</code>}
|
||||
</div>
|
||||
{mailResult.smtp.error && (
|
||||
<p className="ml-3.5 text-[10px] text-muted-foreground">Fehler: {mailResult.smtp.error}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('size-1.5 rounded-full', mailResult.imap.ok ? 'bg-foreground' : 'bg-muted-foreground/40')} />
|
||||
<span className="text-muted-foreground">IMAP:</span>
|
||||
<span>{mailResult.imap.message}</span>
|
||||
{mailResult.imap.banner && <code className="text-[9px] font-mono text-muted-foreground">[{mailResult.imap.banner.slice(0, 50)}]</code>}
|
||||
</div>
|
||||
{mailResult.imap.error && (
|
||||
<p className="ml-3.5 text-[10px] text-muted-foreground">Fehler: {mailResult.imap.error}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user