/** * POST /api/offers/questionnaire/save — persist the initial-call questionnaire * on the lead/partner (offer_conditions.questionnaire) and, when `attach` is * true, render the FILLED PDF, attach it to the record (Strapi, same flow as the * quote/contract/SEPA PDFs) and log a C_ContactActivity with the answers. * * Body: { source: 'lead'|'partner', recordId, recordUu?, language: 'de'|'en', * meta: { company, contactName, interviewer, date, channel }, * answers: {...}, attach: boolean } * Returns { status, message, filename?, attachedUrl?, warning? }. */ import refreshTokenHelper from '../../../utils/refreshTokenHelper' import errorHandlingHelper from '../../../utils/errorHandlingHelper' import forceLogoutHelper from '../../../utils/forceLogoutHelper' import { buildQuestionnairePdf } from '../../../utils/offers/questionnairePdf' import { attachToStrapi } from '../../../utils/inbox/strapiAttach' import { logContactActivity, strapiPublicFileUrl } from '../../../utils/offers/contactActivity' import { saveOfferConditions, readOfferConditions } from '../../../utils/offers/offerConditions' import { QUESTIONNAIRE, QUESTIONNAIRE_VERSION, qLabel, formatQuestionnaireAnswer, mergeQuestionnaireAnswers, questionnaireFilename, questionnaireProgress } from '../../../../app/utils/leadQuestionnaire' const handleFunc = async (event: any, authToken: string | null = null) => { const token = authToken ?? await getTokenHelper(event) if (!token) return { status: 401, message: 'Not authenticated' } const body: any = await readBody(event) const language = body?.language === 'en' ? 'en' : 'de' const en = language === 'en' const source = body?.source === 'lead' ? 'lead' : 'partner' const recordId = Number(body?.recordId) || 0 if (!(recordId > 0)) return { status: 400, message: 'recordId required' } const meta = { company: String(body?.meta?.company || '').trim(), contactName: String(body?.meta?.contactName || '').trim(), interviewer: String(body?.meta?.interviewer || '').trim(), date: String(body?.meta?.date || '').trim() || new Date().toISOString().slice(0, 10), channel: ['phone', 'meeting', 'video'].includes(body?.meta?.channel) ? body.meta.channel : '' } const answers = mergeQuestionnaireAnswers(body?.answers) const attach = body?.attach === true const now = new Date().toISOString() const warnings: string[] = [] // 1) answers on the record (draft AND final) — read-merge-write keeps quote/contract/sepa intact const section: any = { version: QUESTIONNAIRE_VERSION, language, meta, answers, updatedAt: now, updatedBy: Number(getCookie(event, 'logship_user_id')) || null } let prior: any = null try { prior = (await readOfferConditions(event, token, source, recordId))?.questionnaire || null } catch { /* fail-soft */ } if (prior?.completedAt) section.completedAt = prior.completedAt if (prior?.attachedUrl) section.attachedUrl = prior.attachedUrl if (prior?.filename) section.filename = prior.filename let filename = '' let attachedUrl = '' if (attach) { // 2) filled PDF → Strapi attachment on the record const buffer = await buildQuestionnairePdf({ language, mode: 'filled', meta, answers }) filename = questionnaireFilename(language, meta.company, 'filled') try { const up = await attachToStrapi(event, token, { tableName: source === 'lead' ? 'Lead_User' : 'C_BPartner', recordId, recordUu: body?.recordUu || undefined, buffer, filename, mimeType: 'application/pdf' }) attachedUrl = strapiPublicFileUrl(up.fileUrl) } catch (err: any) { console.error('[Questionnaire] attach failed:', err?.message || err) warnings.push(en ? 'PDF could not be attached' : 'PDF konnte nicht angehängt werden') } section.completedAt = now section.filename = filename if (attachedUrl) section.attachedUrl = attachedUrl // 3) activity with a readable summary of every answered question try { const lines: string[] = [] for (const cat of QUESTIONNAIRE) { const rows = cat.questions.map((q) => ({ q, a: formatQuestionnaireAnswer(q, answers, language) })).filter((r) => r.a) if (!rows.length) continue lines.push(`## ${qLabel(cat, language)}`) for (const r of rows) lines.push(`${qLabel(r.q, language)}: ${r.a}`) } const p = questionnaireProgress(answers) const channel = meta.channel ? ({ de: { phone: 'Telefon', meeting: 'persönlich', video: 'Video-Call' }, en: { phone: 'phone', meeting: 'in person', video: 'video call' } } as any)[language][meta.channel] : '' await logContactActivity(event, token, { source, recordId, type: meta.channel === 'meeting' ? 'ME' : 'PC', description: en ? `Initial-call questionnaire completed (${p.answered}/${p.total})` : `Erstgespräch-Fragebogen ausgefüllt (${p.answered}/${p.total})`, comments: [ en ? `Questionnaire${channel ? ` (${channel})` : ''} recorded by ${meta.interviewer || '—'} on ${meta.date}.` : `Fragebogen${channel ? ` (${channel})` : ''} aufgenommen von ${meta.interviewer || '—'} am ${meta.date}.`, '', ...lines ].join('\n'), documents: attachedUrl ? [{ title: en ? 'Questionnaire (PDF)' : 'Fragebogen Erstgespräch (PDF)', url: attachedUrl }] : [] }) } catch (err: any) { console.error('[Questionnaire] activity failed:', err?.message || err) } } try { await saveOfferConditions(event, token, source, recordId, 'questionnaire', section) } catch (err: any) { console.error('[Questionnaire] conditions save failed:', err?.message || err) warnings.push(en ? 'Answers could not be stored on the record (offer_conditions)' : 'Antworten konnten nicht am Datensatz gespeichert werden (offer_conditions)') } return { status: 200, message: attach ? (en ? 'Questionnaire saved and attached' : 'Fragebogen gespeichert und angehängt') : (en ? 'Draft saved' : 'Entwurf gespeichert'), filename: filename || undefined, attachedUrl: attachedUrl || undefined, warning: warnings.join(' · ') || undefined } } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { try { const authToken = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })