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

@@ -128,6 +128,9 @@ const NeuAuftrag = defineComponent({
if (d.fontSizeLetter) neu.fontSizeLetter = d.fontSizeLetter;
if (d.fontSizeEnvelope) neu.fontSizeEnvelope = d.fontSizeEnvelope;
if (typeof d.kuverts === 'boolean') neu.kuverts = d.kuverts;
if (typeof d.absenderAufKuvert === 'boolean') neu.absenderAufKuvert = d.absenderAufKuvert;
if (typeof d.envelopeSender === 'string') neu.envelopeSender = d.envelopeSender;
if (d.envelopeSenderScale) neu.envelopeSenderScale = d.envelopeSenderScale;
if (d.adressModus) neu.adressModus = d.adressModus;
if (Array.isArray(d.rows) && d.rows.length) {
neu.rows = d.rows.map((r) => ({

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',

View File

@@ -727,7 +727,7 @@ const Board = defineComponent({
if (!o) return;
try {
const full = (await api.get('/items/orders/' + o.id, { params: {
fields: 'text_template,font,multi_page,realistic,font_scale_letter,font_scale_envelope,needs_envelope,product.key,format.key',
fields: 'text_template,font,multi_page,realistic,font_scale_letter,font_scale_envelope,needs_envelope,envelope_sender,envelope_sender_scale,product.key,format.key',
} })).data?.data || {};
const ent = (await api.get('/items/order_entries', { params: {
'filter[order][_eq]': o.id, sort: 'letter_number', limit: -1, fields: 'free_text,placeholders,letter_number',
@@ -746,6 +746,9 @@ const Board = defineComponent({
fontSizeLetter: Math.round((Number(full.font_scale_letter) || 1) * 100),
fontSizeEnvelope: Math.round((Number(full.font_scale_envelope) || 1) * 100),
kuverts: !!full.needs_envelope,
absenderAufKuvert: !!(full.envelope_sender && String(full.envelope_sender).trim()),
envelopeSender: full.envelope_sender || '',
envelopeSenderScale: Math.round((Number(full.envelope_sender_scale) || 1) * 100),
adressModus: rows.some((r) => r.free_text.includes('\n')) ? '5zeilen' : 'individuell',
rows,
};