From 776bd6366da2a2965575e76b9aace51745249290 Mon Sep 17 00:00:00 2001 From: "Randy.Fischer" Date: Wed, 15 Apr 2026 15:25:38 +0200 Subject: [PATCH] Add PWA-ready MVP: split HTML/CSS/JS + DDD layering + service worker - public/index.html: markup-only entry, classic + + + + + + + + + + + + + + + + + + + + + diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..6fa8978 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,114 @@ +(function () { + const A = window.App.Application; + const D = window.App.Domain; + const I = window.App.Infrastructure; + + function openOrder(orderKey) { + A.state.currentOrderKey = orderKey; + const order = A.state.orders.find(o => o.orderKey === orderKey); + if (!order) return; + A.resetForm(); + A.prefillFromOrder(order); + A.showScreen('screen-form'); + A.loadSavedData(orderKey); + } + + function goBackToOverview() { + A.saveCurrentForm(); + A.state.currentOrderKey = null; + A.resetForm(); + A.renderOverviewScreen(openOrder); + A.showScreen('screen-overview'); + } + + async function markOpnameGereed() { + if (!A.state.currentOrderKey) { alert('Geen order geselecteerd.'); return; } + await A.saveCurrentForm(); + A.state.orderStatuses[A.state.currentOrderKey] = 'Opname gereed'; + await I.saveOrderStatus(A.state.currentOrderKey, 'Opname gereed'); + goBackToOverview(); + } + + function wireOverviewToolbar() { + document.getElementById('csvFileInput').addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) return; + A.state.orders = await I.loadOrdersFromUpload(file); + A.renderOverviewScreen(openOrder); + e.target.value = ''; + }); + + document.getElementById('btnOpenInstall').addEventListener('click', () => A.openInstallScreen()); + + document.querySelectorAll('.filter-btn').forEach(btn => { + btn.addEventListener('click', () => A.setFilter(btn.dataset.filter, openOrder)); + }); + + document.querySelectorAll('.col-filter').forEach(inp => { + inp.addEventListener('input', () => { + inp.classList.toggle('has-value', inp.value.length > 0); + A.setColumnFilter(inp.dataset.col, inp.value, openOrder); + }); + }); + } + + function wireInstallScreen() { + document.getElementById('btnInstallBack').addEventListener('click', () => { + A.showScreen('screen-overview'); + }); + } + + function wireFormToolbar() { + document.getElementById('btnBack').addEventListener('click', goBackToOverview); + document.getElementById('btnOpnameGereed').addEventListener('click', markOpnameGereed); + document.getElementById('btnExport').addEventListener('click', () => A.exportFormData()); + document.getElementById('btnPrint').addEventListener('click', () => window.print()); + + document.getElementById('xmlFileInput').addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) return; + A.parseAndLoadXML(await file.text()); + }); + + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => A.switchTab(btn.dataset.tab)); + }); + + document.getElementById('cameraInput').addEventListener('change', A.handlePhotoCapture); + document.getElementById('galleryInput').addEventListener('change', A.handlePhotoCapture); + document.getElementById('photoModalClose').addEventListener('click', A.closePhotoModal); + document.getElementById('lightbox').addEventListener('click', A.closeLightbox); + document.getElementById('btnOpenSvg').addEventListener('click', () => { + window.open('../PDF/Plaatje wissel_GW.svg', '_blank'); + }); + } + + function wireKeyboard() { + document.addEventListener('keydown', e => { + if (e.key === 'Escape') { A.closePhotoModal(); A.closeLightbox(); } + }); + } + + async function init() { + await I.openDB(); + A.state.orders = D.parseCSV(I.SEED_ORDERS_CSV); + A.state.orderStatuses = await I.loadAllOrderStatuses(); + + wireOverviewToolbar(); + wireFormToolbar(); + wireInstallScreen(); + wireKeyboard(); + + A.renderOverviewScreen(openOrder); + A.initColumnResize(); + A.initOverviewCapture(); + + A.pwa.register(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/public/js/namespace.js b/public/js/namespace.js new file mode 100644 index 0000000..e28c863 --- /dev/null +++ b/public/js/namespace.js @@ -0,0 +1 @@ +window.App = window.App || { Domain: {}, Application: {}, Infrastructure: {} }; diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..81a1538 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "Duimstok-inspecties Bovenbouw", + "short_name": "Duimstok", + "description": "ProRail duimstok-inspecties voor wissels, overwegen en spoor.", + "lang": "nl", + "dir": "ltr", + "start_url": "./index.html", + "scope": "./", + "display": "standalone", + "orientation": "any", + "background_color": "#f5f5f5", + "theme_color": "#003082", + "icons": [ + { + "src": "icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..ed08810 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,72 @@ +const CACHE_VERSION = 'duimstok-v1'; + +const APP_SHELL = [ + './', + './index.html', + './manifest.webmanifest', + './icons/icon.svg', + './css/base.css', + './css/overview.css', + './css/form.css', + './css/modals.css', + './css/install.css', + './css/responsive.css', + './js/namespace.js', + './js/main.js', + '../src/Domain/sectionMap.js', + '../src/Domain/scoring.js', + '../src/Domain/orderParser.js', + '../src/Infrastructure/utils.js', + '../src/Infrastructure/geolocation.js', + '../src/Infrastructure/db.js', + '../src/Infrastructure/seedOrders.js', + '../src/Infrastructure/csvLoader.js', + '../src/Application/state.js', + '../src/Application/persistence.js', + '../src/Application/screens.js', + '../src/Application/photoService.js', + '../src/Application/inspectionForm.js', + '../src/Application/orderOverview.js', + '../src/Application/xmlImport.js', + '../src/Application/exportService.js', + '../src/Application/pwa.js', + '../src/Application/installScreen.js' +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_VERSION).then((cache) => cache.addAll(APP_SHELL)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))) + ).then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + const req = event.request; + if (req.method !== 'GET') return; + const url = new URL(req.url); + if (url.origin !== self.location.origin) return; + + event.respondWith( + caches.match(req).then((cached) => { + if (cached) return cached; + return fetch(req).then((resp) => { + if (resp && resp.status === 200 && resp.type === 'basic') { + const copy = resp.clone(); + caches.open(CACHE_VERSION).then((cache) => cache.put(req, copy)); + } + return resp; + }).catch(() => { + if (req.mode === 'navigate') return caches.match('./index.html'); + return new Response('', { status: 504, statusText: 'Offline' }); + }); + }) + ); +}); diff --git a/src/Application/exportService.js b/src/Application/exportService.js new file mode 100644 index 0000000..bb724dd --- /dev/null +++ b/src/Application/exportService.js @@ -0,0 +1,34 @@ +(function (A, I) { + A.exportFormData = async function () { + await A.saveCurrentForm(); + const fd = A.state.formData; + const x = ['', '
']; + x.push(`${I.esc(fd.orderNr)}`); + x.push(`${I.esc(document.getElementById('inp_inspecteur').value)}`); + x.push(`${I.esc(document.getElementById('inp_inspectiedatum').value)}`); + x.push(`${I.esc(document.getElementById('inp_techjaar').value)}`); + x.push(`${I.esc(document.getElementById('inp_inspjaar').value)}`); + x.push(`${I.esc(document.getElementById('inp_opmerkingen').value)}`); + x.push(''); + for (const [pos, photos] of Object.entries(fd.overviewPhotos)) { + photos.forEach((_, i) => x.push(`${I.esc(pos)}${I.esc(fd.orderNr.replace('/', '-'))}_${pos}_foto${i + 1}`)); + } + x.push(''); + x.push(''); + for (const [nr, data] of Object.entries(fd.scores)) { + const fNrs = (fd.photos[nr] || []).map((_, i) => `${fd.orderNr}_loc${nr}_foto${i + 1}`).join('; '); + x.push(`${nr}${I.esc(data.dwl || '')}${I.esc(data.bal || '')}${I.esc(fNrs)}${I.esc(data.opm || '')}`); + } + x.push('', '
'); + I.downloadBlob(new Blob([x.join('\n')], { type: 'application/xml' }), + `${fd.orderNr.replace('/', '-')}_inspectie_${new Date().toISOString().slice(0, 10)}.xml`); + let pc = 0; + for (const [nr, photos] of Object.entries(fd.photos)) { + photos.forEach((p, i) => { pc++; I.downloadBlob(I.dataUrlToBlob(p.dataUrl), `${fd.orderNr.replace('/', '-')}_loc${nr}_foto${i + 1}.jpg`); }); + } + for (const [pos, photos] of Object.entries(fd.overviewPhotos)) { + photos.forEach((p, i) => { pc++; I.downloadBlob(I.dataUrlToBlob(p.dataUrl), `${fd.orderNr.replace('/', '-')}_${pos}_foto${i + 1}.jpg`); }); + } + alert(`Opgeslagen!\n- 1 XML-bestand\n- ${pc} foto('s)`); + }; +})(window.App.Application, window.App.Infrastructure); diff --git a/src/Application/inspectionForm.js b/src/Application/inspectionForm.js new file mode 100644 index 0000000..9f08eb3 --- /dev/null +++ b/src/Application/inspectionForm.js @@ -0,0 +1,144 @@ +(function (A, D, I) { + A.switchTab = function (tabId) { + document.querySelectorAll('.tab-btn').forEach(btn => + btn.classList.toggle('active', btn.dataset.tab === tabId)); + document.querySelectorAll('.tab-panel').forEach(panel => + panel.classList.toggle('active', panel.id === 'panel-' + tabId)); + }; + + A.resetForm = function () { + A.state.formData = A.emptyFormData(); + document.querySelectorAll('.header-grid .value').forEach(el => el.textContent = ''); + document.querySelectorAll('#hdr_taakomschrijving, #hdr_aanleiding').forEach(el => el.textContent = ''); + document.getElementById('inp_inspecteur').value = ''; + document.getElementById('inp_inspectiedatum').value = ''; + document.getElementById('inp_techjaar').value = ''; + document.getElementById('inp_inspjaar').value = ''; + document.getElementById('inp_opmerkingen').value = ''; + document.getElementById('assessmentBody').innerHTML = ''; + const totaal = document.getElementById('totaalscore'); + totaal.textContent = '-'; + totaal.className = 'cell score-cell'; + document.getElementById('totaal_fotonummers').textContent = ''; + document.getElementById('statusFotos').textContent = "Foto's: 0"; + document.getElementById('statusSaved').textContent = ''; + document.getElementById('statusOrder').textContent = ''; + A.updateOverviewBadges(); + A.renderOverviewThumbs(); + A.switchTab('inspectie'); + }; + + A.prefillFromOrder = function (order) { + A.state.formData.orderNr = order.orderKey; + document.getElementById('hdr_omschrijving').textContent = order.omschrijving; + document.getElementById('hdr_eqart').textContent = order.objectsoort; + document.getElementById('hdr_startpoint').textContent = order.startpunt; + document.getElementById('hdr_endpoint').textContent = order.eindpunt; + document.getElementById('hdr_orderoperatie').textContent = order.orderKey; + document.getElementById('inp_inspecteur').value = order.inspecteur || ''; + document.getElementById('statusOrder').textContent = 'Order: ' + order.orderKey + ' | ' + order.omschrijving; + document.getElementById('formToolbarTitle').textContent = order.omschrijving; + }; + + function buildScoreOptions(selected) { + return '' + + D.SCORE_VALUES.map(v => ``).join(''); + } + + A.buildAssessmentTable = function (scoreElements) { + const tbody = document.getElementById('assessmentBody'); + tbody.innerHTML = ''; + let currentSection = ''; + scoreElements.forEach(el => { + const nr = parseInt(el.querySelector('NR').textContent.trim()); + const locatie = el.querySelector('LOCATIE').textContent.trim(); + const dwlScore = el.querySelector('DWL_SCORE').textContent.trim(); + const balScore = el.querySelector('BAL_SCORE').textContent.trim(); + const section = D.getSectionForNr(nr); + if (section && section !== currentSection) { + currentSection = section; + const sr = document.createElement('tr'); + sr.className = 'section-row'; + sr.innerHTML = `${section}`; + tbody.appendChild(sr); + } + const tr = document.createElement('tr'); + tr.dataset.nr = nr; + tr.innerHTML = ` + ${nr} + ${locatie} + + + + `; + tbody.appendChild(tr); + if (!A.state.formData.scores[nr]) A.state.formData.scores[nr] = { dwl: dwlScore, bal: balScore, opm: '' }; + if (!A.state.formData.photos[nr]) A.state.formData.photos[nr] = []; + + const dwlSel = tr.querySelector('select[data-type="dwl"]'); + if (dwlScore) dwlSel.className = D.scoreClass(dwlScore); + const balSel = tr.querySelector('select[data-type="bal"]'); + if (balScore) balSel.className = D.scoreClass(balScore); + }); + + tbody.querySelectorAll('select[data-nr]').forEach(sel => sel.addEventListener('change', () => onScoreChange(sel))); + tbody.querySelectorAll('input[data-field="opm"]').forEach(inp => inp.addEventListener('input', () => onOpmChange(inp))); + tbody.querySelectorAll('button[data-action="open-photo"]').forEach(btn => + btn.addEventListener('click', () => A.openPhotoModal(+btn.dataset.nr, btn.dataset.loc))); + }; + + function onScoreChange(sel) { + const nr = parseInt(sel.dataset.nr); + const type = sel.dataset.type; + const val = sel.value; + if (!A.state.formData.scores[nr]) A.state.formData.scores[nr] = { dwl: '', bal: '', opm: '' }; + A.state.formData.scores[nr][type] = val; + sel.className = D.scoreClass(val); + A.updateTotaalscore(); + A.autoSave(); + } + + function onOpmChange(inp) { + const nr = parseInt(inp.dataset.nr); + if (!A.state.formData.scores[nr]) A.state.formData.scores[nr] = { dwl: '', bal: '', opm: '' }; + A.state.formData.scores[nr].opm = inp.value; + A.autoSave(); + } + + A.updateTotaalscore = function () { + const worst = D.computeWorstScore(A.state.formData.scores); + const el = document.getElementById('totaalscore'); + el.textContent = worst || '-'; + el.className = 'cell score-cell'; + if (worst) el.classList.add(D.scoreClass(worst)); + }; + + A.loadSavedData = async function (orderNr) { + const saved = await I.loadInspection(orderNr); + if (!saved) return; + if (saved.inspecteur) document.getElementById('inp_inspecteur').value = saved.inspecteur; + if (saved.inspectiedatum) document.getElementById('inp_inspectiedatum').value = saved.inspectiedatum; + if (saved.techJaar) document.getElementById('inp_techjaar').value = saved.techJaar; + if (saved.inspJaar) document.getElementById('inp_inspjaar').value = saved.inspJaar; + if (saved.opmerkingen) document.getElementById('inp_opmerkingen').value = saved.opmerkingen; + if (saved.scores) { + A.state.formData.scores = saved.scores; + for (const [nr, data] of Object.entries(saved.scores)) { + const dS = document.querySelector(`select[data-nr="${nr}"][data-type="dwl"]`); + const bS = document.querySelector(`select[data-nr="${nr}"][data-type="bal"]`); + const oI = document.querySelector(`input[data-nr="${nr}"][data-field="opm"]`); + if (dS && data.dwl) { dS.value = data.dwl; dS.className = D.scoreClass(data.dwl); } + if (bS && data.bal) { bS.value = data.bal; bS.className = D.scoreClass(data.bal); } + if (oI && data.opm) oI.value = data.opm; + } + A.updateTotaalscore(); + } + if (saved.photos) { A.state.formData.photos = saved.photos; A.updatePhotoBadges(); } + if (saved.overviewPhotos) { + A.state.formData.overviewPhotos = saved.overviewPhotos; + A.updateOverviewBadges(); + A.renderOverviewThumbs(); + } + document.getElementById('statusSaved').textContent = 'Eerder opgeslagen data hersteld'; + }; +})(window.App.Application, window.App.Domain, window.App.Infrastructure); diff --git a/src/Application/installScreen.js b/src/Application/installScreen.js new file mode 100644 index 0000000..aa7962b --- /dev/null +++ b/src/Application/installScreen.js @@ -0,0 +1,147 @@ +(function (A) { + function statusBlock() { + if (A.pwa.isFileProtocol()) { + return `
+ Let op: de app is nu geopend vanaf uw bestandssysteem + (file://). Installeren als PWA werkt alleen via een webserver + (bijv. http://localhost of een gedeploydde versie). +
`; + } + if (A.pwa.isStandalone()) { + return `
+ Geïnstalleerd. U gebruikt de app al in PWA-modus. +
`; + } + if (!A.pwa.isSupported()) { + return `
+ Deze browser ondersteunt PWA-installatie niet volledig. Probeer het in + Chrome, Edge, Safari (iOS 16.4+) of een Chromium-gebaseerde browser. +
`; + } + return `
+ Volg onderstaande stappen om de app op dit apparaat te installeren. +
`; + } + + function stepsForPlatform(platform) { + if (platform === 'ios') { + return `
    +
  1. Tik onderin het scherm op het Delen-pictogram + (vierkant met pijl omhoog).
  2. +
  3. Scrol omlaag en tik op Zet op beginscherm.
  4. +
  5. Bevestig met Voeg toe. Het Duimstok-icoon verschijnt + tussen uw apps.
  6. +
  7. Open de app vanaf het beginscherm voor volledige werking offline.
  8. +
+

+ iOS ondersteunt geen automatische installatieknop. Installatie gebeurt + altijd via het Delen-menu van Safari. +

`; + } + if (platform === 'android') { + return `
    +
  1. Gebruik de knop App installeren hieronder, of
  2. +
  3. Tik rechtsboven op \u22EE (menu) en kies + App installeren / Toevoegen aan startscherm.
  4. +
  5. Bevestig met Installeren.
  6. +
`; + } + if (platform === 'desktop-chromium') { + return `
    +
  1. Gebruik de knop App installeren hieronder, of
  2. +
  3. Klik op het installeerpictogram \u2B07 rechts in de + adresbalk.
  4. +
  5. Als alternatief: menu \u22EE + → AppsDeze site installeren.
  6. +
`; + } + if (platform === 'firefox') { + return `
    +
  1. Firefox desktop ondersteunt PWA-installatie niet standaard.
  2. +
  3. Maak een snelkoppeling op het bureaublad via + BestandPagina opslaan als... + of gebruik Firefox op Android, waar Toevoegen aan startscherm + wel werkt.
  4. +
`; + } + if (platform === 'desktop-safari') { + return `
    +
  1. Ga naar het menu Archief → + Voeg toe aan Dock (macOS Sonoma of nieuwer).
  2. +
  3. De app verschijnt in het Dock en werkt offline.
  4. +
`; + } + return `
    +
  1. Uw browser is niet herkend. Zoek in het menu naar + "Installeren" of "Toevoegen aan startscherm".
  2. +
`; + } + + function benefitsBlock() { + return `
+

Voordelen van installatie

+ +
`; + } + + function mainCTA() { + if (A.pwa.isFileProtocol()) return ''; + if (A.pwa.isStandalone()) return ''; + return ``; + } + + A.renderInstallScreen = function () { + const body = document.getElementById('installBody'); + const platform = A.pwa.getPlatform(); + body.innerHTML = ` +
+ Duimstok app-icoon +

Duimstok-inspecties als app

+

Installeer deze applicatie op uw telefoon, tablet of desktop + voor offline gebruik in het veld.

+
+ ${statusBlock()} +
+

Installatie op uw apparaat

+ ${stepsForPlatform(platform)} + ${mainCTA()} +
+ ${benefitsBlock()} + `; + + const cta = document.getElementById('installCta'); + if (cta && !cta.disabled) { + cta.addEventListener('click', async () => { + cta.disabled = true; + cta.textContent = 'Bezig...'; + await A.pwa.promptInstall(); + A.renderInstallScreen(); + }); + } + }; + + A.openInstallScreen = function () { + A.renderInstallScreen(); + A.showScreen('screen-install'); + }; + + document.addEventListener('pwa:install-available', () => { + if (document.getElementById('screen-install').classList.contains('active')) { + A.renderInstallScreen(); + } + }); + + document.addEventListener('pwa:installed', () => { + if (document.getElementById('screen-install').classList.contains('active')) { + A.renderInstallScreen(); + } + }); +})(window.App.Application); diff --git a/src/Application/orderOverview.js b/src/Application/orderOverview.js new file mode 100644 index 0000000..b96eb6d --- /dev/null +++ b/src/Application/orderOverview.js @@ -0,0 +1,147 @@ +(function (A, D) { + function getOrderDisplayFields(o) { + const status = D.getEffectiveStatus(o, A.state.orderStatuses); + const done = D.isCompleted(o, A.state.orderStatuses); + const statusText = done ? (status === 'Gereed' ? 'Gereed' : 'Opname gereed') : 'Op te nemen'; + return { + orderText: o.order + '/' + o.operatie, + typeText: o.objectsoort, + omschrText: o.omschrijving, + hoevhText: o.hoeveelheid + ' ' + o.eenheid, + statusText + }; + } + + function matchesColumnFilters(o) { + const d = getOrderDisplayFields(o); + const lc = s => s.toLowerCase(); + const f = A.state.columnFilters; + if (f.order && !lc(d.orderText).includes(lc(f.order))) return false; + if (f.type && !lc(d.typeText).includes(lc(f.type))) return false; + if (f.omschrijving && !lc(d.omschrText).includes(lc(f.omschrijving))) return false; + if (f.hoeveelheid && !lc(d.hoevhText).includes(lc(f.hoeveelheid))) return false; + if (f.status && !lc(d.statusText).includes(lc(f.status))) return false; + return true; + } + + function renderSummaryCards() { + const { orders, orderStatuses } = A.state; + const remaining = orders.filter(o => !D.isCompleted(o, orderStatuses)); + const wissels = remaining.filter(o => o.objectsoort === 'Wissel').length; + const overwegen = remaining.filter(o => o.objectsoort === 'Overwegbevloering').length; + const spoorMeters = remaining.filter(o => o.objectsoort === 'Spoor') + .reduce((sum, o) => sum + o.hoeveelheid, 0); + + document.getElementById('summaryCards').innerHTML = ` +
+
+
${wissels}
Wissels nog op te nemen
+
+
+
+
${overwegen}
Overwegbevloeringen nog op te nemen
+
+
+
+
${spoorMeters} m
Meter spoor nog op te nemen
+
+
+
+
${remaining.length} / ${orders.length}
Totaal nog uit te voeren
+
+ `; + + const done = orders.filter(o => D.isCompleted(o, orderStatuses)).length; + const pct = orders.length > 0 ? Math.round((done / orders.length) * 100) : 0; + document.getElementById('progressFill').style.width = pct + '%'; + document.getElementById('progressText').textContent = `${done} van ${orders.length} voltooid (${pct}%)`; + } + + function renderOrderTable(onRowClick) { + const tbody = document.getElementById('orderTableBody'); + const filtered = A.state.orders.filter(o => { + if (A.state.currentFilter === 'open' && D.isCompleted(o, A.state.orderStatuses)) return false; + if (A.state.currentFilter !== 'alle' && A.state.currentFilter !== 'open' + && o.objectsoort !== A.state.currentFilter) return false; + return matchesColumnFilters(o); + }); + + tbody.innerHTML = filtered.map(o => { + const d = getOrderDisplayFields(o); + const done = D.isCompleted(o, A.state.orderStatuses); + const typeClass = o.objectsoort === 'Wissel' ? 'wissel' + : o.objectsoort === 'Overwegbevloering' ? 'overweg' : 'spoor'; + const statusClass = done ? 'done' : 'open'; + return ` + ${o.order}/${o.operatie} + ${o.objectsoort} + ${o.omschrijving} + ${o.hoeveelheid} ${o.eenheid} + ${d.statusText} + `; + }).join(''); + + tbody.querySelectorAll('tr[data-order-key]').forEach(tr => { + tr.addEventListener('click', () => onRowClick(tr.dataset.orderKey)); + }); + } + + A.renderOverviewScreen = function (onRowClick) { + renderSummaryCards(); + renderOrderTable(onRowClick); + }; + + A.setFilter = function (filter, onRowClick) { + A.state.currentFilter = filter; + document.querySelectorAll('.filter-btn').forEach(b => { + b.classList.toggle('active', b.dataset.filter === filter); + }); + renderOrderTable(onRowClick); + }; + + A.setColumnFilter = function (col, value, onRowClick) { + A.state.columnFilters[col] = value; + renderOrderTable(onRowClick); + }; + + A.initColumnResize = function () { + const table = document.getElementById('orderTable'); + if (!table) return; + const cols = table.querySelectorAll('colgroup col'); + const handles = table.querySelectorAll('.col-resize'); + + handles.forEach(handle => { + handle.addEventListener('mousedown', startResize); + handle.addEventListener('touchstart', startResize, { passive: false }); + }); + + function startResize(e) { + e.preventDefault(); + e.stopPropagation(); + const colIdx = parseInt(e.target.dataset.col); + const col = cols[colIdx]; + if (!col) return; + const th = e.target.parentElement; + const startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; + const startWidth = th.offsetWidth; + e.target.classList.add('dragging'); + + function onMove(ev) { + const clientX = ev.type === 'touchmove' ? ev.touches[0].clientX : ev.clientX; + const newWidth = Math.max(50, startWidth + (clientX - startX)); + col.style.width = newWidth + 'px'; + } + function onUp() { + e.target.classList.remove('dragging'); + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + document.removeEventListener('touchmove', onMove); + document.removeEventListener('touchend', onUp); + } + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + document.addEventListener('touchmove', onMove, { passive: false }); + document.addEventListener('touchend', onUp); + } + }; +})(window.App.Application, window.App.Domain); diff --git a/src/Application/persistence.js b/src/Application/persistence.js new file mode 100644 index 0000000..8c8dd14 --- /dev/null +++ b/src/Application/persistence.js @@ -0,0 +1,22 @@ +(function (A, I) { + function readFormFields() { + const fd = A.state.formData; + fd.inspecteur = document.getElementById('inp_inspecteur').value; + fd.inspectiedatum = document.getElementById('inp_inspectiedatum').value; + fd.techJaar = document.getElementById('inp_techjaar').value; + fd.inspJaar = document.getElementById('inp_inspjaar').value; + fd.opmerkingen = document.getElementById('inp_opmerkingen').value; + } + + A.saveCurrentForm = async function () { + if (!A.state.formData.orderNr) return; + readFormFields(); + await I.saveInspection(A.state.formData); + document.getElementById('statusSaved').textContent = + 'Opgeslagen: ' + new Date().toLocaleTimeString('nl-NL'); + }; + + A.autoSave = function () { + A.scheduleAutoSave(() => A.saveCurrentForm()); + }; +})(window.App.Application, window.App.Infrastructure); diff --git a/src/Application/photoService.js b/src/Application/photoService.js new file mode 100644 index 0000000..d660afa --- /dev/null +++ b/src/Application/photoService.js @@ -0,0 +1,163 @@ +(function (A, D, I) { + A.openPhotoModal = function (nr, loc) { + A.state.currentPhotoRow = nr; + document.getElementById('photoModalTitle').textContent = `Foto's - Nr ${nr}: ${loc}`; + document.getElementById('photoModal').classList.add('active'); + A.renderPhotoGrid(); + }; + + A.closePhotoModal = function () { + document.getElementById('photoModal').classList.remove('active'); + A.state.currentPhotoRow = null; + }; + + A.handlePhotoCapture = async function (e) { + if (!e.target.files.length || A.state.currentPhotoRow === null) return; + const row = A.state.currentPhotoRow; + for (const file of e.target.files) { + if (!A.state.formData.photos[row]) A.state.formData.photos[row] = []; + A.state.formData.photos[row].push({ + dataUrl: await I.readFileAsDataUrl(file), + timestamp: new Date().toISOString(), + gps: await I.getGPS(), + filename: file.name + }); + } + e.target.value = ''; + A.renderPhotoGrid(); + A.updatePhotoBadges(); + A.autoSave(); + }; + + A.renderPhotoGrid = function () { + const grid = document.getElementById('photoGrid'); + const row = A.state.currentPhotoRow; + const photos = A.state.formData.photos[row] || []; + if (!photos.length) { + grid.innerHTML = '

Nog geen foto\'s.

'; + return; + } + grid.innerHTML = photos.map((p, i) => ` +
+ + +
${new Date(p.timestamp).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' })}${p.gps ? ' \u2316' : ''}
+
`).join(''); + + grid.querySelectorAll('img[data-r]').forEach(img => img.addEventListener('click', () => { + const ph = A.state.formData.photos[img.dataset.r]?.[+img.dataset.i]; + if (ph) A.openLightbox(ph.dataUrl); + })); + grid.querySelectorAll('button[data-action="delete-photo"]').forEach(btn => btn.addEventListener('click', (ev) => { + ev.stopPropagation(); + A.deletePhoto(+btn.dataset.r, +btn.dataset.i); + })); + }; + + A.deletePhoto = function (nr, i) { + if (!confirm('Foto verwijderen?')) return; + A.state.formData.photos[nr].splice(i, 1); + A.renderPhotoGrid(); + A.updatePhotoBadges(); + A.autoSave(); + }; + + A.updatePhotoBadges = function () { + let total = 0; + for (const [nr, photos] of Object.entries(A.state.formData.photos)) { + const b = document.getElementById('badge_' + nr); + if (b) b.textContent = photos.length || ''; + total += photos.length; + } + for (const photos of Object.values(A.state.formData.overviewPhotos)) total += photos.length; + document.getElementById('statusFotos').textContent = `Foto's: ${total}`; + const sum = []; + for (const [nr, photos] of Object.entries(A.state.formData.photos)) { + if (photos.length) sum.push(`Loc${nr}(${photos.length})`); + } + document.getElementById('totaal_fotonummers').textContent = sum.join(', '); + }; + + A.handleOverviewCapture = async function (e) { + const pos = e.target.dataset.pos; + if (!e.target.files.length || !pos) return; + if (!A.state.formData.overviewPhotos[pos]) A.state.formData.overviewPhotos[pos] = []; + for (const file of e.target.files) { + A.state.formData.overviewPhotos[pos].push({ + dataUrl: await I.readFileAsDataUrl(file), + timestamp: new Date().toISOString(), + gps: await I.getGPS(), + filename: file.name + }); + } + e.target.value = ''; + A.updateOverviewBadges(); + A.renderOverviewThumbs(); + A.autoSave(); + }; + + A.updateOverviewBadges = function () { + let tot = 0; + for (const [pos, photos] of Object.entries(A.state.formData.overviewPhotos)) { + const b = document.getElementById('cbadge_' + pos); + if (b) b.textContent = photos.length || ''; + tot += photos.length; + } + const tb = document.getElementById('tabBadgeOvz'); + tb.textContent = tot; + tb.classList.toggle('visible', tot > 0); + A.updatePhotoBadges(); + }; + + A.renderOverviewThumbs = function () { + const grid = document.getElementById('overviewThumbGrid'); + const all = []; + for (const [pos, photos] of Object.entries(A.state.formData.overviewPhotos)) { + photos.forEach((p, i) => all.push({ pos, index: i, photo: p, label: D.overviewPositions[pos] || pos })); + } + if (!all.length) { + grid.innerHTML = '
Nog geen foto\'s gemaakt.
'; + return; + } + grid.innerHTML = all.map(it => ` +
+ + +
${it.label}
+
`).join(''); + + grid.querySelectorAll('img[data-pos]').forEach(img => img.addEventListener('click', ev => { + ev.stopPropagation(); + const ph = A.state.formData.overviewPhotos[img.dataset.pos]?.[+img.dataset.idx]; + if (ph) A.openLightbox(ph.dataUrl); + })); + grid.querySelectorAll('button[data-action="delete-ov-photo"]').forEach(btn => btn.addEventListener('click', ev => { + ev.stopPropagation(); + A.deleteOvPhoto(btn.dataset.pos, +btn.dataset.idx); + })); + }; + + A.deleteOvPhoto = function (pos, i) { + if (!confirm('Foto verwijderen?')) return; + A.state.formData.overviewPhotos[pos].splice(i, 1); + if (!A.state.formData.overviewPhotos[pos].length) delete A.state.formData.overviewPhotos[pos]; + A.updateOverviewBadges(); + A.renderOverviewThumbs(); + A.autoSave(); + }; + + A.initOverviewCapture = function () { + document.querySelectorAll('.capture-label input[type="file"]').forEach(inp => + inp.addEventListener('change', A.handleOverviewCapture)); + }; + + A.openLightbox = function (src) { + document.getElementById('lightboxImg').src = src; + document.getElementById('lightbox').classList.add('active'); + }; + + A.closeLightbox = function () { + document.getElementById('lightbox').classList.remove('active'); + document.getElementById('lightboxImg').src = ''; + }; +})(window.App.Application, window.App.Domain, window.App.Infrastructure); diff --git a/src/Application/pwa.js b/src/Application/pwa.js new file mode 100644 index 0000000..73b9ac1 --- /dev/null +++ b/src/Application/pwa.js @@ -0,0 +1,59 @@ +(function (A) { + let deferredPrompt = null; + let swRegistration = null; + + A.pwa = { + isFileProtocol() { + return location.protocol === 'file:'; + }, + isSupported() { + return 'serviceWorker' in navigator && !A.pwa.isFileProtocol(); + }, + isStandalone() { + return window.matchMedia('(display-mode: standalone)').matches + || window.navigator.standalone === true; + }, + getPlatform() { + const ua = navigator.userAgent; + const isIOS = /iPad|iPhone|iPod/.test(ua) && !window.MSStream; + const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; + if (isIOS || isIPadOS) return 'ios'; + if (/Android/.test(ua)) return 'android'; + if (/Chrome|Chromium|Edg/.test(ua)) return 'desktop-chromium'; + if (/Firefox/.test(ua)) return 'firefox'; + if (/Safari/.test(ua)) return 'desktop-safari'; + return 'other'; + }, + canPromptInstall() { + return deferredPrompt !== null; + }, + async promptInstall() { + if (!deferredPrompt) return { outcome: 'unavailable' }; + deferredPrompt.prompt(); + const choice = await deferredPrompt.userChoice; + deferredPrompt = null; + return choice; + }, + async register() { + if (!A.pwa.isSupported()) return null; + try { + swRegistration = await navigator.serviceWorker.register('./sw.js', { scope: './' }); + return swRegistration; + } catch (err) { + console.warn('Service worker registration failed:', err); + return null; + } + } + }; + + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + deferredPrompt = e; + document.dispatchEvent(new CustomEvent('pwa:install-available')); + }); + + window.addEventListener('appinstalled', () => { + deferredPrompt = null; + document.dispatchEvent(new CustomEvent('pwa:installed')); + }); +})(window.App.Application); diff --git a/src/Application/screens.js b/src/Application/screens.js new file mode 100644 index 0000000..ed69a5c --- /dev/null +++ b/src/Application/screens.js @@ -0,0 +1,7 @@ +(function (A) { + A.showScreen = function (id) { + document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); + document.getElementById(id).classList.add('active'); + window.scrollTo(0, 0); + }; +})(window.App.Application); diff --git a/src/Application/state.js b/src/Application/state.js new file mode 100644 index 0000000..8e0de94 --- /dev/null +++ b/src/Application/state.js @@ -0,0 +1,26 @@ +(function (A) { + A.state = { + orders: [], + orderStatuses: {}, + currentFilter: 'alle', + currentOrderKey: null, + currentPhotoRow: null, + columnFilters: { order: '', type: '', omschrijving: '', hoeveelheid: '', status: '' }, + formData: emptyFormData() + }; + + A.emptyFormData = emptyFormData; + + function emptyFormData() { + return { + orderNr: '', inspecteur: '', inspectiedatum: '', techJaar: '', inspJaar: '', + opmerkingen: '', scores: {}, photos: {}, overviewPhotos: {} + }; + } + + let saveTimeout = null; + A.scheduleAutoSave = function (saveFn) { + if (saveTimeout) clearTimeout(saveTimeout); + saveTimeout = setTimeout(saveFn, 2000); + }; +})(window.App.Application); diff --git a/src/Application/xmlImport.js b/src/Application/xmlImport.js new file mode 100644 index 0000000..f2a2ad9 --- /dev/null +++ b/src/Application/xmlImport.js @@ -0,0 +1,52 @@ +(function (A, I) { + A.parseAndLoadXML = function (xmlText) { + const parser = new DOMParser(); + const xml = parser.parseFromString(xmlText, 'text/xml'); + const getText = (tag) => { const el = xml.querySelector(tag); return el ? el.textContent.trim() : ''; }; + + const orderNr = getText('AUFNR_VORNR'); + A.state.formData.orderNr = orderNr; + A.state.currentOrderKey = orderNr; + + document.getElementById('hdr_geo').textContent = getText('GEO'); + document.getElementById('hdr_geotxt').textContent = getText('GEOTXT'); + document.getElementById('hdr_equnr').textContent = getText('EQUNR'); + document.getElementById('hdr_wisselnr').textContent = getText('WISSELNR') || getText('EQFNR_EQUI'); + document.getElementById('hdr_eqart').textContent = getText('EQART'); + document.getElementById('hdr_soort').textContent = getText('SOORT'); + document.getElementById('hdr_startpoint').textContent = getText('START_POINT'); + document.getElementById('hdr_endpoint').textContent = getText('END_POINT'); + document.getElementById('hdr_hoekverhouding').textContent = getText('HOEKVERHOUDING'); + document.getElementById('hdr_omschrijving').textContent = getText('EQKTX'); + document.getElementById('hdr_orderoperatie').textContent = getText('AUFNR_VORNR'); + document.getElementById('hdr_profiel').textContent = getText('PROFIEL'); + document.getElementById('hdr_dwarsligger').textContent = getText('DWARSLIGGER') || '-'; + document.getElementById('hdr_afwijking').textContent = getText('AFWIJKING') || '-'; + document.getElementById('hdr_hergebruikt').textContent = '-'; + document.getElementById('hdr_wisselverw').textContent = '-'; + document.getElementById('hdr_spoor').textContent = getText('SPOOR'); + document.getElementById('hdr_afschr').textContent = getText('ZAFSCHR_GRP_CODE'); + document.getElementById('hdr_meetegengebogen').textContent = '-'; + document.getElementById('hdr_classificatie').textContent = '-'; + document.getElementById('hdr_voegloos').textContent = '-'; + document.getElementById('hdr_plaatsingsdatum').textContent = I.formatDate(getText('DATUM_START')); + document.getElementById('hdr_aanschafdatum').textContent = '-'; + document.getElementById('hdr_generatie').textContent = '-'; + document.getElementById('hdr_taakomschrijving').textContent = getText('LTXA1'); + document.getElementById('hdr_aanleiding').textContent = getText('KURZTEXT'); + + const eqktx = getText('EQKTX'); + if (eqktx) document.getElementById('overzichtTitle').textContent = eqktx; + document.getElementById('formToolbarTitle').textContent = eqktx || orderNr; + + if (getText('INSPECTEUR')) document.getElementById('inp_inspecteur').value = getText('INSPECTEUR'); + if (getText('INSP_DATUM')) document.getElementById('inp_inspectiedatum').value = getText('INSP_DATUM'); + if (getText('TECH_JAAR')) document.getElementById('inp_techjaar').value = getText('TECH_JAAR'); + if (getText('INSP_JAAR')) document.getElementById('inp_inspjaar').value = getText('INSP_JAAR'); + if (getText('OPMERKING')) document.getElementById('inp_opmerkingen').value = getText('OPMERKING'); + + A.buildAssessmentTable(xml.querySelectorAll('WISSEL_SCORE')); + document.getElementById('statusOrder').textContent = 'Order: ' + orderNr + ' | ' + eqktx; + A.loadSavedData(orderNr); + }; +})(window.App.Application, window.App.Infrastructure); diff --git a/src/Domain/orderParser.js b/src/Domain/orderParser.js new file mode 100644 index 0000000..4e5699d --- /dev/null +++ b/src/Domain/orderParser.js @@ -0,0 +1,32 @@ +(function (D) { + D.parseCSV = function (csvText) { + const lines = csvText.trim().split('\n'); + if (lines.length < 2) return []; + return lines.slice(1).filter(l => l.trim()).map(line => { + const cols = line.split(';'); + const operatie = cols[1].trim(); + return { + order: cols[0].trim(), + operatie: operatie, + orderKey: cols[0].trim() + '/' + operatie.padStart(4, '0'), + objectsoort: cols[2].trim(), + omschrijving: cols[3].trim(), + startpunt: cols[4].trim(), + eindpunt: cols[5].trim(), + hoeveelheid: parseFloat(cols[6].replace(',', '.')), + eenheid: cols[7].trim(), + csvStatus: cols[8].trim(), + inspecteur: cols[9] ? cols[9].trim() : '' + }; + }); + }; + + D.getEffectiveStatus = function (order, orderStatuses) { + return orderStatuses[order.orderKey] || order.csvStatus; + }; + + D.isCompleted = function (order, orderStatuses) { + const s = D.getEffectiveStatus(order, orderStatuses); + return s === 'Opname gereed' || s === 'Gereed'; + }; +})(window.App.Domain); diff --git a/src/Domain/scoring.js b/src/Domain/scoring.js new file mode 100644 index 0000000..de8d93a --- /dev/null +++ b/src/Domain/scoring.js @@ -0,0 +1,17 @@ +(function (D) { + D.SCORE_VALUES = ['B1','B2','B3','G1','G2','G3','G4','E3','E4','E5','E6']; + const SCORE_ORDER = ['', ...D.SCORE_VALUES]; + + D.computeWorstScore = function (scores) { + let wIdx = 0, wScore = ''; + for (const data of Object.values(scores)) { + const m = Math.max(SCORE_ORDER.indexOf(data.dwl || ''), SCORE_ORDER.indexOf(data.bal || '')); + if (m > wIdx) { wIdx = m; wScore = SCORE_ORDER[m]; } + } + return wScore; + }; + + D.scoreClass = function (value) { + return value ? 'score-' + value.toLowerCase() : ''; + }; +})(window.App.Domain); diff --git a/src/Domain/sectionMap.js b/src/Domain/sectionMap.js new file mode 100644 index 0000000..75c4ee6 --- /dev/null +++ b/src/Domain/sectionMap.js @@ -0,0 +1,22 @@ +(function (D) { + D.sectionMap = { + 'Sectie 1: Tongbeweging': [1, 2, 3, 4], + 'Sectie 2: Middengedeelte': [5, 6, 7, 8, 9, 10], + 'Sectie 3: Puntstuk gedeelte': [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23] + }; + + D.getSectionForNr = function (nr) { + for (const [section, nrs] of Object.entries(D.sectionMap)) { + if (nrs.includes(nr)) return section; + } + return null; + }; + + D.overviewPositions = { + ovz_tl: 'Overzichtsfoto LB', ovz_tr: 'Overzichtsfoto RB', + ovz_bl: 'Overzichtsfoto LO', ovz_br: 'Overzichtsfoto RO', + foto_s3_l: 'Foto Puntstuk L', foto_s3_r: 'Foto Puntstuk R', + foto_s2_l: 'Foto Midden L', foto_s2_r: 'Foto Midden R', + foto_s1_l: 'Foto Tong L', foto_s1_r: 'Foto Tong R' + }; +})(window.App.Domain); diff --git a/src/Infrastructure/csvLoader.js b/src/Infrastructure/csvLoader.js new file mode 100644 index 0000000..e9f4970 --- /dev/null +++ b/src/Infrastructure/csvLoader.js @@ -0,0 +1,6 @@ +(function (I, D) { + I.loadOrdersFromUpload = async function (file) { + const text = await file.text(); + return D.parseCSV(text); + }; +})(window.App.Infrastructure, window.App.Domain); diff --git a/src/Infrastructure/db.js b/src/Infrastructure/db.js new file mode 100644 index 0000000..ab859f2 --- /dev/null +++ b/src/Infrastructure/db.js @@ -0,0 +1,69 @@ +(function (I) { + const DB_NAME = 'DuimstokInspecties'; + const DB_VERSION = 3; + let db = null; + + I.openDB = function () { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onupgradeneeded = (e) => { + const d = e.target.result; + if (!d.objectStoreNames.contains('inspections')) + d.createObjectStore('inspections', { keyPath: 'orderNr' }); + if (!d.objectStoreNames.contains('orderStatuses')) + d.createObjectStore('orderStatuses', { keyPath: 'orderKey' }); + }; + req.onsuccess = (e) => { db = e.target.result; resolve(db); }; + req.onerror = (e) => reject(e); + }); + }; + + async function ensureDb() { + if (!db) await I.openDB(); + return db; + } + + I.saveInspection = async function (formData) { + const d = await ensureDb(); + return new Promise((resolve, reject) => { + const tx = d.transaction('inspections', 'readwrite'); + tx.objectStore('inspections').put(JSON.parse(JSON.stringify(formData))); + tx.oncomplete = () => resolve(); + tx.onerror = reject; + }); + }; + + I.loadInspection = async function (orderNr) { + const d = await ensureDb(); + return new Promise((resolve, reject) => { + const tx = d.transaction('inspections', 'readonly'); + const req = tx.objectStore('inspections').get(orderNr); + req.onsuccess = () => resolve(req.result); + req.onerror = reject; + }); + }; + + I.saveOrderStatus = async function (orderKey, status) { + const d = await ensureDb(); + return new Promise((resolve, reject) => { + const tx = d.transaction('orderStatuses', 'readwrite'); + tx.objectStore('orderStatuses').put({ orderKey, status, updatedAt: new Date().toISOString() }); + tx.oncomplete = resolve; + tx.onerror = reject; + }); + }; + + I.loadAllOrderStatuses = async function () { + const d = await ensureDb(); + return new Promise((resolve, reject) => { + const tx = d.transaction('orderStatuses', 'readonly'); + const req = tx.objectStore('orderStatuses').getAll(); + req.onsuccess = () => { + const map = {}; + (req.result || []).forEach(r => { map[r.orderKey] = r.status; }); + resolve(map); + }; + req.onerror = reject; + }); + }; +})(window.App.Infrastructure); diff --git a/src/Infrastructure/geolocation.js b/src/Infrastructure/geolocation.js new file mode 100644 index 0000000..e3f25fe --- /dev/null +++ b/src/Infrastructure/geolocation.js @@ -0,0 +1,12 @@ +(function (I) { + I.getGPS = function () { + return new Promise(resolve => { + if (!navigator.geolocation) { resolve(null); return; } + navigator.geolocation.getCurrentPosition( + p => resolve({ lat: p.coords.latitude, lng: p.coords.longitude, accuracy: p.coords.accuracy }), + () => resolve(null), + { enableHighAccuracy: true, timeout: 5000, maximumAge: 30000 } + ); + }); + }; +})(window.App.Infrastructure); diff --git a/src/Infrastructure/seedOrders.js b/src/Infrastructure/seedOrders.js new file mode 100644 index 0000000..66c3235 --- /dev/null +++ b/src/Infrastructure/seedOrders.js @@ -0,0 +1,28 @@ +(function (I) { + I.SEED_ORDERS_CSV = `Order;Operatie;Objectsoort;Omschr. TO taak;Startpunt;Eindpunt;Operatiehoevh.;Eenheid;Status;Inspecteur +4170437;10;Spoor;034 sp RU DWL 300-13.900;9.752,00;9.782,00;30;m;Op te nemen;Hein de Vries - Middelkamp +4170439;10;Spoor;034 sp NE DWL 300-13.899;9.757,00;9.788,00;31;m;Op te nemen;Hein de Vries - Middelkamp +4170313;10;Overwegbevloering;033 sp 601a 15.3 bevl 1 Kanaaldijk;15.326,00;15.333,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4170314;10;Overwegbevloering;033 sp 602a 15.3 bevl 1 Kanaaldijk;15.326,00;15.333,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4170315;10;Overwegbevloering;033 sp DZ 16.1 bevl 1 Bockhorstweg;16.156,00;16.163,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4170330;10;Overwegbevloering;033 sp DZ 20.4 bevl 1 Hazenberg;20.450,00;20.459,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;10;Wissel;011 Wl 7 GW 1:9;67.818,00;67.856,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;20;Wissel;011 Wl 8 GW 1:9;67.847,00;67.882,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;40;Wissel;011 Wl 11 GW 1:9;67.869,00;67.908,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;30;Wissel;011 Wl 10 GW 1:9;67.887,00;67.926,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;50;Wissel;011 Wl 12 GW 1:9;67.905,00;67.941,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;60;Wissel;011 Wl 13 GW 1:9;67.908,00;67.944,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;70;Wissel;011 Wl 29 GW 1:9;68.522,00;68.561,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;80;Wissel;011 Wl 32 GW 1:9;68.579,00;68.617,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;90;Wissel;011 Wl 33 GW 1:9;68.582,00;68.619,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;100;Wissel;011 Wl 34 GW 1:9;68.592,00;68.626,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;120;Wissel;011 Wl 36 GW 1:9;68.618,00;68.653,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;110;Wissel;011 Wl 35 GW 1:9;68.620,00;68.655,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;130;Wissel;011 Wl 70 GW 1:9;68.811,00;68.850,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;140;Wissel;011 Wl 71 GW 1:9;68.855,00;68.891,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;150;Wissel;011 Wl 72 GW 1:9;68.882,00;68.918,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;160;Wissel;011 Wl 74 GW 1:9;68.915,00;68.948,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;170;Wissel;011 Wl 82 GW 1:9;69.089,00;69.127,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4551480;180;Wissel;011 Wl 86 GW 1:9;69.131,00;69.169,00;1;st;Op te nemen;Hein de Vries - Middelkamp +4170452;10;Spoor;038 sp WY DWL 95.975-105.000;96.357,00;96.829,00;472;m;Gereed;Hein de Vries - Middelkamp`; +})(window.App.Infrastructure); diff --git a/src/Infrastructure/utils.js b/src/Infrastructure/utils.js new file mode 100644 index 0000000..74b83b1 --- /dev/null +++ b/src/Infrastructure/utils.js @@ -0,0 +1,35 @@ +(function (I) { + I.esc = function (s) { + return String(s).replace(/&/g, '&').replace(//g, '>'); + }; + + I.downloadBlob = function (blob, name) { + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = name; + a.click(); + URL.revokeObjectURL(a.href); + }; + + I.dataUrlToBlob = function (dataUrl) { + const parts = dataUrl.split(','); + const mime = parts[0].match(/:(.*?);/)[1]; + const bin = atob(parts[1]); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + return new Blob([arr], { type: mime }); + }; + + I.readFileAsDataUrl = function (file) { + return new Promise(resolve => { + const fr = new FileReader(); + fr.onload = () => resolve(fr.result); + fr.readAsDataURL(file); + }); + }; + + I.formatDate = function (d) { + if (!d || d.length !== 8) return d || '-'; + return d.substring(6, 8) + '.' + d.substring(4, 6) + '.' + d.substring(0, 4); + }; +})(window.App.Infrastructure);