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:
@@ -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) => ({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user