feat(lexware): add integration foundation
This commit is contained in:
parent
8d37eb29b3
commit
ffffb68898
28 changed files with 1150 additions and 11 deletions
18
frontend/athena/app/api/lexware/settings/route.ts
Normal file
18
frontend/athena/app/api/lexware/settings/route.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
|
||||
import { assertSameOrigin } from "@/lib/server/request-guards";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return proxyHermesRequest(request, "/lexware/settings");
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
const originError = assertSameOrigin(request);
|
||||
|
||||
if (originError) {
|
||||
return originError;
|
||||
}
|
||||
|
||||
return proxyHermesRequest(request, "/lexware/settings");
|
||||
}
|
||||
14
frontend/athena/app/api/lexware/test-connection/route.ts
Normal file
14
frontend/athena/app/api/lexware/test-connection/route.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
|
||||
import { assertSameOrigin } from "@/lib/server/request-guards";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const originError = assertSameOrigin(request);
|
||||
|
||||
if (originError) {
|
||||
return originError;
|
||||
}
|
||||
|
||||
return proxyHermesRequest(request, "/lexware/test-connection");
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
|
||||
import { assertSameOrigin } from "@/lib/server/request-guards";
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string; estimateId: string }>;
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
const originError = assertSameOrigin(request);
|
||||
|
||||
if (originError) {
|
||||
return originError;
|
||||
}
|
||||
|
||||
const { id, estimateId } = await params;
|
||||
return proxyHermesRequest(request, `/repairs/${id}/estimates/${estimateId}/lexware/prepare-invoice`);
|
||||
}
|
||||
|
|
@ -176,6 +176,7 @@ export default function RepairDetailPage({ params }: Params) {
|
|||
const canDeleteEstimates = hasPermission(currentUser, "repair_estimates.delete");
|
||||
const canSendEstimates = hasPermission(currentUser, "repair_estimates.send");
|
||||
const canRevokeEstimates = hasPermission(currentUser, "repair_estimates.revoke");
|
||||
const canLexwareExport = hasPermission(currentUser, "lexware.export");
|
||||
|
||||
async function createPublicLink() {
|
||||
if (!repair || !canManagePublicLink) return;
|
||||
|
|
@ -444,6 +445,7 @@ export default function RepairDetailPage({ params }: Params) {
|
|||
canDelete={canDeleteEstimates}
|
||||
canSend={canSendEstimates}
|
||||
canRevoke={canRevokeEstimates}
|
||||
canLexwareExport={canLexwareExport}
|
||||
/>
|
||||
</DetailSection>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { ExternalLink, MailCheck, Save, Send, ShieldCheck } from "lucide-react";
|
||||
import { ExternalLink, MailCheck, ReceiptText, Save, Send, ShieldCheck } from "lucide-react";
|
||||
|
||||
import { useToast } from "@/components/common/ToastProvider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/lib/api";
|
||||
import type {
|
||||
LexwareSettings,
|
||||
LexwareSettingsPayload,
|
||||
LexwareTestConnectionResponse,
|
||||
} from "@/types/lexware";
|
||||
import type {
|
||||
PublicLinksSettings,
|
||||
SettingsSource,
|
||||
|
|
@ -16,7 +21,7 @@ import type {
|
|||
SmtpTestResponse,
|
||||
} from "@/types/system-settings";
|
||||
|
||||
type SettingsTab = "smtp" | "public-links";
|
||||
type SettingsTab = "smtp" | "public-links" | "lexware";
|
||||
|
||||
const sourceLabels: Record<SettingsSource, string> = {
|
||||
database: "Admin-Konfiguration",
|
||||
|
|
@ -46,6 +51,18 @@ function emptySmtpSettings(): SmtpSettings {
|
|||
};
|
||||
}
|
||||
|
||||
function emptyLexwareSettings(): LexwareSettings {
|
||||
return {
|
||||
enabled: false,
|
||||
api_base_url: "https://api.lexware.io",
|
||||
api_key_is_set: false,
|
||||
organization_name: "",
|
||||
default_tax_rate: "19.00",
|
||||
default_payment_terms_days: 14,
|
||||
source: "missing",
|
||||
};
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { showToast } = useToast();
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>("smtp");
|
||||
|
|
@ -56,22 +73,28 @@ export default function SettingsPage() {
|
|||
repair_status_base_url: "",
|
||||
source: "missing",
|
||||
});
|
||||
const [lexware, setLexware] = useState<LexwareSettings>(emptyLexwareSettings());
|
||||
const [newLexwareApiKey, setNewLexwareApiKey] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [savingSmtp, setSavingSmtp] = useState(false);
|
||||
const [sendingTest, setSendingTest] = useState(false);
|
||||
const [savingPublicLinks, setSavingPublicLinks] = useState(false);
|
||||
const [savingLexware, setSavingLexware] = useState(false);
|
||||
const [testingLexware, setTestingLexware] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const loadSettings = useCallback(async () => {
|
||||
setError("");
|
||||
setLoading(true);
|
||||
try {
|
||||
const [smtpResponse, publicLinksResponse] = await Promise.all([
|
||||
const [smtpResponse, publicLinksResponse, lexwareResponse] = await Promise.all([
|
||||
api.get<SmtpSettings>("/system-settings/smtp"),
|
||||
api.get<PublicLinksSettings>("/system-settings/public-links"),
|
||||
api.get<LexwareSettings>("/lexware/settings"),
|
||||
]);
|
||||
setSmtp(smtpResponse.data);
|
||||
setPublicLinks(publicLinksResponse.data);
|
||||
setLexware(lexwareResponse.data);
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err, "Einstellungen konnten nicht geladen werden."));
|
||||
} finally {
|
||||
|
|
@ -158,6 +181,56 @@ export default function SettingsPage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function saveLexware() {
|
||||
setSavingLexware(true);
|
||||
try {
|
||||
const payload: LexwareSettingsPayload = {
|
||||
enabled: lexware.enabled,
|
||||
api_base_url: lexware.api_base_url,
|
||||
organization_name: lexware.organization_name,
|
||||
default_tax_rate: lexware.default_tax_rate,
|
||||
default_payment_terms_days: lexware.default_payment_terms_days,
|
||||
};
|
||||
|
||||
if (newLexwareApiKey) {
|
||||
payload.api_key = newLexwareApiKey;
|
||||
}
|
||||
|
||||
const response = await api.put<LexwareSettings>("/lexware/settings", payload);
|
||||
setLexware(response.data);
|
||||
setNewLexwareApiKey("");
|
||||
showToast({ type: "success", title: "Lexware-Konfiguration gespeichert" });
|
||||
} catch (err) {
|
||||
showToast({
|
||||
type: "error",
|
||||
title: "Lexware konnte nicht gespeichert werden",
|
||||
description: getErrorMessage(err, "Bitte prüfe die Lexware-Einstellungen."),
|
||||
});
|
||||
} finally {
|
||||
setSavingLexware(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function testLexwareConnection() {
|
||||
setTestingLexware(true);
|
||||
try {
|
||||
const response = await api.post<LexwareTestConnectionResponse>("/lexware/test-connection");
|
||||
showToast({
|
||||
type: response.data.success ? "success" : "error",
|
||||
title: response.data.success ? "Lexware-Verbindung erfolgreich" : "Lexware-Verbindung fehlgeschlagen",
|
||||
description: response.data.message,
|
||||
});
|
||||
} catch (err) {
|
||||
showToast({
|
||||
type: "error",
|
||||
title: "Lexware-Verbindung fehlgeschlagen",
|
||||
description: getErrorMessage(err, "Die Verbindung konnte nicht geprüft werden."),
|
||||
});
|
||||
} finally {
|
||||
setTestingLexware(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="rounded-lg border bg-white p-8 text-slate-500">Einstellungen werden geladen...</div>;
|
||||
}
|
||||
|
|
@ -171,7 +244,7 @@ export default function SettingsPage() {
|
|||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-950">Einstellungen</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">SMTP-Versand und öffentliche Statuslinks verwalten</p>
|
||||
<p className="mt-1 text-sm text-slate-500">SMTP-Versand, öffentliche Statuslinks und Lexware Office verwalten</p>
|
||||
</div>
|
||||
<div className="inline-flex w-fit rounded-lg border bg-white p-1">
|
||||
<TabButton active={activeTab === "smtp"} onClick={() => setActiveTab("smtp")}>
|
||||
|
|
@ -180,6 +253,9 @@ export default function SettingsPage() {
|
|||
<TabButton active={activeTab === "public-links"} onClick={() => setActiveTab("public-links")}>
|
||||
Öffentliche Links
|
||||
</TabButton>
|
||||
<TabButton active={activeTab === "lexware"} onClick={() => setActiveTab("lexware")}>
|
||||
Lexware Office
|
||||
</TabButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -287,7 +363,7 @@ export default function SettingsPage() {
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
) : activeTab === "public-links" ? (
|
||||
<section className="rounded-lg border bg-white p-6">
|
||||
<div className="mb-6 flex flex-col gap-3 border-b pb-5 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
|
|
@ -319,6 +395,86 @@ export default function SettingsPage() {
|
|||
</p>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="rounded-lg border bg-white p-6">
|
||||
<div className="mb-6 flex flex-col gap-3 border-b pb-5 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ReceiptText className="h-5 w-5 text-slate-500" />
|
||||
<h2 className="text-lg font-semibold text-slate-950">Lexware Office</h2>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Quelle: {sourceLabels[lexware.source]}
|
||||
{lexware.api_key_is_set ? " · API-Key ist gesetzt" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => void testLexwareConnection()} disabled={testingLexware}>
|
||||
<ShieldCheck />
|
||||
{testingLexware ? "Prüft..." : "Verbindung testen"}
|
||||
</Button>
|
||||
<Button type="button" onClick={() => void saveLexware()} disabled={savingLexware}>
|
||||
<Save />
|
||||
{savingLexware ? "Speichert..." : "Speichern"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 lg:grid-cols-2">
|
||||
<label className="flex items-center gap-3 rounded-lg border p-4 text-sm font-medium text-slate-800">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-slate-300"
|
||||
checked={lexware.enabled}
|
||||
onChange={(event) => setLexware((current) => ({ ...current, enabled: event.target.checked }))}
|
||||
/>
|
||||
Lexware-Integration aktivieren
|
||||
</label>
|
||||
|
||||
<Field label="Organisation">
|
||||
<Input value={lexware.organization_name} onChange={(event) => setLexware((current) => ({ ...current, organization_name: event.target.value }))} />
|
||||
</Field>
|
||||
|
||||
<Field label="API Base URL">
|
||||
<Input value={lexware.api_base_url} onChange={(event) => setLexware((current) => ({ ...current, api_base_url: event.target.value }))} />
|
||||
</Field>
|
||||
|
||||
<Field label={lexware.api_key_is_set ? "Neuen API-Key setzen" : "API-Key"}>
|
||||
<Input
|
||||
type="password"
|
||||
value={newLexwareApiKey}
|
||||
placeholder={lexware.api_key_is_set ? "Leer lassen, um aktuellen API-Key zu behalten" : ""}
|
||||
onChange={(event) => setNewLexwareApiKey(event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Standard MwSt. %">
|
||||
<Input
|
||||
inputMode="decimal"
|
||||
value={lexware.default_tax_rate}
|
||||
onChange={(event) => setLexware((current) => ({ ...current, default_tax_rate: event.target.value }))}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Zahlungsziel Tage">
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={365}
|
||||
value={lexware.default_payment_terms_days}
|
||||
onChange={(event) => setLexware((current) => ({ ...current, default_payment_terms_days: Number(event.target.value) || 0 }))}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-start gap-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-950">
|
||||
<ShieldCheck className="mt-0.5 h-5 w-5 shrink-0" />
|
||||
<p>
|
||||
Olympus bereitet Werkstatt- und KV-Daten vor. Lexware Office bleibt führend für Buchhaltung,
|
||||
Rechnungen, Steuer und DATEV/EÜR. Der API-Key wird nicht angezeigt und nicht an den Browser zurückgegeben.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { AlertTriangle, FileCheck2, PackageSearch, Plus, Send, Trash2, Undo2, XCircle } from "lucide-react";
|
||||
import { AlertTriangle, FileCheck2, PackageSearch, Plus, ReceiptText, Send, Trash2, Undo2, XCircle } from "lucide-react";
|
||||
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog";
|
||||
import { useToast } from "@/components/common/ToastProvider";
|
||||
|
|
@ -21,6 +21,7 @@ import type {
|
|||
InventoryCategory,
|
||||
InventoryItem,
|
||||
} from "@/types/inventory";
|
||||
import type { LexwareInvoicePreparation } from "@/types/lexware";
|
||||
import type {
|
||||
RepairEstimate,
|
||||
RepairEstimateItemType,
|
||||
|
|
@ -164,6 +165,7 @@ type Props = {
|
|||
canDelete: boolean;
|
||||
canSend: boolean;
|
||||
canRevoke: boolean;
|
||||
canLexwareExport: boolean;
|
||||
};
|
||||
|
||||
export default function RepairEstimatesSection({
|
||||
|
|
@ -175,6 +177,7 @@ export default function RepairEstimatesSection({
|
|||
canDelete,
|
||||
canSend,
|
||||
canRevoke,
|
||||
canLexwareExport,
|
||||
}: Props) {
|
||||
const { showToast } = useToast();
|
||||
const [estimates, setEstimates] = useState<RepairEstimate[]>([]);
|
||||
|
|
@ -188,6 +191,8 @@ export default function RepairEstimatesSection({
|
|||
const [deleteTarget, setDeleteTarget] = useState<RepairEstimate | null>(null);
|
||||
const [cancelTarget, setCancelTarget] = useState<RepairEstimate | null>(null);
|
||||
const [revokeTarget, setRevokeTarget] = useState<RepairEstimate | null>(null);
|
||||
const [lexwareResult, setLexwareResult] = useState<LexwareInvoicePreparation | null>(null);
|
||||
const [lexwareDialogOpen, setLexwareDialogOpen] = useState(false);
|
||||
const [inventoryDialogOpen, setInventoryDialogOpen] = useState(false);
|
||||
const [inventoryItems, setInventoryItems] = useState<InventoryItem[]>([]);
|
||||
const [inventoryCategories, setInventoryCategories] = useState<InventoryCategory[]>([]);
|
||||
|
|
@ -407,6 +412,24 @@ export default function RepairEstimatesSection({
|
|||
}
|
||||
}
|
||||
|
||||
async function prepareLexwareInvoice(estimate: RepairEstimate) {
|
||||
setPendingId(estimate.id);
|
||||
try {
|
||||
const response = await api.post<LexwareInvoicePreparation>(`/repairs/${repairId}/estimates/${estimate.id}/lexware/prepare-invoice`);
|
||||
setLexwareResult(response.data);
|
||||
setLexwareDialogOpen(true);
|
||||
showToast({
|
||||
type: response.data.ready_for_export ? "success" : "error",
|
||||
title: response.data.ready_for_export ? "Lexware-Rechnung vorbereitet" : "Lexware-Vorbereitung mit Hinweisen",
|
||||
description: response.data.ready_for_export ? "Die Daten wurden geprüft und für den späteren Export vorgemerkt." : "Bitte prüfe die Hinweise vor dem Export.",
|
||||
});
|
||||
} catch (err) {
|
||||
showToast({ type: "error", title: "Lexware-Rechnung konnte nicht vorbereitet werden", description: getErrorMessage(err) });
|
||||
} finally {
|
||||
setPendingId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEstimate() {
|
||||
if (!deleteTarget) return;
|
||||
setPendingId(deleteTarget.id);
|
||||
|
|
@ -496,6 +519,7 @@ export default function RepairEstimatesSection({
|
|||
{canSend && ["draft", "sent"].includes(estimate.status) && <Button type="button" size="sm" onClick={() => void sendEstimate(estimate)} disabled={pendingId === estimate.id || !customerEmail}><Send />Senden</Button>}
|
||||
{(canUpdate || canSend) && ["draft", "sent"].includes(estimate.status) && <Button type="button" variant="outline" size="sm" onClick={() => setCancelTarget(estimate)} disabled={pendingId === estimate.id}><XCircle />Stornieren</Button>}
|
||||
{canRevoke && estimate.status === "approved" && <Button type="button" variant="destructive" size="sm" onClick={() => setRevokeTarget(estimate)} disabled={pendingId === estimate.id}><Undo2 />Freigabe zurücknehmen</Button>}
|
||||
{canLexwareExport && estimate.status === "approved" && <Button type="button" variant="outline" size="sm" onClick={() => void prepareLexwareInvoice(estimate)} disabled={pendingId === estimate.id}><ReceiptText />Lexware-Rechnung vorbereiten</Button>}
|
||||
{canDelete && ["draft", "cancelled"].includes(estimate.status) && <Button type="button" variant="destructive" size="sm" onClick={() => setDeleteTarget(estimate)} disabled={pendingId === estimate.id}><Trash2 />Löschen</Button>}
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -629,6 +653,66 @@ export default function RepairEstimatesSection({
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={lexwareDialogOpen} onOpenChange={setLexwareDialogOpen}>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Lexware-Rechnung vorbereiten</DialogTitle>
|
||||
<DialogDescription>Die Rechnung wird noch nicht automatisch in Lexware erstellt.</DialogDescription>
|
||||
</DialogHeader>
|
||||
{lexwareResult && (
|
||||
<div className="grid max-h-[70vh] gap-4 overflow-y-auto pr-1 text-sm">
|
||||
<div className={`rounded-lg border p-4 ${lexwareResult.ready_for_export ? "bg-emerald-50 text-emerald-950" : "bg-amber-50 text-amber-950"}`}>
|
||||
<p className="font-semibold">{lexwareResult.ready_for_export ? "Bereit für späteren Export" : "Vorbereitung mit Hinweisen"}</p>
|
||||
<p className="mt-1">Sync-Record #{lexwareResult.sync_record_id}</p>
|
||||
</div>
|
||||
|
||||
{lexwareResult.warnings.length > 0 && (
|
||||
<div className="rounded-lg border bg-amber-50 p-4 text-amber-950">
|
||||
<p className="font-semibold">Warnungen</p>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5">
|
||||
{lexwareResult.warnings.map((warning) => <li key={warning}>{warning}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-3 rounded-lg border bg-slate-50 p-4 md:grid-cols-2">
|
||||
<Meta label="Kunde" value={lexwareResult.customer_mapping.name} />
|
||||
<Meta label="E-Mail" value={lexwareResult.customer_mapping.email || "-"} />
|
||||
<Meta label="Telefon" value={lexwareResult.customer_mapping.phone || "-"} />
|
||||
<Meta label="Kontaktabgleich" value={lexwareResult.customer_mapping.search_strategy} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border">
|
||||
<div className="grid grid-cols-[1fr_auto_auto] gap-3 border-b bg-slate-50 p-3 text-xs font-semibold uppercase text-slate-500">
|
||||
<span>Position</span>
|
||||
<span>Steuer</span>
|
||||
<span>Summe</span>
|
||||
</div>
|
||||
{lexwareResult.line_item_mapping.map((item, index) => (
|
||||
<div key={`${item.title}-${index}`} className="grid grid-cols-[1fr_auto_auto] gap-3 border-b p-3 last:border-b-0">
|
||||
<div>
|
||||
<p className="font-medium text-slate-950">{item.title}</p>
|
||||
<p className="text-xs text-slate-500">{item.quantity} {item.unit} × {item.unit_price} €</p>
|
||||
</div>
|
||||
<span>{item.tax_rate} %</span>
|
||||
<span className="font-medium text-slate-950">{item.total} €</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 rounded-lg border bg-slate-50 p-4 md:grid-cols-3">
|
||||
<Meta label="Netto" value={`${String(lexwareResult.payload_summary.subtotal ?? "-")} €`} />
|
||||
<Meta label="MwSt." value={`${String(lexwareResult.payload_summary.tax ?? "-")} €`} />
|
||||
<Meta label="Gesamt" value={`${String(lexwareResult.payload_summary.total ?? "-")} €`} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setLexwareDialogOpen(false)}>Schließen</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(cancelTarget)}
|
||||
title="Kostenvoranschlag stornieren?"
|
||||
|
|
|
|||
56
frontend/athena/types/lexware.ts
Normal file
56
frontend/athena/types/lexware.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import type { SettingsSource } from "@/types/system-settings";
|
||||
|
||||
export interface LexwareSettings {
|
||||
enabled: boolean;
|
||||
api_base_url: string;
|
||||
api_key_is_set: boolean;
|
||||
organization_name: string;
|
||||
default_tax_rate: string;
|
||||
default_payment_terms_days: number;
|
||||
source: SettingsSource;
|
||||
}
|
||||
|
||||
export interface LexwareSettingsPayload {
|
||||
enabled: boolean;
|
||||
api_base_url: string;
|
||||
api_key?: string;
|
||||
organization_name: string;
|
||||
default_tax_rate: string;
|
||||
default_payment_terms_days: number;
|
||||
}
|
||||
|
||||
export interface LexwareTestConnectionResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
source: SettingsSource;
|
||||
api_base_url: string;
|
||||
organization_name: string;
|
||||
}
|
||||
|
||||
export interface LexwareCustomerMapping {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
search_strategy: string;
|
||||
create_payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface LexwareLineItemMapping {
|
||||
title: string;
|
||||
description: string | null;
|
||||
quantity: string;
|
||||
unit: string;
|
||||
unit_price: string;
|
||||
tax_rate: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
export interface LexwareInvoicePreparation {
|
||||
ready_for_export: boolean;
|
||||
payload_summary: Record<string, unknown>;
|
||||
customer_mapping: LexwareCustomerMapping;
|
||||
line_item_mapping: LexwareLineItemMapping[];
|
||||
tax_mapping: Record<string, unknown>;
|
||||
warnings: string[];
|
||||
sync_record_id: number;
|
||||
}
|
||||
|
|
@ -216,6 +216,10 @@ export interface RepairEstimate {
|
|||
approved_at: string | null;
|
||||
declined_at: string | null;
|
||||
customer_response_message: string | null;
|
||||
lexware_invoice_id: string | null;
|
||||
lexware_invoice_number: string | null;
|
||||
lexware_invoice_status: string | null;
|
||||
lexware_synced_at: string | null;
|
||||
created_by_user_id: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue