Kundenangebote (customer_offers) + Absender-Uebernahme-Fix

- Bootstrap: Collection customer_offers (Kunde+Produkt, Name-Zusatz, Aufschlaege/Stueck,
  mehrseitig erlauben, Notiz) + Order-Felder customer_offer/product_name_suffix.
- orders: calculatePrice addiert Angebots-Aufschlaege (serverseitig geprueft),
  Endpunkt /my-offers, POST / speichert Angebot + Name-Zusatz.
- produktion: 'In manuellen Druck uebernehmen' traegt jetzt auch Kuvert-Absender + Groesse mit.
- neuauftrag: uebernimmt Absender-Felder aus dem Auftrag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-03 12:10:28 +02:00
parent a44515de89
commit 1b2541e307
4 changed files with 71 additions and 3 deletions

View File

@@ -527,6 +527,25 @@ const handler = (router, { services, getSchema, logger }) => {
let net = lines.reduce((s, l) => s + l.amount, 0);
// 5) Gutschein
// Kundenangebot (customer_offers): frei benannte Aufschläge pro Stück, wenn der
// eingeloggte Kunde ein Angebot gewählt hat. Serverseitig geprüft (Zugehörigkeit).
let offer = null;
if (input.offer) {
const off = (await svc('customer_offers').readByQuery({
filter: { id: { _eq: input.offer }, active: { _eq: true } }, limit: 1,
fields: ['id', 'customer', 'product.key', 'name_suffix', 'surcharges', 'allow_multi_page'],
}))?.[0];
// Nur anwenden, wenn das Angebot dem anfragenden Kunden UND dem Produkt gehört.
if (off && input.customerId && String(off.customer) === String(input.customerId)
&& (!off.product?.key || off.product.key === input.product)) {
offer = off;
for (const s of (Array.isArray(off.surcharges) ? off.surcharges : [])) {
const betrag = Number(s?.amount) || 0;
if (betrag) { const gesamt = betrag * menge; add(s.label || 'Aufschlag', gesamt, true); net += gesamt; }
}
}
}
let voucher = null;
if (input.voucher_code) {
const v = (await svc('vouchers').readByQuery({
@@ -556,7 +575,7 @@ const handler = (router, { services, getSchema, logger }) => {
const paypalAllowed = net < limit;
return { product, lines, net_total: net, unit_net_total: unitNet, vat_amount: vat, gross_total: gross,
mwst_percent: mwst, paypal_allowed: paypalAllowed, paypal_limit_net: limit, voucher };
mwst_percent: mwst, paypal_allowed: paypalAllowed, paypal_limit_net: limit, voucher, offer };
}
function validate(body) {
@@ -578,6 +597,7 @@ const handler = (router, { services, getSchema, logger }) => {
const body = req.body || {};
const quote = await calculatePrice(schema, {
...body, quantity: body.quantity ?? (body.entries || []).length,
customerId: req.accountability?.user || null,
});
return res.json({
lines: quote.lines, net_total: quote.net_total, unit_net_total: quote.unit_net_total,
@@ -590,6 +610,26 @@ const handler = (router, { services, getSchema, logger }) => {
}
});
// ── Angebote des eingeloggten Kunden (Auswahl im Konfigurator) ────────────
router.get('/my-offers', async (req, res) => {
const uid = req.accountability?.user;
if (!uid) return res.json({ offers: [] });
try {
const schema = await getSchema();
const svc = (c) => new ItemsService(c, { schema, accountability: null });
const filter = { customer: { _eq: uid }, active: { _eq: true } };
if (req.query.product) filter.product = { key: { _eq: String(req.query.product) } };
const offers = await svc('customer_offers').readByQuery({
filter, limit: -1, sort: ['label'],
fields: ['id', 'label', 'name_suffix', 'note', 'allow_multi_page', 'surcharges', 'product.key', 'product.name'],
});
return res.json({ offers: offers || [] });
} catch (err) {
logger.error(`[skrift-orders] my-offers: ${err.stack || err.message}`);
return res.status(500).json({ error: 'Angebote konnten nicht geladen werden.' });
}
});
// ── Vorschau ─────────────────────────────────────────────────────────────
/**
* Erzeugt bis zu PREVIEW_MAX_DOCS Vorschau-SVGs über das Backend.
@@ -1682,7 +1722,7 @@ const handler = (router, { services, getSchema, logger }) => {
const { settings } = await loadPricingContext(schema);
const clean = makeSanitizer(settings.char_whitelist || '\\x20-\\x7E');
const quote = await calculatePrice(schema, { ...body, quantity: entries.length });
const quote = await calculatePrice(schema, { ...body, quantity: entries.length, customerId: req.accountability?.user || null });
// Zahlungsart erzwingen, wenn über dem PayPal-Limit
let paymentMethod = body.payment_method === 'paypal' ? 'paypal' : 'rechnung';
@@ -1774,6 +1814,9 @@ const handler = (router, { services, getSchema, logger }) => {
customer: customer.id,
customer_email: email,
product: quote.product.id,
// Gewähltes Kundenangebot (Aufschläge sind bereits im Preis) + Namens-Zusatz.
customer_offer: quote.offer?.id ?? null,
product_name_suffix: quote.offer?.name_suffix ?? null,
format: formatId,
person_type: body.person_type === 'unternehmen' ? 'unternehmen' : 'privat',
font: ['tilda', 'alva', 'ellie'].includes(body.font) ? body.font : 'tilda',