// Cloudflare Pages Function — /api/send-admin-mail
// Verstuurt een mail naar de admin van een cluster met een Excel-bijlage van de openstaande (nog niet betaalde) wagens
const SENDGRID_API_KEY = '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';
const FROM_EMAIL = 'atp@vanmosseltemplates.nl';
const FROM_NAME = 'Van Mossel – ATP (Dean Malezsewski)';
const REPLY_TO_EMAIL = 'dean.malezsewski@vanmossel.be';
const REPLY_TO_NAME = 'Dean Malezsewski';
function corsHeaders() {
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json',
};
}
function esc(s) {
if (!s) return '';
return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
function fmtDate(d) {
if (!d) return '';
const p = String(d).split('-');
return p.length === 3 ? `${p[2]}/${p[1]}/${p[0]}` : String(d);
}
// ===================== Minimale XLSX-generator (geen externe libraries) =====================
function crc32(buf) {
const table = crc32.table || (crc32.table = (() => {
const t = [];
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
t[n] = c >>> 0;
}
return t;
})());
let crc = 0xFFFFFFFF;
for (let i = 0; i < buf.length; i++) crc = table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
return (crc ^ 0xFFFFFFFF) >>> 0;
}
function strToBytes(str) {
return new TextEncoder().encode(str);
}
function makeZip(files) {
const localParts = [];
const centralParts = [];
let offset = 0;
for (const f of files) {
const nameBytes = strToBytes(f.name);
const data = f.data;
const crc = crc32(data);
const size = data.length;
const localHeader = new Uint8Array(30 + nameBytes.length);
const dv = new DataView(localHeader.buffer);
dv.setUint32(0, 0x04034b50, true);
dv.setUint16(4, 20, true);
dv.setUint16(6, 0, true);
dv.setUint16(8, 0, true);
dv.setUint16(10, 0, true);
dv.setUint16(12, 0, true);
dv.setUint32(14, crc, true);
dv.setUint32(18, size, true);
dv.setUint32(22, size, true);
dv.setUint16(26, nameBytes.length, true);
dv.setUint16(28, 0, true);
localHeader.set(nameBytes, 30);
localParts.push(localHeader, data);
const centralHeader = new Uint8Array(46 + nameBytes.length);
const cdv = new DataView(centralHeader.buffer);
cdv.setUint32(0, 0x02014b50, true);
cdv.setUint16(4, 20, true);
cdv.setUint16(6, 20, true);
cdv.setUint16(8, 0, true);
cdv.setUint16(10, 0, true);
cdv.setUint16(12, 0, true);
cdv.setUint16(14, 0, true);
cdv.setUint32(16, crc, true);
cdv.setUint32(20, size, true);
cdv.setUint32(24, size, true);
cdv.setUint16(28, nameBytes.length, true);
cdv.setUint16(30, 0, true);
cdv.setUint16(32, 0, true);
cdv.setUint16(34, 0, true);
cdv.setUint16(36, 0, true);
cdv.setUint32(38, 0, true);
cdv.setUint32(42, offset, true);
centralHeader.set(nameBytes, 46);
centralParts.push(centralHeader);
offset += localHeader.length + data.length;
}
const centralSize = centralParts.reduce((s, p) => s + p.length, 0);
const centralOffset = offset;
const eocd = new Uint8Array(22);
const edv = new DataView(eocd.buffer);
edv.setUint32(0, 0x06054b50, true);
edv.setUint16(4, 0, true);
edv.setUint16(6, 0, true);
edv.setUint16(8, files.length, true);
edv.setUint16(10, files.length, true);
edv.setUint32(12, centralSize, true);
edv.setUint32(16, centralOffset, true);
edv.setUint16(20, 0, true);
const totalSize = offset + centralSize + eocd.length;
const out = new Uint8Array(totalSize);
let pos = 0;
for (const p of localParts) { out.set(p, pos); pos += p.length; }
for (const p of centralParts) { out.set(p, pos); pos += p.length; }
out.set(eocd, pos);
return out;
}
function xmlEsc(s) {
return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
function colLetter(n) {
let s = '';
n++;
while (n > 0) {
const m = (n - 1) % 26;
s = String.fromCharCode(65 + m) + s;
n = Math.floor((n - 1) / 26);
}
return s;
}
function buildSheetXml(headers, rows) {
let rowsXml = `
Beste,
In bijlage vind je een Excel-lijst met de openstaande wagen(s) uit cluster ${esc(cluster || '')} (${vehicles.length} stuk(s)).
Kan je per wagen aanduiden of deze reeds betaald is, en het bestand terugsturen?
Alvast bedankt!
`; const htmlEmail = `