diff --git a/posClient.js b/posClient.js index e7e8cfe..98c63bf 100644 --- a/posClient.js +++ b/posClient.js @@ -14,11 +14,9 @@ function getConfig() { // 1. Authentifizierung & WWSVC-Headers nach Spezifikation function generateHeaders(appId, passId, algo = 'md5') { - // Zeitstempel Format: yyyy-MM-ddTHH:mm:ss oder epoch/ISO const ts = new Date().toISOString(); const reqId = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2); - // WWSVC Hash: MD5 oder SHA256 über appId + passId + ts const rawString = `${appId || ''}${passId || ''}${ts}`; const hash = crypto.createHash(algo === 'sha256' ? 'sha256' : 'md5').update(rawString).digest('hex'); @@ -40,7 +38,83 @@ function formatTime(d) { return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } -// 2. Datenabruf-Ablauf: CASPOS.EXEC (Vorbereitung) & CASPOS.GET (Download) +// QR-Code Setup Handshake +async function processQrSetup(qrPayload) { + let data = qrPayload; + if (typeof qrPayload === 'string') { + try { + data = JSON.parse(qrPayload.trim()); + } catch (e) { + throw new Error('Ungültiges QR-Code JSON-Format'); + } + } + + if (data.$type !== 'ww-setup') { + throw new Error('Ungültiger QR-Code-Typ: $type muss "ww-setup" sein'); + } + + const { url, servicepasslink, mdcDeviceId } = data; + if (!url || !servicepasslink) { + throw new Error('Fehlende Pflichtfelder (url oder servicepasslink) im QR-Code'); + } + + const cleanUrl = url.replace(/\/$/, ''); + + // 1. GET ServicePass: GET {url}/@LNK{servicepasslink} + const linkUrl = `${cleanUrl}/@LNK${servicepasslink}`; + const resLink = await fetch(linkUrl, { + method: 'GET', + headers: { 'User-Agent': 'Getraenkekarte-POS-Client/1.0' } + }); + + if (!resLink.ok) { + throw new Error(`Fehler beim Abruf von ServicePass (@LNK): ${resLink.status} ${resLink.statusText}`); + } + + const passData = await resLink.json(); + const passId = passData.passId || passData.PassId || passData.id; + const appId = passData.appId || passData.AppId || passData.key; + + if (!passId || !appId) { + throw new Error('Antwort von ServicePass enthält keine gültige passId/appId'); + } + + // 2. ServicePass registrieren (lokal in DB) + const upsert = db.prepare('INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'); + upsert.run('pos_url', cleanUrl); + upsert.run('pos_pass_id', passId); + upsert.run('pos_app_id', appId); + if (mdcDeviceId) upsert.run('pos_cash_register', String(mdcDeviceId)); + + // 3. Pass aktivieren (PUT /WWSVC/EXECURL/{passId}/CASPOS.PUT/REGISTERID={mdcDeviceId}/ENTITY=SERVICEPASS/SERVICEPASS_ID={passId}) + const devId = mdcDeviceId || '1'; + const activatePath = `/WWSVC/EXECURL/${passId}/CASPOS.PUT/REGISTERID=${devId}/ENTITY=SERVICEPASS/SERVICEPASS_ID=${passId}`; + const activateUrl = `${cleanUrl}${activatePath}`; + + const activateHeaders = generateHeaders(appId, passId); + try { + const actRes = await fetch(activateUrl, { + method: 'PUT', + headers: activateHeaders + }); + console.log(`ServicePass Activation Status: ${actRes.status}`); + } catch (err) { + console.warn('Aktivierungsaufruf Warnung:', err.message); + } + + db.prepare('INSERT INTO sync_logs (status, message) VALUES (?, ?)') + .run('SUCCESS', `QR-Code Handshake erfolgreich. Pass-ID: ${passId}, Kasse: ${devId}`); + + return { + success: true, + url: cleanUrl, + passId, + appId, + mdcDeviceId: devId + }; +} + +// Datenabruf-Ablauf: CASPOS.EXEC (Vorbereitung) & CASPOS.GET (Download) async function performSync(syncType = 'ERSTEINRICHTUNG') { const config = getConfig(); const { pos_url, pos_pass_id, pos_app_id, pos_cash_register, last_sync_time } = config; @@ -49,12 +123,11 @@ async function performSync(syncType = 'ERSTEINRICHTUNG') { throw new Error('POS Cloud URL nicht konfiguriert'); } - // Basis-URL sauber aufbereiten let baseUrl = pos_url.trim(); // A) Differenzabgleich Vorbereitung wenn ART=AENDERUNGEN if (syncType === 'AENDERUNGEN') { - let fromDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // Default: 24h zurück + let fromDate = new Date(Date.now() - 24 * 60 * 60 * 1000); if (last_sync_time) { const parsed = new Date(last_sync_time); if (!isNaN(parsed.getTime())) fromDate = parsed; @@ -113,14 +186,13 @@ async function performSync(syncType = 'ERSTEINRICHTUNG') { const result = await parseAndStoreZip(buffer); - // Letzten erfolgreichen Sync-Zeitstempel speichern db.prepare('INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value') .run('last_sync_time', new Date().toISOString()); return result; } -// 3. Inhalt ZIP & Dateitypen (JSON & XML Parsing für Products, Groups, Deletions) +// Inhalt ZIP & Dateitypen (JSON & XML Parsing) async function parseAndStoreZip(buffer) { const zip = new AdmZip(buffer); const zipEntries = zip.getEntries(); @@ -209,7 +281,6 @@ async function parseAndStoreZip(buffer) { const text2 = item.text2 || item.description || item.zutaten || item.Text2 || item.Description || ''; const groupId = String(item.groupId || item.warengruppe || item.GroupId || item.ProductGroupId || ''); - // Preisermittlung (preis1 / vk1 / standard brutto) let preis1 = 0; if (typeof item.prices === 'object' && item.prices !== null) { preis1 = parseFloat(item.prices.vk1 || item.prices.VK1 || item.prices.price1 || item.prices.standard || 0); @@ -264,7 +335,6 @@ async function parseAndStoreZip(buffer) { return { productsCount, groupsCount, deletionsCount, message: msg }; } -// Manueller/Mock Import für UI Testing function importJsonData(data) { const insertProduct = db.prepare(` INSERT INTO products (id, text1, text2, group_id, preis1, unit, active) @@ -311,6 +381,7 @@ function importJsonData(data) { } module.exports = { + processQrSetup, performSync, parseAndStoreZip, importJsonData, diff --git a/public/admin.html b/public/admin.html index f2d4016..eed2659 100644 --- a/public/admin.html +++ b/public/admin.html @@ -58,7 +58,7 @@ text-transform: uppercase; letter-spacing: 0.5px; } - input, select { + input, textarea, select { width: 100%; padding: 12px 14px; background: rgba(15, 23, 42, 0.8); @@ -68,8 +68,9 @@ font-size: 14px; outline: none; transition: border-color 0.2s; + font-family: inherit; } - input:focus { + input:focus, textarea:focus { border-color: var(--primary); } .btn { @@ -101,7 +102,11 @@ color: #090b10; } .btn-sync:hover { background: #0284c7; } - .btn-danger { background: var(--danger); color: #fff; } + .btn-qr { + background: #8b5cf6; + color: #fff; + } + .btn-qr:hover { background: #7c3aed; } .grid-2 { display: grid; @@ -184,7 +189,7 @@