:root{
--bg:#f6f7f9;
--card:#ffffff;
--ink:#1b1f24;
--muted:#5b6776;
--line:#d7dde5;
--soft:#eef1f5;
--accent:#1f4fff;
--shadow:0 10px 28px rgba(16,24,40,.10);
--radius:14px;
--radius-sm:10px;
--mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans:system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.45}
.wrap{max-width:1100px;margin:24px auto 40px;padding:0 16px}
/* Top buttons row */
.topbar{display:flex;gap:12px;align-items:center;justify-content:flex-end;flex-wrap:wrap;margin-bottom:12px}
button,.btn{
border:1px solid var(--line);
background:var(--card);
color:var(--ink);
border-radius:10px;
padding:10px 12px;
font-size:13px;
cursor:pointer;
transition:transform .04s ease, background .2s ease, border-color .2s ease;
text-decoration:none;
display:inline-flex;
align-items:center;
gap:8px;
user-select:none;
}
button:hover,.btn:hover{border-color:#b9c3d1;background:#fbfcfe}
button:active,.btn:active{transform:translateY(1px)}
.btn-primary{border-color:rgba(31,79,255,.35);background:rgba(31,79,255,.08)}
.btn-primary:hover{background:rgba(31,79,255,.12);border-color:rgba(31,79,255,.45)}
/* New centered title */
.page-title{
text-align:center;
background:var(--card);
border:1px solid var(--line);
border-radius:var(--radius);
box-shadow:var(--shadow);
padding:18px 16px;
margin-bottom:14px;
}
.title-main{
font-size:28px;
font-weight:650;
letter-spacing:1px;
margin:0;
line-height:1.15;
}
.title-sub{
font-size:14px;
color:var(--muted);
margin:6px 0 0 0;
line-height:1.35;
}
.grid{display:grid;grid-template-columns:1.10fr .90fr;gap:14px;align-items:start}
@media (max-width:980px){.grid{grid-template-columns:1fr}}
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
.card .hd{padding:16px 16px 12px;border-bottom:1px solid var(--line)}
.card .hd h2{margin:0;font-size:15px;letter-spacing:.2px}
.card .hd p{margin:6px 0 0;color:var(--muted);font-size:13px}
.card .bd{padding:16px}
.trust{display:grid;gap:10px}
.trust .note{background:var(--soft);border:1px solid #e2e7ef;border-radius:var(--radius-sm);padding:12px;font-size:13px;color:var(--muted)}
.trust .note strong{color:var(--ink);font-weight:650}
form{display:grid;gap:14px}
.section{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
.section .shd{background:#fbfcfe;border-bottom:1px solid var(--line);padding:12px 14px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
.section .shd h3{margin:0;font-size:14px;letter-spacing:.2px}
.section .shd .hint{font-size:12px;color:var(--muted);white-space:nowrap}
.section .sbd{padding:14px}
.row{display:grid;grid-template-columns:1fr;gap:12px}
.field{display:grid;gap:6px}
label{font-size:12px;color:var(--muted)}
input,select,textarea{
width:100%;
box-sizing:border-box;
border:1px solid var(--line);
border-radius:10px;
padding:12px 12px;
font-size:14px;
outline:none;
background:#fff;
color:var(--ink);
}
textarea{min-height:120px;resize:vertical}
input:focus,select:focus,textarea:focus{
border-color:rgba(31,79,255,.55);
box-shadow:0 0 0 4px rgba(31,79,255,.12);
}
.inline{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.pill{
border:1px solid var(--line);
border-radius:999px;
padding:8px 10px;
font-size:13px;
background:#fff;
display:inline-flex;
gap:8px;
align-items:center;
cursor:pointer;
user-select:none;
}
.pill input{width:auto;margin:0}
.help{font-size:12px;color:var(--muted);margin-top:2px}
.req{
font-family:var(--mono);
font-size:11px;
color:var(--muted);
background:var(--soft);
border:1px solid #e2e7ef;
border-radius:999px;
padding:4px 8px;
}
.divider{height:1px;background:var(--line);margin:10px 0}
.summary{position:sticky;top:14px;display:grid;gap:12px}
@media (max-width:980px){.summary{position:static}}
.kv{display:grid;gap:8px;font-size:13px}
.kv .item{display:flex;justify-content:space-between;gap:10px;color:var(--muted)}
.kv .item b{color:var(--ink);font-weight:650;text-align:right}
.emailPreview{
border:1px dashed #c7d0dd;
border-radius:12px;
padding:12px;
background:#fbfcfe;
font-family:var(--mono);
font-size:12px;
white-space:pre-wrap;
word-break:break-word;
color:#2b3440;
max-height:380px;
overflow:auto;
}
.foot{color:var(--muted);font-size:12px;margin-top:10px;line-height:1.55}
.error{
display:none;
border:1px solid #f3c7c7;
background:#fff5f5;
color:#8a1f1f;
padding:10px 12px;
border-radius:12px;
font-size:13px;
}
/* Hidden helper field to hold the final email body */
#finalText{
position:absolute;
left:-9999px;
top:auto;
width:1px;
height:1px;
overflow:hidden;
}
@page{size:A4;margin:12mm}
@media print{
body{background:#fff}
.wrap{max-width:none;margin:0;padding:0}
.topbar,.summary .actionsBlock{display:none !important}
.grid{grid-template-columns:1fr;gap:0}
.card{box-shadow:none;border:0}
.card .hd{padding:0 0 10px}
.card .bd{padding:0}
.page-title{box-shadow:none;border:0;padding:0;margin:0 0 10px 0}
.section{break-inside:avoid;page-break-inside:avoid}
.section .shd{background:#fff}
input,select,textarea{border:1px solid #9aa6b2}
.trust{display:none}
.emailPreview{border:1px solid #9aa6b2}
}
ORDER ON CALL
Planning request – only valid after written confirmation by Ruan BV (email)
This form creates a clear planning reference and improves reliability for both parties.
No delivery is guaranteed until Ruan BV confirms in writing. After confirmation and arrival,
the agreed quantity is placed on a separate reserved stock list/location under the Buyer’s company name
and delivered on call-off.
Language required
1. Supplier
2. Buyer
3. Product
Choose a product SKU 06FR001 – Chicken breast steaks fry-steamed (10 kg boxes) SKU 02FRT01 – Marinated chicken breast steaks fry-steamed (10 kg boxes) SKU 06FR001 – Fry-steamed mini steaks (Chickenchips) (10 kg boxes)
SKU 02KPT01 – Breaded chicken goujon tenders “Succes” ± 50 g (10 x 1 kg) Manual (edit everything)
4. Planning details
5. Confirmation
Summary
Preview of the text that will be copied / emailed / printed.
(function(){ const form = document.getElementById('reqForm'); const preview = document.getElementById('preview'); const finalText = document.getElementById('finalText'); const kv = document.getElementById('kv'); const formError = document.getElementById('formError'); const btnPrint = document.getElementById('btnPrint'); const btnCopy = document.getElementById('btnCopy'); const btnOpenEmail = document.getElementById('btnOpenEmail'); const btnReset = document.getElementById('btnReset'); const btnOpenGmail = document.getElementById('btnOpenGmail'); const btnOpenOutlook = document.getElementById('btnOpenOutlook'); const btnOpenAppleMail = document.getElementById('btnOpenAppleMail'); const monthlyWrap = document.getElementById('monthlyWrap'); const monthlyCalloff = document.getElementById('monthlyCalloff'); const reservationDateEl = document.getElementById('reservationDate'); const productSelect = document.getElementById('productSelect'); const products = { p1: { code: "06FR001", packaging: { en:"10 kg boxes", nl:"10 kg dozen", fr:"Cartons de 10 kg", de:"10-kg-Kartons" }, origin: { en:"Europe", nl:"Europa", fr:"Europe", de:"Europa" }, lead: { en:"1 to 3 months", nl:"1 tot 3 maanden", fr:"1 à 3 mois", de:"1 bis 3 Monate" }, name: { en:"Chicken breast steaks fry-steamed", nl:"Kipfilet steaks frysteamed", fr:"Steaks de filet de poulet fri vapeur", de:"Hähnchenbruststeaks frittiert-gedämpft" }, text: { en:"Chicken breast steaks fry-steamed. Approx. 100 g per piece.", nl:"Kipfilet steaks frysteamed. Ongeveer 100 g per stuk.", fr:"Steaks de filet de poulet fri vapeur. Environ 100 g par pièce.", de:"Hähnchenbruststeaks frittiert-gedämpft. Ca. 100 g pro Stück." } }, p2: { code: "02FRT01", packaging: { en:"10 kg boxes", nl:"10 kg dozen", fr:"Cartons de 10 kg", de:"10-kg-Kartons" }, origin: { en:"Thailand", nl:"Thailand", fr:"Thaïlande", de:"Thailand" }, lead: { en:"4 to 5 months", nl:"4 tot 5 maanden", fr:"4 à 5 mois", de:"4 bis 5 Monate" }, name: { en:"Marinated chicken breast steaks fry-steamed", nl:"Gemarineerde kipfilet steaks frysteamed", fr:"Steaks de filet de poulet marinés fri vapeur", de:"Marinierte Hähnchenbruststeaks frittiert-gedämpft" }, text: { en:"Marinated chicken breast steaks fry-steamed. Approx. 110 g per piece.", nl:"Gemarineerde kipfilet steaks frysteamed. Ongeveer 110 g per stuk.", fr:"Steaks de filet de poulet marinés fri vapeur. Environ 110 g par pièce.", de:"Marinierte Hähnchenbruststeaks frittiert-gedämpft. Ca. 110 g pro Stück." } }, p3: { code: "06FR001", packaging: { en:"10 kg boxes", nl:"10 kg dozen", fr:"Cartons de 10 kg", de:"10-kg-Kartons" }, origin: { en:"Europe", nl:"Europa", fr:"Europe", de:"Europa" }, lead: { en:"1 to 3 months", nl:"1 tot 3 maanden", fr:"1 à 3 mois", de:"1 bis 3 Monate" }, name: { en:"Fry-steamed mini steaks (Chickenchips)", nl:"Ministeaks van kip frysteamed (Chickenchips)", fr:"Mini steaks de poulet fri vapeur (Chickenchips)", de:"Mini-Hähnchensteaks frittiert-gedämpft (Chickenchips)" }, text: { en:"Fry-steamed mini steaks (Chickenchips). Cut chicken tenderloin pieces, marinated and processed.", nl:"Ministeaks van kip frysteamed (Chickenchips). Kiphaasjes gesneden, gemarineerd en bewerkt.", fr:"Mini steaks de poulet fri vapeur (Chickenchips). Aiguillettes de poulet coupées, marinées et travaillées.", de:"Mini-Hähnchensteaks frittiert-gedämpft (Chickenchips). Geschnittene Hähnchen-Innenfilets, mariniert und verarbeitet." } }, p4: { code: "02KPT01", packaging: { en:"10 x 1 kg", nl:"10 x 1 kg", fr:"10 x 1 kg", de:"10 x 1 kg" }, origin: { en:"Thailand", nl:"Thailand", fr:"Thaïlande", de:"Thailand" }, lead: { en:"4 to 5 months", nl:"4 tot 5 maanden", fr:"4 à 5 mois", de:"4 bis 5 Monate" }, name: { en:'Breaded chicken goujon tenders "Succes" ± 50 g', nl:'Gepaneerde kipfilethaasjes "Tenders Succes" ± 50 g', fr:'Aiguillettes de poulet panées "Tenders Succes" ± 50 g', de:'Panierte Hähnchenfiletstreifen "Tenders Succes" ± 50 g' }, text: { en:"Breaded chicken goujon tenders. Approx. 50 g per piece.", nl:"Gepaneerde kipfilethaasjes. Ongeveer 50 g per stuk.", fr:"Aiguillettes de poulet panées. Environ 50 g par pièce.", de:"Panierte Hähnchenfiletstreifen. Ca. 50 g pro Stück." } } }; function val(id){ const el = document.getElementById(id); return el ? String(el.value || "").trim() : ""; } function setVal(id, v){ const el = document.getElementById(id); if(el) el.value = (v || ""); } function radio(name){ const r = form.querySelector('input[name="'+name+'"]:checked'); return r ? r.value : ""; } function lang(){ return radio("lang") || "en"; } function yesNo(v, L){ if(L === "nl") return v === "yes" ? "Ja" : "Nee"; if(L === "fr") return v === "yes" ? "Oui" : "Non"; if(L === "de") return v === "yes" ? "Ja" : "Nein"; return v === "yes" ? "Yes" : "No"; } function showError(msg){ if(!msg){ formError.style.display = "none"; formError.textContent = ""; return; } formError.style.display = "block"; formError.textContent = msg; } function headings(){ const L = lang(); const H = { en:{ title:"ORDER ON CALL PLANNING", subtitle:"Planning request – only valid after written confirmation by Ruan BV (email).", intro:"Purpose: align planning for incoming stock and call-off deliveries. This request supports allocation and scheduling, but it does not create a guaranteed delivery commitment until Ruan BV confirms in writing.", reserve:"Reserved stock (after confirmation): when goods arrive, the agreed quantity is placed on a separate reserved stock list/location under the Buyer’s company name and delivered on call-off as planned.", discipline:"Buyer commitment: the Buyer confirms the requested quantity and call-off plan is realistic and will be followed as agreed. Significant deviations may require re-planning and may affect availability.", fm:"Supply and transport can be affected by events beyond reasonable control (e.g. production disruptions, carrier delays, force majeure). If this occurs, planning may be adjusted or cancelled if delivery becomes impossible. Ruan BV will communicate promptly and propose the best available alternative.", s1:"1. Supplier", s2:"2. Buyer", s3:"3. Product", s4:"4. Planning details", s5:"5. Remarks", s6:"6. Confirmation", lbl:{ supplierName:"Supplier name", supplierAddress:"Supplier address", companyNo:"Company number", sendTo:"Send to (email)", buyerCompany:"Company name", buyerAddress:"Address", buyerCity:"Postal code and city", buyerVat:"VAT number", contact:"Contact person", buyerEmail:"Email", productCode:"Product code / reference", productName:"Product name", packaging:"Packaging", origin:"Country of production", productText:"Product text", reqPrice:"Requested price per kg (EUR)", priceTbc:"Price: to be confirmed by Ruan BV upon acceptance.", reqQty:"Requested reserved quantity (kg)", calloff:"Call-off delivery", monthly:"Expected monthly call-off (kg/month)", deposit:"Deposit", reqDate:"Request date", lead:"Indicative lead time", target:"Preferred target delivery date", earliest:"Earliest acceptable delivery date", location:"Delivery location", remark:"Remark" } }, nl:{ title:"ORDER ON CALL PLANNING", subtitle:"Planningsaanvraag – enkel geldig na schriftelijke bevestiging door Ruan BV (e-mail).", intro:"Doel: planning afstemmen voor aankomende voorraad en leveringen op afroep. Deze aanvraag ondersteunt allocatie en timing, maar creëert geen gegarandeerde leveringsverbintenis zolang Ruan BV niet schriftelijk bevestigt.", reserve:"Gereserveerde voorraad (na bevestiging): bij aankomst wordt de afgesproken hoeveelheid op een aparte stocklijst/locatie gezet onder de naam van de koper en geleverd op afroep volgens planning.", discipline:"Engagement koper: de koper bevestigt dat de reservatiehoeveelheid en afroepplanning realistisch zijn en nagekomen worden. Grote afwijkingen kunnen herplanning vereisen en de beschikbaarheid beïnvloeden.", fm:"Aanvoer en transport kunnen beïnvloed worden door omstandigheden buiten redelijke controle (bv. productiestoringen, transportvertragingen, overmacht). In dat geval kan de planning worden aangepast of geannuleerd indien levering onmogelijk wordt. Ruan BV communiceert zo snel mogelijk en stelt een passend alternatief voor.", s1:"1. Leverancier", s2:"2. Koper", s3:"3. Product", s4:"4. Planningsdetails", s5:"5. Opmerking", s6:"6. Bevestiging", lbl:{ supplierName:"Naam leverancier", supplierAddress:"Adres leverancier", companyNo:"Ondernemingsnummer", sendTo:"Te verzenden naar (e-mail)", buyerCompany:"Bedrijfsnaam", buyerAddress:"Adres", buyerCity:"Postcode en gemeente", buyerVat:"BTW-nummer", contact:"Contactpersoon", buyerEmail:"E-mail", productCode:"Productcode / referentie", productName:"Productnaam", packaging:"Verpakking", origin:"Land van productie", productText:"Producttekst", reqPrice:"Gevraagde prijs per kg (EUR)", priceTbc:"Prijs: te bevestigen door Ruan BV bij aanvaarding.", reqQty:"Gevraagde gereserveerde hoeveelheid (kg)", calloff:"Levering op afroep", monthly:"Verwachte maandelijkse afroep (kg/maand)", deposit:"Voorschot", reqDate:"Aanvraagdatum", lead:"Indicatieve lead time", target:"Gewenste leverdatum", earliest:"Vroegst aanvaardbare leverdatum", location:"Leveradres / locatie", remark:"Opmerking" } }, fr:{ title:"ORDER ON CALL PLANNING", subtitle:"Demande de planification – valable uniquement après confirmation écrite par Ruan BV (email).", intro:"Objectif : aligner la planification du stock entrant et des livraisons sur appel. Cette demande facilite l’allocation et le planning, mais ne crée pas d’engagement de livraison garanti tant que Ruan BV n’a pas confirmé par écrit.", reserve:"Stock réservé (après confirmation) : à l’arrivée, la quantité convenue est placée sur une liste/emplacement séparé au nom de l’acheteur et livrée sur appel selon le planning.", discipline:"Engagement acheteur : l’acheteur confirme que la quantité réservée et le plan de prélèvement sont réalistes et seront respectés. Des écarts importants peuvent nécessiter une replanification et affecter la disponibilité.", fm:"L’approvisionnement et le transport peuvent être affectés par des événements hors du contrôle raisonnable (p. ex. perturbations de production, retards transporteur, force majeure). Dans ce cas, le planning peut être adapté ou annulé si la livraison devient impossible. Ruan BV communiquera rapidement et proposera la meilleure alternative disponible.", s1:"1. Fournisseur", s2:"2. Acheteur", s3:"3. Produit", s4:"4. Détails de planification", s5:"5. Remarque", s6:"6. Confirmation", lbl:{ supplierName:"Nom du fournisseur", supplierAddress:"Adresse du fournisseur", companyNo:"Numéro d’entreprise", sendTo:"Envoyer à (email)", buyerCompany:"Nom de la société", buyerAddress:"Adresse", buyerCity:"Code postal et ville", buyerVat:"Numéro TVA", contact:"Personne de contact", buyerEmail:"Email", productCode:"Code produit / référence", productName:"Nom du produit", packaging:"Emballage", origin:"Pays de production", productText:"Texte produit", reqPrice:"Prix demandé par kg (EUR)", priceTbc:"Prix : à confirmer par Ruan BV après acceptation.", reqQty:"Quantité demandée à réserver (kg)", calloff:"Livraison sur appel", monthly:"Prélèvement mensuel prévu (kg/mois)", deposit:"Acompte", reqDate:"Date de demande", lead:"Délai indicatif", target:"Date de livraison souhaitée", earliest:"Date de livraison la plus tôt acceptable", location:"Lieu de livraison", remark:"Remarque" } }, de:{ title:"ORDER ON CALL PLANNING", subtitle:"Planungsanfrage – nur gültig nach schriftlicher Bestätigung durch Ruan BV (E-Mail).", intro:"Zweck: Planung für eingehende Ware und Abruflieferungen abstimmen. Diese Anfrage unterstützt Zuteilung und Terminierung, begründet jedoch keine garantierte Lieferverpflichtung, solange Ruan BV nicht schriftlich bestätigt.", reserve:"Reservierter Bestand (nach Bestätigung): bei Ankunft wird die vereinbarte Menge auf einer separaten Bestandsliste/Position unter dem Firmennamen des Käufers geführt und gemäß Planung auf Abruf geliefert.", discipline:"Verpflichtung Käufer: der Käufer bestätigt, dass Reservemenge und Abrufplan realistisch sind und eingehalten werden. Wesentliche Abweichungen können Neuplanung erfordern und die Verfügbarkeit beeinflussen.", fm:"Lieferung und Transport können durch Ereignisse außerhalb angemessener Kontrolle beeinflusst werden (z. B. Produktionsstörungen, Verzögerungen beim Transport, höhere Gewalt). In diesem Fall kann die Planung angepasst oder storniert werden, wenn eine Lieferung unmöglich wird. Ruan BV informiert zeitnah und schlägt die beste verfügbare Alternative vor.", s1:"1. Lieferant", s2:"2. Käufer", s3:"3. Produkt", s4:"4. Planungsdetails", s5:"5. Bemerkung", s6:"6. Bestätigung", lbl:{ supplierName:"Name Lieferant", supplierAddress:"Adresse Lieferant", companyNo:"Unternehmensnummer", sendTo:"Senden an (E-Mail)", buyerCompany:"Firmenname", buyerAddress:"Adresse", buyerCity:"PLZ und Ort", buyerVat:"USt-IdNr.", contact:"Ansprechpartner", buyerEmail:"E-Mail", productCode:"Produktcode / Referenz", productName:"Produktname", packaging:"Verpackung", origin:"Produktionsland", productText:"Produkttext", reqPrice:"Angefragter Preis pro kg (EUR)", priceTbc:"Preis: von Ruan BV bei Annahme zu bestätigen.", reqQty:"Angefragte reservierte Menge (kg)", calloff:"Abruflieferung", monthly:"Erwarteter monatlicher Abruf (kg/Monat)", deposit:"Anzahlung", reqDate:"Anfragedatum", lead:"Indikative Lieferzeit", target:"Bevorzugtes Lieferdatum", earliest:"Frühestes akzeptables Lieferdatum", location:"Lieferort", remark:"Bemerkung" } } }; return H[L] || H.en; } function subjectLine(){ const L = lang(); const product = val("productName") || (L==="nl"?"Product":L==="fr"?"Produit":L==="de"?"Produkt":"Product"); const qty = val("reqQty") ? (" – " + val("reqQty") + " kg") : ""; const company = val("buyerCompany") ? (" – " + val("buyerCompany")) : ""; return "Order on Call Planning – " + product + qty + company; } function safeLine(label, value){ return value ? (label + ": " + value) : ""; } function applyProduct(key){ const P = products[key]; const L = lang(); if(!P) return; setVal("productCode", P.code || ""); setVal("packaging", (P.packaging && P.packaging[L]) ? P.packaging[L] : ""); setVal("countryProduction", (P.origin && P.origin[L]) ? P.origin[L] : ""); setVal("leadTime", (P.lead && P.lead[L]) ? P.lead[L] : ""); setVal("productName", (P.name && P.name[L]) ? P.name[L] : ""); setVal("productText", (P.text && P.text[L]) ? P.text[L] : ""); } function updateMonthlyVisibility(){ const on = (radio("calloff") === "yes"); monthlyWrap.style.display = on ? "grid" : "none"; monthlyCalloff.required = on; if(!on) monthlyCalloff.value = ""; } function buildEmailText(){ const H = headings(); const L = lang(); const lbl = H.lbl; const lines = []; lines.push(H.title); lines.push(H.subtitle); lines.push(""); lines.push(H.intro); lines.push(H.reserve); lines.push(H.discipline); lines.push(H.fm); lines.push(""); lines.push(H.s1); lines.push(safeLine(lbl.supplierName, val("supplierName"))); lines.push(safeLine(lbl.supplierAddress, val("supplierAddress"))); lines.push(safeLine(lbl.companyNo, val("supplierCompanyNo"))); lines.push(safeLine(lbl.sendTo, val("sendToEmail"))); lines.push(""); lines.push(H.s2); lines.push(safeLine(lbl.buyerCompany, val("buyerCompany"))); lines.push(safeLine(lbl.buyerAddress, val("buyerAddress"))); lines.push(safeLine(lbl.buyerCity, val("buyerCity"))); lines.push(safeLine(lbl.buyerVat, val("buyerVat"))); lines.push(safeLine(lbl.contact, val("contactPerson"))); lines.push(safeLine(lbl.buyerEmail, val("buyerEmail"))); lines.push(""); lines.push(H.s3); lines.push(safeLine(lbl.productCode, val("productCode"))); lines.push(safeLine(lbl.productName, val("productName"))); lines.push(safeLine(lbl.packaging, val("packaging"))); lines.push(safeLine(lbl.origin, val("countryProduction"))); lines.push(safeLine(lbl.productText, val("productText"))); lines.push(""); lines.push(H.s4); lines.push(val("reqPrice") ? (lbl.reqPrice + ": " + val("reqPrice")) : lbl.priceTbc); lines.push(lbl.reqQty + ": " + (val("reqQty") || "")); lines.push(lbl.calloff + ": " + yesNo(radio("calloff"), L)); if(radio("calloff") === "yes"){ lines.push(lbl.monthly + ": " + (val("monthlyCalloff") || "")); } const dep = radio("deposit") === "yes" ? (L==="nl"?"Ja (5%) – enkel na bevestiging":L==="fr"?"Oui (5%) – uniquement après confirmation":L==="de"?"Ja (5%) – nur nach Bestätigung":"Yes (5%) – only after confirmation") : (L==="nl"?"Nee":L==="fr"?"Non":L==="de"?"Nein":"No"); lines.push(lbl.deposit + ": " + dep); lines.push(lbl.reqDate + ": " + (val("reservationDate") || "")); if(val("leadTime")) lines.push(lbl.lead + ": " + val("leadTime")); if(val("targetDate")) lines.push(lbl.target + ": " + val("targetDate")); if(val("earliestDate")) lines.push(lbl.earliest + ": " + val("earliestDate")); if(val("deliveryLocation")) lines.push(lbl.location + ": " + val("deliveryLocation")); lines.push(""); lines.push(H.s5); lines.push(val("remark") ? val("remark") : "-"); lines.push(""); lines.push(H.s6); lines.push(L==="nl" ? "De koper bevestigt dat deze planningsaanvraag correct is en pas geldig wordt na schriftelijke bevestiging door Ruan BV (e-mail)." : L==="fr" ? "L’acheteur confirme que cette demande est correcte et ne devient valable qu’après confirmation écrite par Ruan BV (email)." : L==="de" ? "Der Käufer bestätigt, dass diese Planungsanfrage korrekt ist und erst nach schriftlicher Bestätigung durch Ruan BV (E-Mail) gültig wird." : "The Buyer confirms this planning request is correct and becomes valid only after written confirmation by Ruan BV (email)." ); return lines.filter(Boolean).join("\n"); } function escapeHtml(s){ return s.replace(/[&"']/g, (c) => ({ "&":"&", "":">", '"':""", "'":"'" }[c])); } function updateKV(){ const L = lang(); const H = headings(); const lbl = H.lbl; const items = [ [lbl.buyerCompany, val("buyerCompany")], [lbl.contact, val("contactPerson")], [lbl.buyerEmail, val("buyerEmail")], [lbl.productName, val("productName")], [lbl.reqQty, val("reqQty") ? (val("reqQty") + " kg") : ""], [lbl.calloff, yesNo(radio("calloff"), L)], [lbl.monthly, radio("calloff") === "yes" ? (val("monthlyCalloff") ? (val("monthlyCalloff") + " kg/month") : "") : ""], [lbl.reqDate, val("reservationDate")], [lbl.lead, val("leadTime")] ]; kv.innerHTML = items .filter(([k,v]) => (v && String(v).trim().length > 0)) .map(([k,v]) => `
`)
.join("");
}
function updatePreview(){
showError("");
updateMonthlyVisibility();
const text = buildEmailText();
preview.textContent = text;
finalText.value = text;
updateKV();
}
function validateForEmail(){
showError("");
const requiredIds = ["sendToEmail","buyerCompany","contactPerson","buyerEmail","productName","reqQty","reservationDate","acceptTerms"];
for(const id of requiredIds){
const el = document.getElementById(id);
if(el && el.required){
if(el.type === "checkbox"){
if(!el.checked) return { ok:false, msg:"Please tick the confirmation checkbox before opening email." };
}else{
if(!String(el.value || "").trim()) return { ok:false, msg:"Please complete all required fields before opening email." };
}
}
}
if(radio("calloff") === "yes" && !String(monthlyCalloff.value || "").trim()){
return { ok:false, msg:"Monthly call-off is required when Call-off is set to Yes." };
}
return { ok:true };
}
function mailtoUrl(){
const to = encodeURIComponent(val("sendToEmail"));
const subject = encodeURIComponent(subjectLine());
const body = encodeURIComponent(finalText.value || "");
return `mailto:${to}?subject=${subject}&body=${body}`;
}
function gmailComposeUrl(){
const to = encodeURIComponent(val("sendToEmail"));
const subject = encodeURIComponent(subjectLine());
const body = encodeURIComponent(finalText.value || "");
return `https://mail.google.com/mail/?view=cm&fs=1&to=${to}&su=${subject}&body=${body}`;
}
function outlookComposeUrl(){
const to = encodeURIComponent(val("sendToEmail"));
const subject = encodeURIComponent(subjectLine());
const body = encodeURIComponent(finalText.value || "");
return `https://outlook.office.com/mail/deeplink/compose?to=${to}&subject=${subject}&body=${body}`;
}
function setTodayIfEmpty(){
const d = new Date();
const iso = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())).toISOString().slice(0,10);
if(!reservationDateEl.value) reservationDateEl.value = iso;
}
function refreshProductForLanguage(){
const key = productSelect.value;
if(key && key !== "manual" && products[key]){
applyProduct(key);
}
}
/* Init */
setTodayIfEmpty();
updateMonthlyVisibility();
updatePreview();
form.addEventListener("input", updatePreview);
form.addEventListener("change", (e) => {
if(e.target && e.target.name === "lang"){
refreshProductForLanguage();
}
if(e.target && e.target.id === "productSelect"){
const key = productSelect.value;
if(key && key !== "manual"){
applyProduct(key);
}
}
updatePreview();
});
btnPrint.addEventListener("click", () => window.print());
btnReset.addEventListener("click", () => {
form.reset();
setTodayIfEmpty();
updateMonthlyVisibility();
updatePreview();
});
btnCopy.addEventListener("click", async () => {
try{
await navigator.clipboard.writeText(finalText.value || "");
showError("");
}catch(err){
finalText.focus();
finalText.select();
try{ document.execCommand("copy"); }catch(e){}
}
});
btnOpenEmail.addEventListener("click", () => {
const v = validateForEmail();
if(!v.ok){ showError(v.msg); return; }
window.location.href = mailtoUrl();
});
btnOpenGmail.addEventListener("click", () => {
const v = validateForEmail();
if(!v.ok){ showError(v.msg); return; }
window.open(gmailComposeUrl(), "_blank", "noopener");
});
btnOpenOutlook.addEventListener("click", () => {
const v = validateForEmail();
if(!v.ok){ showError(v.msg); return; }
window.open(outlookComposeUrl(), "_blank", "noopener");
});
btnOpenAppleMail.addEventListener("click", () => {
const v = validateForEmail();
if(!v.ok){ showError(v.msg); return; }
window.location.href = mailtoUrl();
});
if(productSelect.value && productSelect.value !== "manual"){
applyProduct(productSelect.value);
updatePreview();
}
})();