/** * Predefined customer e-mails for the lead page's "E-Mail senden" composer. * * The catalog follows the sales flow of a 3PL inquiry — before the quote (qualify, * schedule a call, collect missing data, re-check interest), after the quote (first * follow-up, second follow-up / validity, contract questions, long-silence check), * contract & onboarding (next steps after acceptance) and the polite break-up mail. * Texts are plain text (paragraphs separated by blank lines) and END AT THE GREETING: * the branded signature (name, title, phone, legal footer) is appended server-side * by staffMail.ts, exactly like the AI-draft send. Square-bracket markers such as * "[Datum/Uhrzeit 1]" are meant to be replaced by the user before sending. * * Placeholders ({{salutation}}, {{name}}, {{firstName}}, {{company}}, {{senderName}}, * {{senderFirstName}}, {{quoteDate}}, {{quoteValidUntil}}, {{contractDate}}) are * filled by renderLeadMailTemplates() from the lead record + offer_conditions. */ export type MailLang = 'de' | 'en' export type TemplateGroup = 'pre' | 'post' | 'contract' | 'general' export interface LeadMailTemplate { key: string group: TemplateGroup icon: string label: Record hint: Record subject: Record body: Record } export interface LeadMailStage { hasQuote: boolean quoteConfirmed: boolean hasContract: boolean contractSigned: boolean quoteDate: string quoteValidUntil: string contractDate: string } export const TEMPLATE_GROUPS: Record> = { pre: { de: 'Vor dem Angebot', en: 'Before the quote' }, post: { de: 'Nach dem Angebot', en: 'After the quote' }, contract: { de: 'Vertrag & Onboarding', en: 'Contract & onboarding' }, general: { de: 'Allgemein', en: 'General' } } const CLOSING = { de: 'Mit freundlichen Grüßen', en: 'Kind regards' } export const LEAD_MAIL_TEMPLATES: LeadMailTemplate[] = [ // ───────────── before the quote ───────────── { key: 'inquiry_thanks', group: 'pre', icon: 'ti-message-2-question', label: { de: 'Danke für Ihre Anfrage + Rückfragen', en: 'Thanks for your inquiry + questions' }, hint: { de: 'Erste Antwort: Eckdaten abfragen, um das Angebot kalkulieren zu können', en: 'First reply: ask for the key figures needed to calculate the quote' }, subject: { de: 'Ihre Fulfillment-Anfrage bei LogYou – kurze Rückfragen', en: 'Your fulfillment inquiry at LogYou – a few quick questions' }, body: { de: `{{salutation}} vielen Dank für Ihre Anfrage und Ihr Interesse an LogYou. Damit wir Ihnen ein passendes und verbindliches Angebot erstellen können, benötigen wir noch ein paar Eckdaten: • Versandvolumen: Wie viele Bestellungen versenden Sie aktuell pro Monat (ggf. Saisonspitzen)? • Sortiment: Wie viele Artikel (SKUs) haben Sie und wie groß/schwer sind diese typischerweise? • Shop-System / Marktplätze: Welche Systeme sollen angebunden werden (z. B. Shopify, Amazon, WooCommerce)? • Wareneingang: In welcher Form und Menge liefern Sie an (Paletten, Kartons, Container)? • Lagerbedarf: Wie viele Paletten- bzw. Regalplätze benötigen Sie voraussichtlich? • Zeitplan: Ab wann möchten Sie starten? Gerne besprechen wir die Punkte auch in einem kurzen Telefonat – sagen Sie mir einfach, wann es Ihnen passt. ${CLOSING.de}`, en: `{{salutation}} Thank you for your inquiry and your interest in LogYou. To prepare a suitable and binding quote, we need a few key figures: • Shipping volume: How many orders do you currently ship per month (incl. seasonal peaks)? • Assortment: How many products (SKUs) do you have and what size/weight are they typically? • Shop system / marketplaces: Which systems should be connected (e.g. Shopify, Amazon, WooCommerce)? • Inbound: In which form and quantity do you deliver (pallets, cartons, containers)? • Storage: How many pallet or shelf spaces do you expect to need? • Timeline: When would you like to start? We are happy to go through these points in a short call – just let me know when it suits you. ${CLOSING.en}` } }, { key: 'call_proposal', group: 'pre', icon: 'ti-phone-call', label: { de: 'Terminvorschlag Kennenlerngespräch', en: 'Call proposal' }, hint: { de: 'Kurzes Telefonat (15–20 min) mit zwei Terminvorschlägen', en: 'Short call (15–20 min) with two proposed slots' }, subject: { de: 'Kurzes Kennenlerngespräch zu Ihrer Anfrage – Terminvorschlag', en: 'Short intro call about your inquiry – proposed times' }, body: { de: `{{salutation}} vielen Dank für Ihre Anfrage. Erfahrungsgemäß klären sich die wichtigsten Fragen (Volumen, Sortiment, Anbindung, Start) am schnellsten in einem kurzen Telefonat von 15–20 Minuten. Danach kann ich Ihnen ein passgenaues Angebot erstellen. Passt Ihnen einer der folgenden Termine? • [Datum/Uhrzeit 1] • [Datum/Uhrzeit 2] Alternativ nennen Sie mir gerne einen Wunschtermin oder rufen mich direkt an. ${CLOSING.de}`, en: `{{salutation}} Thank you for your inquiry. In our experience the key points (volume, assortment, integration, start date) are clarified fastest in a short 15–20 minute call. Afterwards I can prepare a quote tailored to your needs. Would one of the following times work for you? • [date/time 1] • [date/time 2] Alternatively, just suggest a time that suits you or call me directly. ${CLOSING.en}` } }, { key: 'missing_info', group: 'pre', icon: 'ti-list-check', label: { de: 'Fehlende Angaben für das Angebot', en: 'Missing details for the quote' }, hint: { de: 'Nach dem Gespräch: konkrete offene Punkte nachfordern', en: 'After the call: request the specific open points' }, subject: { de: 'Ihr Angebot – noch wenige Angaben offen', en: 'Your quote – a few details still open' }, body: { de: `{{salutation}} vielen Dank für das gute Gespräch. Ich bereite Ihr Angebot gerade vor – für eine verbindliche Kalkulation fehlen mir noch folgende Angaben: • [Offener Punkt 1, z. B. durchschnittliches Paketgewicht] • [Offener Punkt 2, z. B. Anzahl Artikel pro Bestellung] • [Offener Punkt 3, z. B. gewünschte Versanddienstleister] Sobald ich die Informationen habe, erhalten Sie das Angebot innerhalb von [1–2 Werktagen]. ${CLOSING.de}`, en: `{{salutation}} Thank you for the good conversation. I am preparing your quote – for a binding calculation I still need the following details: • [open point 1, e.g. average parcel weight] • [open point 2, e.g. items per order] • [open point 3, e.g. preferred carriers] As soon as I have the information, you will receive the quote within [1–2 business days]. ${CLOSING.en}` } }, { key: 'still_relevant_pre', group: 'pre', icon: 'ti-help-circle', label: { de: 'Ist Ihre Anfrage noch aktuell?', en: 'Is your inquiry still relevant?' }, hint: { de: 'Lead antwortet nicht mehr, bevor ein Angebot erstellt wurde', en: 'Lead went quiet before a quote was made' }, subject: { de: 'Ihre Fulfillment-Anfrage – noch aktuell?', en: 'Your fulfillment inquiry – still relevant?' }, body: { de: `{{salutation}} vor einiger Zeit hatten Sie bei uns wegen Fulfillment und Lagerung angefragt. Da ich seither nichts mehr von Ihnen gehört habe, wollte ich kurz nachfragen: Ist das Thema bei Ihnen noch aktuell? Falls ja, melden Sie sich gerne – wir erstellen Ihnen kurzfristig ein Angebot oder klären offene Fragen in einem kurzen Telefonat. Falls Sie sich bereits anders entschieden haben, freue ich mich über eine kurze Rückmeldung, damit ich Ihre Anfrage entsprechend ablegen kann. ${CLOSING.de}`, en: `{{salutation}} Some time ago you contacted us regarding fulfillment and warehousing. As I have not heard back from you since, I wanted to check briefly: is this still relevant for you? If so, just let me know – we will prepare a quote at short notice or clarify open questions in a quick call. If you have already decided otherwise, I would appreciate a short note so I can close your inquiry accordingly. ${CLOSING.en}` } }, // ───────────── after the quote ───────────── { key: 'quote_received', group: 'post', icon: 'ti-file-euro', label: { de: 'Angebot erhalten? Fragen dazu?', en: 'Quote received? Any questions?' }, hint: { de: 'Erste Nachfassung 3–5 Tage nach dem Versand', en: 'First follow-up 3–5 days after sending' }, subject: { de: 'Unser Angebot vom {{quoteDate}} – haben Sie Fragen?', en: 'Our quote of {{quoteDate}} – any questions?' }, body: { de: `{{salutation}} am {{quoteDate}} haben wir Ihnen unser Angebot für Fulfillment und Lagerung zugesendet. Ich wollte kurz nachfragen, ob es gut bei Ihnen angekommen ist und ob Sie bereits einen ersten Blick darauf werfen konnten. Gibt es Punkte, die wir erläutern oder anpassen sollen – etwa zu Preisen, Leistungsumfang, Anbindung oder dem Startzeitpunkt? Gerne gehe ich das Angebot mit Ihnen in einem kurzen Telefonat durch. ${CLOSING.de}`, en: `{{salutation}} On {{quoteDate}} we sent you our quote for fulfillment and warehousing. I wanted to check briefly whether it arrived safely and whether you have had a chance to take a first look. Are there any points we should explain or adjust – e.g. pricing, scope of services, integration or the start date? I am happy to walk you through the quote in a short call. ${CLOSING.en}` } }, { key: 'quote_followup2', group: 'post', icon: 'ti-calendar-due', label: { de: 'Zweite Nachfassung / Gültigkeit', en: 'Second follow-up / validity' }, hint: { de: 'Angebot läuft bald ab – offene Hürden erfragen', en: 'Quote expires soon – ask what is holding things up' }, subject: { de: 'Ihr Angebot ist gültig bis {{quoteValidUntil}} – wie geht es weiter?', en: 'Your quote is valid until {{quoteValidUntil}} – next steps?' }, body: { de: `{{salutation}} ich möchte mich noch einmal zu unserem Angebot vom {{quoteDate}} melden. Es ist noch bis zum {{quoteValidUntil}} gültig – danach müssten wir die Konditionen ggf. neu kalkulieren. Gibt es aus Ihrer Sicht noch etwas, das einer Entscheidung im Weg steht – Preis, Leistungsumfang, Zeitplan oder interne Abstimmungen? Sagen Sie mir gerne, wo wir nachbessern können. Falls Sie mehr Zeit benötigen, verlängern wir die Gültigkeit selbstverständlich. ${CLOSING.de}`, en: `{{salutation}} I would like to follow up once more on our quote of {{quoteDate}}. It remains valid until {{quoteValidUntil}} – after that we may have to recalculate the conditions. Is there anything from your point of view that stands in the way of a decision – price, scope, timeline or internal alignment? Please let me know where we can improve. If you need more time, we will of course extend the validity. ${CLOSING.en}` } }, { key: 'contract_questions', group: 'post', icon: 'ti-file-certificate', label: { de: 'Fragen zum Vertrag?', en: 'Questions about the contract?' }, hint: { de: 'Vertrag gesendet, aber noch nicht unterschrieben', en: 'Contract sent but not yet signed' }, subject: { de: 'Ihr Fulfillment-Vertrag – offene Fragen?', en: 'Your fulfillment contract – any open questions?' }, body: { de: `{{salutation}} am {{contractDate}} haben wir Ihnen den Fulfillment-Rahmenvertrag mit dem Link zur digitalen Unterschrift zugesendet. Ich wollte kurz nachfragen, ob Sie Fragen zu einzelnen Punkten haben – etwa zu Laufzeit, Kündigungsfristen, Haftung oder der Auftragsverarbeitung (AVV). Gerne erläutere ich Ihnen die Regelungen in einem kurzen Gespräch. Sobald der Vertrag unterschrieben ist, planen wir gemeinsam den Start und den ersten Wareneingang. ${CLOSING.de}`, en: `{{salutation}} On {{contractDate}} we sent you the fulfillment framework agreement with the link for digital signature. I wanted to check whether you have questions about any of the points – e.g. term, notice periods, liability or the data processing agreement (DPA). I am happy to explain the provisions in a short call. As soon as the contract is signed, we will plan the start and the first inbound delivery together. ${CLOSING.en}` } }, { key: 'quote_still_active', group: 'post', icon: 'ti-refresh', label: { de: 'Angebot: Thema noch aktuell?', en: 'Quote: still relevant?' }, hint: { de: 'Längere Funkstille nach dem Angebot – Aktualisierung anbieten', en: 'Long silence after the quote – offer an update' }, subject: { de: 'Fulfillment mit LogYou – ist das Thema noch aktuell?', en: 'Fulfillment with LogYou – still on your agenda?' }, body: { de: `{{salutation}} vor einiger Zeit hatten wir Ihnen ein Angebot für Fulfillment und Lagerung zugesendet. Da ich seither nichts mehr von Ihnen gehört habe, wollte ich kurz nachfragen, ob das Thema bei Ihnen noch aktuell ist. Falls sich Ihre Anforderungen inzwischen geändert haben (Volumen, Sortiment, Marktplätze), aktualisieren wir das Angebot gerne. Und falls Sie sich anders entschieden haben, freue ich mich über eine kurze Rückmeldung – gerne auch mit dem Grund, damit wir uns verbessern können. ${CLOSING.de}`, en: `{{salutation}} Some time ago we sent you a quote for fulfillment and warehousing. As I have not heard back from you since, I wanted to check briefly whether this is still on your agenda. If your requirements have changed in the meantime (volume, assortment, marketplaces), we will gladly update the quote. And if you have decided otherwise, I would appreciate a short note – ideally with the reason, so we can improve. ${CLOSING.en}` } }, // ───────────── contract & onboarding ───────────── { key: 'next_steps', group: 'contract', icon: 'ti-rocket', label: { de: 'Zusage: nächste Schritte / Onboarding', en: 'Acceptance: next steps / onboarding' }, hint: { de: 'Nach Angebotsannahme oder Unterschrift: Was wir jetzt brauchen', en: 'After acceptance or signature: what we need now' }, subject: { de: 'Willkommen bei LogYou – die nächsten Schritte', en: 'Welcome to LogYou – the next steps' }, body: { de: `{{salutation}} herzlich willkommen bei LogYou – wir freuen uns sehr auf die Zusammenarbeit! Damit der Start reibungslos läuft, hier die nächsten Schritte: 1. Artikelstammdaten: Bitte senden Sie uns Ihre Produktliste (SKU, Bezeichnung, EAN, Maße, Gewicht) – gerne als CSV/Excel. 2. Shop-Anbindung: Wir richten die Schnittstelle zu Ihrem Shop-System bzw. Ihren Marktplätzen ein; dafür benötigen wir einen Zugang bzw. die API-Freigabe. 3. Wareneingang: Bitte melden Sie die erste Anlieferung mit Lieferschein und Mengen an (Paletten/Kartons, voraussichtliches Datum). 4. Verpackung & Beileger: Teilen Sie uns Ihre Wünsche zu Versandverpackung, Beilegern und Retourenabwicklung mit. 5. SEPA-Mandat: Falls noch nicht geschehen, erhalten Sie den Link zum SEPA-Lastschriftmandat separat. Für ein kurzes Onboarding-Gespräch schlage ich [Datum/Uhrzeit] vor – passt Ihnen das? ${CLOSING.de}`, en: `{{salutation}} A warm welcome to LogYou – we are very much looking forward to working with you! To ensure a smooth start, here are the next steps: 1. Product master data: Please send us your product list (SKU, name, EAN, dimensions, weight) – ideally as CSV/Excel. 2. Shop integration: We will set up the interface to your shop system or marketplaces; for this we need access or the API approval. 3. Inbound: Please announce the first delivery with delivery note and quantities (pallets/cartons, expected date). 4. Packaging & inserts: Let us know your requirements for shipping packaging, inserts and returns handling. 5. SEPA mandate: If not done yet, you will receive the link to the SEPA direct debit mandate separately. For a short onboarding call I suggest [date/time] – does that work for you? ${CLOSING.en}` } }, { key: 'closing', group: 'contract', icon: 'ti-door-exit', label: { de: 'Anfrage vorerst schließen', en: 'Close the inquiry for now' }, hint: { de: 'Letzte Mail nach mehreren Nachfassungen – Tür bleibt offen', en: 'Last mail after several follow-ups – door stays open' }, subject: { de: 'Ihre Anfrage bei LogYou – wir schließen den Vorgang vorerst', en: 'Your inquiry at LogYou – closing the file for now' }, body: { de: `{{salutation}} da ich trotz mehrerer Nachfragen keine Rückmeldung von Ihnen erhalten habe, gehe ich davon aus, dass das Thema Fulfillment für Sie derzeit nicht aktuell ist. Ich schließe Ihre Anfrage daher vorerst ab. Selbstverständlich können Sie jederzeit wieder auf uns zukommen – wir erstellen Ihnen dann gerne ein aktualisiertes Angebot. Ich wünsche Ihnen weiterhin viel Erfolg mit Ihrem Geschäft. ${CLOSING.de}`, en: `{{salutation}} As I have not received a reply despite several follow-ups, I assume that fulfillment is not a current topic for you at the moment. I will therefore close your inquiry for now. Of course you are welcome to get back to us at any time – we will gladly prepare an updated quote. I wish you continued success with your business. ${CLOSING.en}` } }, // ───────────── general ───────────── { key: 'free', group: 'general', icon: 'ti-pencil', label: { de: 'Freie Nachricht', en: 'Free message' }, hint: { de: 'Nur Anrede und Grußformel', en: 'Salutation and closing only' }, subject: { de: 'Ihre Anfrage bei LogYou', en: 'Your inquiry at LogYou' }, body: { de: `{{salutation}} ${CLOSING.de}`, en: `{{salutation}} ${CLOSING.en}` } } ] const formatDate = (iso: string, lang: MailLang) => { if (!iso) return '' const d = new Date(iso) if (Number.isNaN(d.getTime())) return '' return d.toLocaleDateString(lang === 'en' ? 'en-GB' : 'de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) } /** Quote / contract milestones from the AD_User.offer_conditions JSON (same keys the leads list reads). */ export const stageFromConditions = (cond: any, lang: MailLang): LeadMailStage => { const quote = cond?.quote && typeof cond.quote === 'object' ? cond.quote : {} const contract = cond?.contract && typeof cond.contract === 'object' ? cond.contract : {} const quoteSentAt = String(quote?.lastSend?.at || quote?.savedAt || '') let validUntil = '' if (quoteSentAt) { const days = Number(quote?.validDays) const d = new Date(quoteSentAt) if (!Number.isNaN(d.getTime()) && days > 0) { d.setDate(d.getDate() + days); validUntil = d.toISOString() } else if (quote?.lastSend?.confirmationExpiresAt) validUntil = String(quote.lastSend.confirmationExpiresAt) } return { hasQuote: !!quoteSentAt, quoteConfirmed: !!quote?.confirmation?.confirmedAt, hasContract: !!(contract?.lastSend?.at || contract?.savedAt), contractSigned: !!contract?.signature, quoteDate: formatDate(quoteSentAt, lang), quoteValidUntil: formatDate(validUntil, lang), contractDate: formatDate(String(contract?.lastSend?.at || contract?.savedAt || ''), lang) } } /** Which templates fit the lead's current stage (shown first / marked in the picker). */ export const recommendedKeys = (stage: LeadMailStage): string[] => { if (stage.contractSigned || stage.quoteConfirmed) return ['next_steps'] if (stage.hasContract) return ['contract_questions', 'next_steps'] if (stage.hasQuote) return ['quote_received', 'quote_followup2', 'quote_still_active'] return ['inquiry_thanks', 'call_proposal', 'missing_info', 'still_relevant_pre'] } export interface LeadMailPerson { name: string; greeting: string; company: string } /** "Sehr geehrter Herr Müller," / "Dear Ms Smith," — falls back to a neutral form when no greeting is stored. */ export const salutationFor = (person: LeadMailPerson, lang: MailLang) => { const name = String(person.name || '').trim() const lastName = name.split(/\s+/).filter(Boolean).pop() || '' const g = String(person.greeting || '').toLowerCase() const male = /^(herr|mr\b|mr\.|sir|señor|sr\.)/.test(g) const female = /^(frau|ms\b|ms\.|mrs|miss|madam|señora|sra\.)/.test(g) if (lang === 'en') { if (lastName && male) return `Dear Mr ${lastName},` if (lastName && female) return `Dear Ms ${lastName},` return name ? `Hello ${name},` : 'Hello,' } if (lastName && male) return `Sehr geehrter Herr ${lastName},` if (lastName && female) return `Sehr geehrte Frau ${lastName},` return name ? `Guten Tag ${name},` : 'Guten Tag,' } export interface RenderedTemplate { key: string group: TemplateGroup groupLabel: string icon: string label: string hint: string subject: string body: string recommended: boolean } export const renderLeadMailTemplates = (person: LeadMailPerson, sender: { name: string }, stage: LeadMailStage, lang: MailLang): RenderedTemplate[] => { const senderName = String(sender?.name || '').trim() const vars: Record = { salutation: salutationFor(person, lang), name: String(person.name || '').trim(), firstName: String(person.name || '').trim().split(/\s+/)[0] || '', company: String(person.company || '').trim(), senderName, senderFirstName: senderName.split(/\s+/)[0] || '', quoteDate: stage.quoteDate || (lang === 'en' ? '[date]' : '[Datum]'), quoteValidUntil: stage.quoteValidUntil || (lang === 'en' ? '[date]' : '[Datum]'), contractDate: stage.contractDate || (lang === 'en' ? '[date]' : '[Datum]') } const fill = (s: string) => s.replace(/\{\{(\w+)\}\}/g, (_, k) => (k in vars ? vars[k] : `{{${k}}}`)) const rec = new Set(recommendedKeys(stage)) return LEAD_MAIL_TEMPLATES.map((tpl) => ({ key: tpl.key, group: tpl.group, groupLabel: TEMPLATE_GROUPS[tpl.group][lang], icon: tpl.icon, label: tpl.label[lang], hint: tpl.hint[lang], subject: fill(tpl.subject[lang]), body: fill(tpl.body[lang]), recommended: rec.has(tpl.key) })) }