feat(handelsware-datev): Modulseite mit Import, Konten und Einstellungen

- Reiter Import: Vorschau mit neu-Markierung, Buchungsdatum (TTMM) aus dem Dateinamen, Download speichert neue Konten
- Reiter Konten: anlegen, bearbeiten, loeschen mit Rueckfrage, CSV-Import (ersetzt alles, nach Rueckfrage) und -Export
- Reiter Einstellungen nur fuer Administratoren; Texte de/en

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-02 11:47:09 +02:00
parent 44c1d4351f
commit 42b89f110b
15 changed files with 1726 additions and 1 deletions
+143
View File
@@ -0,0 +1,143 @@
/**
* Handelsware — API-Client (quick-261002-fm5). Konsumiert
* `/modules/handelsware-datev`. Die Excel-Datei geht als Multipart-Feld `file`;
* beim Export zusaetzlich `buchungsdatum` und `newAccounts` (JSON-Text der von
* der Vorschau gemeldeten neuen Konten) — die API liest alle Zeilen erneut aus
* derselben Datei, damit die TXT nicht von der Arbeitsmappe abweichen kann.
*/
import { accountingRequest, type FileResponse } from '@/lib/accounting-request';
export type { FileResponse } from '@/lib/accounting-request';
export { AccountingRequestError as HandelswareRequestError } from '@/lib/accounting-request';
const BASE = '/modules/handelsware-datev';
export interface HandelswareSettings {
erloeskonto: number | null;
startGegenkonto: number | null;
configured: boolean;
}
export interface HandelswareSettingsInput {
erloeskonto: number;
startGegenkonto: number;
}
export interface HandelswareAccount {
id: string;
name: string;
gegenkonto: number;
erloeskonto: number;
}
export interface HandelswareAccountInput {
name: string;
gegenkonto: number;
erloeskonto: number;
}
export interface NewAccount {
name: string;
gegenkonto: number;
erloeskonto: number;
}
export interface PreviewRow {
line: number;
buchungstext: string;
umsatz: string;
sollHaben: 'S' | 'H';
gegenkonto: number;
erloeskonto: number;
isNew: boolean;
}
export interface RowError {
line: number;
code: string;
message: string;
}
export interface HandelswarePreview {
headerText: string;
suggestedBuchungsdatum: string;
exportFilename: string;
rows: PreviewRow[];
newAccounts: NewAccount[];
rowErrors: RowError[];
}
export type HandelswareExportResult = FileResponse & { createdCount: number };
const DAYS_PER_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
/** TTMM: vier Ziffern, Monat 1-12, Tag passend zum Monat (Februar bis 29) — wie die API. */
export function isValidBuchungsdatum(ttmm: string): boolean {
if (!/^\d{4}$/.test(ttmm)) return false;
const day = Number.parseInt(ttmm.slice(0, 2), 10);
const month = Number.parseInt(ttmm.slice(2, 4), 10);
if (month < 1 || month > 12) return false;
return day >= 1 && day <= DAYS_PER_MONTH[month - 1];
}
function fileForm(file: File): FormData {
const form = new FormData();
form.append('file', file);
return form;
}
export function getHandelswareSettings(): Promise<HandelswareSettings> {
return accountingRequest(`${BASE}/settings`);
}
export function saveHandelswareSettings(
input: HandelswareSettingsInput,
): Promise<HandelswareSettings> {
return accountingRequest(`${BASE}/settings`, { method: 'PUT', json: input });
}
export function previewHandelsware(file: File): Promise<HandelswarePreview> {
return accountingRequest(`${BASE}/preview`, { method: 'POST', form: fileForm(file) });
}
export function exportHandelsware(
file: File,
buchungsdatum: string,
newAccounts: NewAccount[],
): Promise<HandelswareExportResult> {
const form = fileForm(file);
form.append('buchungsdatum', buchungsdatum);
form.append('newAccounts', JSON.stringify(newAccounts));
return accountingRequest(`${BASE}/export`, { method: 'POST', form });
}
export function listAccounts(): Promise<HandelswareAccount[]> {
return accountingRequest(`${BASE}/accounts`);
}
export function createAccount(input: HandelswareAccountInput): Promise<HandelswareAccount> {
return accountingRequest(`${BASE}/accounts`, { method: 'POST', json: input });
}
export function updateAccount(
id: string,
input: HandelswareAccountInput,
): Promise<HandelswareAccount> {
return accountingRequest(`${BASE}/accounts/${encodeURIComponent(id)}`, {
method: 'PUT',
json: input,
});
}
export function deleteAccount(id: string): Promise<{ deleted: true }> {
return accountingRequest(`${BASE}/accounts/${encodeURIComponent(id)}`, { method: 'DELETE' });
}
export function importAccountsCsv(file: File): Promise<{ count: number }> {
return accountingRequest(`${BASE}/accounts/import-csv`, { method: 'POST', form: fileForm(file) });
}
export function exportAccountsCsv(): Promise<FileResponse> {
return accountingRequest(`${BASE}/accounts/export-csv`);
}