feat: Add invoice items element with dynamic pricing from pricing table
- Add new 'invoice-items-muh' element to invoice template - Add 'Positionen' category back to palette groups - Create helper functions to format price and generate invoice content - Add /current endpoint to SystemPricingController for authenticated users - Load pricing when creating starter layout and display: - Monatliche Systemgebühr MUH with net price - Umsatzsteuer (19%) - Gesamtsumme (brutto) - Update starter layout positioning for new element
This commit is contained in:
@@ -9,7 +9,6 @@ import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/pricing")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public class SystemPricingController {
|
||||
|
||||
private final SystemPricingService systemPricingService;
|
||||
@@ -19,13 +18,23 @@ public class SystemPricingController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public PricingResponse getPricing() {
|
||||
Optional<SystemPricing> pricing = systemPricingService.getCurrentPricing();
|
||||
return pricing.map(p -> new PricingResponse(p.monthlyPrice(), p.updatedAt().toString()))
|
||||
.orElseGet(() -> new PricingResponse(null, null));
|
||||
}
|
||||
|
||||
@GetMapping("/current")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public PricingResponse getCurrentPricing() {
|
||||
Optional<SystemPricing> pricing = systemPricingService.getCurrentPricing();
|
||||
return pricing.map(p -> new PricingResponse(p.monthlyPrice(), p.updatedAt().toString()))
|
||||
.orElseGet(() -> new PricingResponse(null, null));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public PricingResponse savePricing(@RequestBody PricingRequest request) {
|
||||
SystemPricing saved = systemPricingService.savePricing(request.monthlyPrice());
|
||||
return new PricingResponse(saved.monthlyPrice(), saved.updatedAt().toString());
|
||||
|
||||
@@ -361,6 +361,18 @@ const INVOICE_PALETTE_ITEMS: PaletteItem[] = [
|
||||
defaultContent: (user) =>
|
||||
`Tel.: ${user?.phoneNumber ?? ""}\nE-Mail: ${user?.email ?? ""}`,
|
||||
},
|
||||
{
|
||||
id: "invoice-items-muh",
|
||||
category: "invoice-items",
|
||||
label: "Rechnungspositionen MUH",
|
||||
description: "Monatliche Systemgebühr mit Preis aus Preistabelle",
|
||||
width: 500,
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
textAlign: "left",
|
||||
defaultContent: () =>
|
||||
"Monatliche Systemgebühr MUH 0,00 €\nzzgl. Umsatzsteuer (19%) 0,00 €\nGesamtsumme 0,00 €",
|
||||
},
|
||||
{
|
||||
id: "payment-terms",
|
||||
category: "invoice-footer",
|
||||
@@ -428,7 +440,7 @@ const INVOICE_PALETTE_GROUPS: Array<{ category: PaletteCategory; title: string }
|
||||
{ category: "invoice-header", title: "Rechnungskopf" },
|
||||
{ category: "customer-data", title: "Kundendaten" },
|
||||
{ category: "issuer-data", title: "Aussteller" },
|
||||
|
||||
{ category: "invoice-items", title: "Positionen" },
|
||||
{ category: "invoice-footer", title: "Fußbereich" },
|
||||
{ category: "free-elements", title: "Freie Elemente" },
|
||||
];
|
||||
@@ -940,7 +952,24 @@ function createElementFromPalette(
|
||||
});
|
||||
}
|
||||
|
||||
function createInvoiceStarterLayout(user: UserOption | null, paletteItems: PaletteItem[]) {
|
||||
function formatPrice(price: number | null): string {
|
||||
if (price === null || price === undefined) return "0,00";
|
||||
return price.toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function createMuhInvoiceContent(monthlyPrice: number | null): string {
|
||||
const netto = monthlyPrice || 0;
|
||||
const ust = netto * 0.19;
|
||||
const brutto = netto + ust;
|
||||
|
||||
const nettoStr = formatPrice(netto).padStart(10, " ");
|
||||
const ustStr = formatPrice(ust).padStart(10, " ");
|
||||
const bruttoStr = formatPrice(brutto).padStart(10, " ");
|
||||
|
||||
return `Monatliche Systemgebühr MUH ${nettoStr} €\nzzgl. Umsatzsteuer (19%) ${ustStr} €\nGesamtsumme ${bruttoStr} €`;
|
||||
}
|
||||
|
||||
function createInvoiceStarterLayout(user: UserOption | null, paletteItems: PaletteItem[], monthlyPrice: number | null = null) {
|
||||
const horizontalLine = (x: number, y: number, width: number) =>
|
||||
normalizeElement({
|
||||
...createElementFromPalette(requirePaletteItem(paletteItems, "line"), user, { x, y }),
|
||||
@@ -966,9 +995,13 @@ function createInvoiceStarterLayout(user: UserOption | null, paletteItems: Palet
|
||||
createElementFromPalette(requirePaletteItem(paletteItems, "customer-city"), user, { x: 140, y: 270 }),
|
||||
createElementFromPalette(requirePaletteItem(paletteItems, "customer-number"), user, { x: 56, y: 290 }),
|
||||
horizontalLine(56, 330, 646),
|
||||
normalizeElement({
|
||||
...createElementFromPalette(requirePaletteItem(paletteItems, "invoice-items-muh"), user, { x: 56, y: 350 }),
|
||||
content: createMuhInvoiceContent(monthlyPrice),
|
||||
}),
|
||||
horizontalLine(56, 560, 646),
|
||||
createElementFromPalette(requirePaletteItem(paletteItems, "payment-terms"), user, { x: 56, y: 360 }),
|
||||
createElementFromPalette(requirePaletteItem(paletteItems, "bank-details"), user, { x: 56, y: 400 }),
|
||||
createElementFromPalette(requirePaletteItem(paletteItems, "payment-terms"), user, { x: 56, y: 420 }),
|
||||
createElementFromPalette(requirePaletteItem(paletteItems, "bank-details"), user, { x: 56, y: 460 }),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1188,16 +1221,22 @@ export default function InvoiceTemplatePage() {
|
||||
setTemplateError(null);
|
||||
setIsTemplateLoading(true);
|
||||
|
||||
void apiGet<InvoiceTemplateResponse>("/session/invoice-template")
|
||||
.then((response) => {
|
||||
// Load pricing first, then load template
|
||||
void Promise.all([
|
||||
apiGet<InvoiceTemplateResponse>("/session/invoice-template"),
|
||||
apiGet<{ monthlyPrice: number | null }>("/admin/pricing/current").catch(() => ({ monthlyPrice: null })),
|
||||
])
|
||||
.then(([response, pricing]) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monthlyPrice = pricing.monthlyPrice;
|
||||
|
||||
setIsTemplateApiAvailable(true);
|
||||
setTemplateUpdatedAt(response.updatedAt);
|
||||
if (!response.stored) {
|
||||
const starterLayout = createInvoiceStarterLayout(user, INVOICE_PALETTE_ITEMS);
|
||||
const starterLayout = createInvoiceStarterLayout(user, INVOICE_PALETTE_ITEMS, monthlyPrice);
|
||||
setElements(starterLayout);
|
||||
setSelectedElementId(starterLayout[0]?.id ?? null);
|
||||
setResizingElementId(null);
|
||||
@@ -1472,9 +1511,16 @@ export default function InvoiceTemplatePage() {
|
||||
moveCanvasElement(payload.elementId, event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
function handleResetToStarter() {
|
||||
async function handleResetToStarter() {
|
||||
mouseDragCleanupRef.current?.();
|
||||
const starterLayout = createInvoiceStarterLayout(user, INVOICE_PALETTE_ITEMS);
|
||||
let monthlyPrice: number | null = null;
|
||||
try {
|
||||
const pricing = await apiGet<{ monthlyPrice: number | null }>("/admin/pricing/current");
|
||||
monthlyPrice = pricing.monthlyPrice;
|
||||
} catch {
|
||||
// Ignore error and use null
|
||||
}
|
||||
const starterLayout = createInvoiceStarterLayout(user, INVOICE_PALETTE_ITEMS, monthlyPrice);
|
||||
setElements(starterLayout);
|
||||
setSelectedElementId(starterLayout[0]?.id ?? null);
|
||||
setResizingElementId(null);
|
||||
|
||||
Reference in New Issue
Block a user