chore(merge): integrate origin/staging into local staging
All checks were successful
Staging Build / build (push) Successful in 2m57s

Resolved conflict in customer-accordion-list.tsx by keeping
the per-device flattening (DeviceCard) implementation.
This commit is contained in:
DanielS
2026-07-23 09:47:37 +02:00
16 changed files with 255 additions and 50 deletions

View File

@@ -402,6 +402,30 @@ export function WysiwygAdminClient({
</div>
</div>
{/* Verknüpfte monatliche Gebühr */}
<div className="space-y-1 mt-2">
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold block">Verknüpfte Gebühr:</span>
<select
value={prod.linked_fee_product_id || ''}
onChange={async (e) => {
const val = e.target.value || null
setProducts(products.map(p => p.id === prod.id ? { ...p, linked_fee_product_id: val } : p))
await updateProduct(prod.id, { linked_fee_product_id: val }, prod.modules || [])
}}
className="w-full bg-slate-950 border border-white/10 text-[10px] text-slate-300 focus:ring-0 cursor-pointer rounded px-2 py-1 outline-none"
>
<option value="">Keine Gebühr verknüpft</option>
{products
.filter(p => p.billing_interval === 'monthly' && p.id !== prod.id)
.map(p => (
<option key={p.id} value={p.id}>
{p.name} ({new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(p.base_price)} / mtl.)
</option>
))
}
</select>
</div>
{/* Modul hinzufügen Popover */}
<div className="mt-3 pt-2 border-t border-white/5">
<ModulePopover

View File

@@ -82,11 +82,9 @@ export async function GET(
const buffer = await renderToBuffer(
React.createElement(InvoicePDF, {
orderNumber: order.order_number || `AE-${id.slice(0, 8)}`,
dateStr: formattedDate,
order: order,
orderSnapshot: order.order_data,
customer: order.customer_data,
orderData: order.order_data,
totalPrice: Number(order.total_price),
})
);

View File

@@ -82,11 +82,9 @@ export async function GET(
const buffer = await renderToBuffer(
React.createElement(InvoicePDF, {
orderNumber: order.order_number || `AE-${id.slice(0, 8)}`,
dateStr: formattedDate,
order: order,
orderSnapshot: order.order_data,
customer: order.customer_data,
orderData: order.order_data,
totalPrice: Number(order.total_price),
})
);

View File

@@ -77,6 +77,7 @@ export async function POST(request: Request) {
const orderItemsList: any[] = [];
let total = 0;
const feeProductIds = new Set<string>();
for (const item of groupItems) {
const itemSnapshot = buildOrderSnapshot(
@@ -93,8 +94,53 @@ export async function POST(request: Request) {
}));
orderItemsList.push(...itemsWithDevice);
total += itemSnapshot.total;
// Echte linked fee products ermitteln
if (item.selections) {
for (const catId in item.selections) {
const sel = item.selections[catId];
if (sel.productId) {
const p = products.find((prod: any) => prod.id === sel.productId);
if (p && p.linked_fee_product_id) {
feeProductIds.add(p.linked_fee_product_id);
}
}
sel.productIds?.forEach((pId: string) => {
const p = products.find((prod: any) => prod.id === pId);
if (p && p.linked_fee_product_id) {
feeProductIds.add(p.linked_fee_product_id);
}
});
}
}
}
feeProductIds.forEach(feeProdId => {
const feeProduct = products.find((p: any) => p.id === feeProdId);
if (feeProduct) {
const isIntervalMatch =
(type === 'purchase' && feeProduct.billing_interval === 'one_time') ||
(type === 'subscription' && feeProduct.billing_interval === 'monthly');
if (isIntervalMatch) {
orderItemsList.push({
category_id: 'dienste-gebuehren',
category_name: 'Dienste & Gebühren',
product_id: feeProduct.id,
product_name: feeProduct.name,
base_price: Number(feeProduct.base_price),
billing_interval: feeProduct.billing_interval,
selected_modules: [],
item_total: Number(feeProduct.base_price),
device_name: 'Zusatzleistung'
});
total += Number(feeProduct.base_price);
}
}
});
const orderSnapshot = {
schema_version: 1,
billing_cycle: type === 'purchase' ? 'one_time' : 'monthly',

View File

@@ -126,14 +126,17 @@ export default async function MyOrdersPage() {
{/* Produkte kurz */}
{items.length > 0 && (
<div className="flex flex-wrap gap-2">
{items.map((item, i) => (
{items.map((item, idx) => (
<span
key={item.product_id}
className="text-xs bg-white/5 border border-white/10 rounded-full px-3 py-1 text-slate-300"
key={idx}
className="text-xs bg-white/5 border border-white/10 rounded-xl px-3 py-1.5 text-slate-300"
>
<span className="font-semibold text-white mr-1">{item.device_name || 'Kasse'}:</span>
{item.product_name}
{item.selected_modules.length > 0 && (
<span className="text-slate-500 ml-1">+{item.selected_modules.length} Module</span>
<span className="text-slate-500 ml-1">
(+{item.selected_modules.length} {item.selected_modules.length === 1 ? 'Modul' : 'Module'})
</span>
)}
</span>
))}

View File

@@ -118,7 +118,7 @@ export default async function OrderSuccessPage({
{Object.entries(groupedItems).map(([deviceName, devItems]) => (
<div key={deviceName} className="space-y-2 border-b border-white/5 pb-3 last:border-0 last:pb-0">
<div className="bg-white/5 px-2 py-1 rounded text-xs text-primary font-bold">
Kasse: {deviceName}
{deviceName === 'Zusatzleistung' ? 'Backoffice' : `Kasse: ${deviceName}`}
</div>
{devItems.map((item) => (
<div key={item.product_id} className="space-y-1 pl-2">