Compare commits

...

3 Commits

Author SHA1 Message Date
Randy Fischer 684b89b8fc Add Engelsman, Kruising types and rebuild Overwegbevloering diagram
Three new inspection types based on analyzed reference diagrams:
- Engelsman (half/heel): 5-section layout with DWL/BAL scoring
- Kruising (incl. ingesloten): 4-section layout with DWL/BAL scoring
- Overwegbevloering: rebuilt as spatial diagram with 14 photo
  positions (sticker, spoorbord, 6x lage km, 6x hoge km)

Wissel subtype detection from SOORT field in SAP XML. Shared
DWL/BAL logic via usesDwlBal() helper. Section maps per type.
Overview photo export now includes all types.
2026-04-16 08:25:45 +02:00
Randy Fischer 6162b1b644 Add session summary and switch inspection diagram docs 2026-04-16 08:09:09 +02:00
Randy Fischer f2f9b65916 Add multi-inspection-type routing for Wissel, Overweg, and Spoor
Route inspection forms by objectsoort/EQART from CSV or SAP XML.
Each type gets its own header fields, assessment table layout,
overview photo grid, and XML export format. CSS toggles type-specific
sections via body class. SW cache bumped to v3.
2026-04-16 08:09:02 +02:00
11 changed files with 905 additions and 129 deletions

View File

@ -0,0 +1,236 @@
# Session Summary: Duimstok PWA MVP Build
**Date:** 2026-04-16
**Project:** ProRail Duimstok-inspecties Bovenbouw
**Repo:** https://git.en-masse.nl/randy/duimstok.prorail.nl.git
---
## Phase 1: Monolithic HTML Refactor
**Goal:** Split a single-file app (`duimstok-wissel-gw-sw.html`, 1566 lines) into a layered DDD structure.
**What was done:**
- Created `src/Domain/`, `src/Application/`, `src/Infrastructure/` layers
- Converted ES modules to classic IIFE scripts attaching to `window.App.*` namespace
- Embedded CSV seed data as a string constant in `seedOrders.js` (no `fetch()` needed)
- Removed all inline styles (`style=""`) and inline event handlers (`onclick`, `oninput`, etc.)
- Replaced inline styles with CSS classes (`.flex-spacer`, `.cap-arrow.lg`, `.section-sub.sm`, `.fotonummers-cell`)
- Wired all event listeners via `addEventListener` in `main.js` and service modules
**Result:** 30 files, ~2163 lines. Runs by double-clicking `public/index.html` -- no server needed.
**File structure:**
```
01_Applicatie/
├── public/
│ ├── index.html
│ ├── css/ base.css, overview.css, form.css, modals.css, responsive.css
│ └── js/ namespace.js, main.js
└── src/
├── Domain/ sectionMap.js, scoring.js, orderParser.js
├── Application/ state.js, persistence.js, screens.js, photoService.js,
│ inspectionForm.js, orderOverview.js, xmlImport.js, exportService.js
└── Infrastructure/ utils.js, geolocation.js, db.js, seedOrders.js, csvLoader.js
```
---
## Phase 2: PWA Research & Planning
**Prompt used:** "what are the next steps create a PWA version of this MVP, ideally with offline photo upload functionality which sync when back online. Whats the current supported state of PWA regarding these offline functionalities atm"
**Key findings:**
- Service Worker, Cache API, IndexedDB, Manifest, installability all work cross-browser (Chromium, Firefox, Safari/iOS 16.4+)
- Background Sync (`sync` event) only works on Chromium -- not Firefox, not Safari/iOS
- Periodic Background Sync: Chromium only (engagement-gated)
- Background Fetch: Chromium only
- Push notifications: universal for installed PWAs (iOS 16.4+)
- Persistent storage: universal, but Safari historically aggressive on eviction
**Key constraint:** iOS has no true background sync. Design around draining the queue when the user reopens the app and when the `online` event fires.
**6-step incremental plan identified:**
1. Installable shell (manifest + service worker cache)
2. Switch photo storage from data URLs to Blobs
3. Sync queue in IndexedDB
4. Trigger drain on every available signal (online event, app open, SW sync, manual button)
5. Persistent storage + install hint
6. Backend stub (Symfony controller for POST /api/inspections)
---
## Phase 3: PWA Scaffold Implementation
**Prompt used:** "Goal is still conversion later on to Symfony, but for now we do want the PWA functionality (add an additional installation and instruction screen for the PWA installation)"
**Constraint flagged:** PWA features require HTTPS or localhost -- they cannot work from `file://`.
**Resolution:** Dual-mode codebase:
- `file://` mode: app works exactly as before; PWA init gated by `location.protocol !== 'file:'` and quietly skipped
- `http://localhost` or deployed mode: service worker registers, app becomes installable, install screen shows platform-specific instructions
**Files created:**
- `public/manifest.webmanifest` -- app manifest with icons, standalone display, ProRail blue theme
- `public/sw.js` -- service worker with app shell precaching (cache-first for statics, network-first for API)
- `public/icons/icon.svg` -- SVG app icon (ProRail blue with white ruler lines)
- `public/css/install.css` -- styles for the install instruction screen
- `src/Application/pwa.js` -- service worker registration, `beforeinstallprompt` capture, standalone detection
- `src/Application/installScreen.js` -- platform-specific install instructions (Chrome/Edge, iOS Safari, Firefox, generic)
**HTML changes:**
- Added PWA meta tags (`theme-color`, `apple-mobile-web-app-capable`, manifest link, apple-touch-icon)
- Added "Installeren" button to overview toolbar
- Added `screen-install` section with back button and dynamic body
- Added `<link>` for `install.css`
- Added `<script>` tags for `pwa.js` and `installScreen.js`
---
## Phase 4: Git Repository Setup
**Prompt used:** "ini git repo and commit and push, add files and folders, repo https://git.en-masse.nl/randy/duimstok.prorail.nl.git" + "to the develop branch"
**Decisions made:**
- Repo root: `01_Applicatie/` (app code only; ProRail reference PDFs/photos stay outside)
- Deleted old monolithic files: `duimstok-wissel-gw-sw.html` and backup copy
- Created `.gitignore` (DS_Store, Thumbs.db, editors, node_modules, env files)
- Created `README.md` with project structure documentation
- Created `_docs/.gitkeep`
**Git Flow applied:**
1. Initialized repo with `develop` branch -- baseline commit (README, .gitignore, _docs/)
2. Created `feature/initial-pwa-scaffold` branch off develop
3. Committed all app code (30 files, 2163 insertions)
4. Pushed `develop` (baseline only, no staging deploy triggered)
5. Pushed `feature/initial-pwa-scaffold` (feature branch, no deploy implications)
6. Suggested PR creation on Forgejo before merging to develop
---
## Phase 5: Photo Blob Storage Refactor
**Branch:** `feature/photo-blob-storage` (off `feature/initial-pwa-scaffold`)
**What changed:**
- Photos now stored as native `Blob` objects instead of base64 data URLs (~33% smaller)
- `photoSrc()` helper uses `URL.createObjectURL(blob)` for rendering thumbnails
- `URL.revokeObjectURL()` called on lightbox close and when replacing images (memory cleanup)
- Removed `readFileAsDataUrl()` from `utils.js` (no longer needed)
- Removed `dataUrlToBlob()` from `utils.js` (no longer needed)
- `db.js`: removed `JSON.parse(JSON.stringify(formData))` deep-clone -- IndexedDB handles Blobs natively
- `exportService.js`: downloads `p.blob` directly instead of converting from data URL
**Files changed:** `photoService.js`, `exportService.js`, `db.js`, `utils.js`
---
## Phase 6: CSV Upload Persistence
**Branch:** `feature/csv-upload-persistence`
**What changed:**
- New IndexedDB object store `ordersCsv` (DB version bumped 3 -> 4)
- `db.js`: added `saveOrdersCsv(csvText)` and `loadOrdersCsv()` functions
- `csvLoader.js`: `loadOrdersFromUpload()` now returns `{ text, orders }` so raw CSV text gets persisted
- `main.js` init: loads stored CSV from IndexedDB, falls back to embedded seed data if none stored
- `main.js` upload handler: saves CSV text to IndexedDB after parsing
**Files changed:** `db.js`, `csvLoader.js`, `main.js`
---
## Phase 7: Persistent Storage Request
**Branch:** `feature/persistent-storage-request`
**What changed:**
- New file `src/Infrastructure/persistentStorage.js` with `isPersisted()` and `requestPersistence()`
- Calls `navigator.storage.persist()` after the first successful save (reduces iOS eviction risk)
- One-time flag `persistenceRequested` prevents repeated calls
- Updated SW cache version to `duimstok-v2`
- Added `persistentStorage.js` to both `index.html` script tags and SW precache list
**Files changed/created:** `persistentStorage.js` (new), `persistence.js`, `sw.js`, `index.html`
---
## Phase 8: Multi-Inspection Type Scoping (Not Yet Implemented)
**Prompt used:** "continue with question 1: spoor en overweg in map C:\...\04_Gegenereerd door SAP_incl WW_xslx_xlm\XML"
**Research done:**
- Read `_docs/switch-inspection-diagram.md` -- confirms existing wissel_GW photo layout
- Read SAP XML templates for Spoor and Overwegbevloering from reference folders
- Identified 3 inspection types: Wissel, Overwegbevloering, Spoor
**Proposed scope for `feature/inspection-type-routing`:**
1. Routing layer that derives type from `order.objectsoort`
2. Three form panels in `index.html`, shown based on type
3. Wissel: no visual change (existing 10-slot layout)
4. Overwegbevloering: stub form with photos section, no wissel diagram
5. Spoor: stub form emphasizing start/end-KM fields from CSV
6. Export works for all three types
**Status:** Scoped, awaiting answers to 3 questions before implementation.
---
## Global Rules Established
These rules were added to `~/.claude/CLAUDE.md` during the session:
| Rule | Context |
|------|---------|
| **No backwards compatibility in MVPs** | User corrected fallback code for old photo data format |
| **No `Co-Authored-By: Claude` in commits** | User interrupted a commit that included the attribution line |
| **Git Flow workflow** | User requested: develop = staging, master = production, feature branches for all work |
| **`_docs/` for project documentation** | User requested: all docs in project root `_docs/` folder |
| **`file://` runnable MVPs** | User requested: must work by opening `public/index.html` directly |
| **Vanilla HTML/JS/CSS, no frameworks** | Pre-existing rule (maps to eventual Symfony/Twig conversion) |
| **DDD folder structure** | Pre-existing rule (Domain/Application/Infrastructure layers) |
| **Forgejo at git.en-masse.nl** | User specified as default remote host |
---
## Shortcuts & Commands Used
| Command/Action | Purpose | Times Used |
|----------------|---------|------------|
| `/usage` | Check token consumption | 2x |
| `continue` | Resume after idle or interruption | Multiple |
| `merge` | Trigger merge-to-develop flow | Multiple |
| `yes` / `1` / `3` | Quick selection from numbered options | Multiple |
| Interrupted commits | Added global rules mid-flow (no co-author, no backwards compat) | 3x |
---
## Git History (Linear on develop)
```
b005193 Request persistent storage after first save
92ad6a9 Persist uploaded order CSVs across app restarts
e86aa5b Store photos as native Blobs instead of base64 data URLs
776bd63 Add PWA-ready MVP: split HTML/CSS/JS + DDD layering + service worker
367bbca Initial repo scaffold on develop
```
**Branch lifecycle:**
- All feature branches created off `develop` (or stacked off previous feature branch when develop was behind)
- All merged via fast-forward (`--ff-only`)
- All deleted (local + remote) after merge
- Only `develop` remains as active branch
- `master` not yet created (awaiting production readiness)
---
## Open Items
- [ ] Manual verify: capture photo, render thumbnail, reload, check thumbnail persists, export produces valid JPG
- [ ] Manual verify: upload CSV, hard-reload, confirm modified orders still appear
- [ ] Implement `feature/inspection-type-routing` (Wissel/Overwegbevloering/Spoor forms)
- [ ] Create `master` branch when ready for production deployment
- [ ] Set up CI on Forgejo
- [ ] Backend stub (Symfony POST /api/inspections) to enable sync queue testing
- [ ] Authentication for inspectors (currently free-text name -- needs SSO/token discussion with ProRail)

View File

@ -0,0 +1,41 @@
# Switch Inspection Diagram (wissel_GW)
## Source
`06_Figuren_plaatjes/PDF/Plaatje wissel_GW.pdf`
A reference diagram used by the Duimstok inspection app to guide the user through photographing a railway switch (Dutch: *wissel*). The `GW` suffix refers to the switch type.
## Purpose
The diagram acts as an **interactive photo-capture template**. During an inspection, the user is expected to supply a photograph for each labeled camera position. The blue underlined labels in the PDF are hyperlinks — in the app they map to photo-upload slots.
## Layout
Top-down schematic of a single switch, split lengthwise into three color-coded zones:
| Zone | Color | Dutch label | Description |
|------|-------|-------------|-------------|
| 1 | Green | Puntstukgedeelte | Frog / crossing section — where the two rails converge |
| 2 | Turquoise | Middengedeelte | Middle section — closure rails between frog and points |
| 3 | Red | Tongbeweging | Switch-blade / point-motor section — contains the actuator mechanism |
## Camera positions
Eight eye/camera icons surround the switch, each tied to a photo slot:
- **Longitudinal (4×)** — "Overzichtsfoto" (overview photo) at the top-left, top-right, bottom-left, and bottom-right, taken along the track direction.
- **Transverse (6×)** — "Foto" (detail photo) on the left and right of each of the three zones, taken perpendicular to the track.
Total: **10 photo slots per switch inspection** (4 overviews + 6 details).
## Usage in the app
- Render the diagram as the visual index on the switch-inspection screen.
- Each hyperlink label corresponds to a photo slot in the inspection record.
- Slot identifiers should encode both **position** (top/bottom/left/right) and **zone** (frog/middle/blade) so the stored photos can be reassembled into the correct diagram positions in the report.
## Notes
- The diagram is language-dependent (Dutch labels). Any translation must preserve the zone semantics, not just the words.
- The actuator (two cylindrical elements at the bottom of the red zone) is drawn in place — detail photos of the *Tongbeweging* zone should capture this mechanism.

View File

@ -78,6 +78,78 @@
.header-grid .label { background: var(--header-bg); font-weight: 600; color: #333; }
.header-grid .value { background: #fff; color: #000; }
/* Type-based visibility: hide type-specific sections by default */
.header-grid.wissel-only,
.header-grid.overweg-only,
.header-grid.spoor-only { display: none; }
body.type-wissel .header-grid.wissel-only,
body.type-engelsman .header-grid.wissel-only,
body.type-kruising .header-grid.wissel-only { display: grid; }
body.type-overweg .header-grid.overweg-only { display: grid; }
body.type-spoor .header-grid.spoor-only { display: grid; }
.wissel-overview,
.engelsman-overview,
.kruising-overview { display: none; }
body.type-wissel .wissel-overview { display: grid; }
body.type-engelsman .engelsman-overview { display: grid; }
body.type-kruising .kruising-overview { display: grid; }
.overweg-overview { display: none; }
body.type-overweg .overweg-overview { display: block; }
.spoor-overview { display: none; }
body.type-spoor .spoor-overview { display: block; }
.instruction-box.wissel-only,
.instruction-box.overweg-only,
.instruction-box.spoor-only,
.instruction-box.engelsman-only,
.instruction-box.kruising-only { display: none; }
body.type-wissel .instruction-box.wissel-only { display: block; }
body.type-overweg .instruction-box.overweg-only { display: block; }
body.type-spoor .instruction-box.spoor-only { display: block; }
body.type-engelsman .instruction-box.engelsman-only { display: block; }
body.type-kruising .instruction-box.kruising-only { display: block; }
.svg-link-row { display: none; }
body.type-wissel .svg-link-row { display: block; }
/* Overwegbevloering spatial diagram */
.ow-special-row {
display: flex; justify-content: center; gap: 24px; padding: 12px 0;
border-bottom: 1px solid #ddd; margin-bottom: 8px;
}
.ow-diagram {
display: grid; grid-template-columns: 100px 1fr 100px;
gap: 0; max-width: 700px; margin: 0 auto; min-height: 320px;
}
.ow-side {
display: flex; flex-direction: column; align-items: center;
justify-content: space-around; padding: 4px 0;
}
.ow-km-label {
font-size: 10px; font-weight: 700; color: var(--prorail-blue);
writing-mode: vertical-rl; text-orientation: mixed;
letter-spacing: 0.5px; margin-bottom: 8px;
}
.ow-center {
position: relative; display: flex; align-items: center; justify-content: center;
}
.ow-crossing-svg { width: 100%; height: 100%; max-height: 360px; }
.ow-weg-label {
position: absolute; font-size: 11px; font-weight: 700; color: #666;
left: 50%; transform: translateX(-50%);
}
.ow-weg-top { top: 4px; }
.ow-weg-bottom { bottom: 4px; }
/* Generic photo grid for spoor overview */
.ov-photo-grid {
display: flex; justify-content: center; gap: 24px; padding: 16px 0;
}
.ov-photo-grid .capture-label { padding: 12px 8px; }
.task-section {
display: grid; grid-template-columns: 180px 1fr;
border-bottom: 1px solid var(--border);
@ -207,9 +279,21 @@
position: relative; border: 1px solid #aaa; display: flex; flex-direction: column;
justify-content: center; align-items: center; min-height: 140px; overflow: hidden;
}
.section-strip.puntstuk { background: var(--section-punt); border-radius: 4px 4px 0 0; }
.section-strip.puntstuk { background: var(--section-punt); }
.section-strip.midden { background: var(--section-midden); }
.section-strip.tong { background: var(--section-tong); border-radius: 0 0 4px 4px; }
.section-strip.tong { background: var(--section-tong); }
.section-strip.kruisstuk { background: #80deea; }
.eng-nr-label {
font-size: 11px; font-weight: 700; color: var(--prorail-blue);
text-align: center; align-self: center;
}
.engelsman-overview, .kruising-overview {
grid-template-columns: 110px 1fr 110px;
grid-template-rows: auto;
gap: 0; max-width: 700px; margin: 0 auto 16px auto; align-items: stretch;
}
.section-strip .section-name {
font-size: 13px; font-weight: 700; color: var(--prorail-blue);
text-align: center; z-index: 1; text-shadow: 0 0 4px rgba(255,255,255,0.8);

View File

@ -49,6 +49,8 @@
<div class="filter-btns">
<button class="filter-btn active" data-filter="alle">Alle</button>
<button class="filter-btn" data-filter="Wissel">Wissels</button>
<button class="filter-btn" data-filter="Engelsman">Engelsman</button>
<button class="filter-btn" data-filter="Kruising">Kruising</button>
<button class="filter-btn" data-filter="Overwegbevloering">Overwegen</button>
<button class="filter-btn" data-filter="Spoor">Spoor</button>
<button class="filter-btn" data-filter="open">Nog open</button>
@ -125,32 +127,42 @@
<div class="tab-panel active" id="panel-inspectie">
<div class="form-container" id="formContainer">
<div class="form-title">Duimstokformulier Gewoon of Symmetrisch wissel</div>
<div class="form-title" id="formTitle">Duimstokformulier</div>
<div class="header-grid">
<div class="cell label">Geocode</div><div class="cell value" id="hdr_geo"></div>
<div class="cell label">Generatie</div><div class="cell value" id="hdr_generatie"></div>
<div class="cell label">Geocode omschrijving</div><div class="cell value" id="hdr_geotxt"></div>
<div class="cell label">Profiel</div><div class="cell value" id="hdr_profiel"></div>
<div class="cell label">Equipmentnummer</div><div class="cell value" id="hdr_equnr"></div>
<div class="cell label">Wisselligger soort</div><div class="cell value" id="hdr_dwarsligger"></div>
<div class="cell label">Wisselnummer</div><div class="cell value" id="hdr_wisselnr"></div>
<div class="cell label">Achterkant afwijking</div><div class="cell value" id="hdr_afwijking"></div>
<div class="cell label">Objectsoort</div><div class="cell value" id="hdr_eqart"></div>
<div class="cell label">Hergebruikt object?</div><div class="cell value" id="hdr_hergebruikt"></div>
<div class="cell label">Wisselsoort</div><div class="cell value" id="hdr_soort"></div>
<div class="cell label">Aangesloten wisselverwarming</div><div class="cell value" id="hdr_wisselverw"></div>
<div class="cell label">Startpunt</div><div class="cell value" id="hdr_startpoint"></div>
<div class="cell label">Hoofdspoor/Zijspoor</div><div class="cell value" id="hdr_spoor"></div>
<div class="cell label">Eindpunt</div><div class="cell value" id="hdr_endpoint"></div>
<div class="cell label">Omschrijving</div><div class="cell value" id="hdr_omschrijving"></div>
<div class="cell label">Order-operatie</div><div class="cell value" id="hdr_orderoperatie"></div>
<div class="cell label">Afschrijvingsgroep</div><div class="cell value" id="hdr_afschr"></div>
<div class="cell label">Plaatsingsdatum</div><div class="cell value" id="hdr_plaatsingsdatum"></div>
<div class="cell label">Hoofdspoor/Zijspoor</div><div class="cell value" id="hdr_spoor"></div>
</div>
<div class="header-grid wissel-only">
<div class="cell label">Wisselnummer</div><div class="cell value" id="hdr_wisselnr"></div>
<div class="cell label">Wisselsoort</div><div class="cell value" id="hdr_soort"></div>
<div class="cell label">Hoekverhouding</div><div class="cell value" id="hdr_hoekverhouding"></div>
<div class="cell label">Profiel</div><div class="cell value" id="hdr_profiel"></div>
<div class="cell label">Wisselligger soort</div><div class="cell value" id="hdr_dwarsligger"></div>
<div class="cell label">Achterkant afwijking</div><div class="cell value" id="hdr_afwijking"></div>
<div class="cell label">Hergebruikt object?</div><div class="cell value" id="hdr_hergebruikt"></div>
<div class="cell label">Aangesloten wisselverwarming</div><div class="cell value" id="hdr_wisselverw"></div>
<div class="cell label">Generatie</div><div class="cell value" id="hdr_generatie"></div>
<div class="cell label">Mee-/tegengebogen wissel</div><div class="cell value" id="hdr_meetegengebogen"></div>
<div class="cell label">Wissel classificatie</div><div class="cell value" id="hdr_classificatie"></div>
<div class="cell label">Voegloos</div><div class="cell value" id="hdr_voegloos"></div>
<div class="cell label">Plaatsingsdatum</div><div class="cell value" id="hdr_plaatsingsdatum"></div>
<div class="cell label">Hoekverhouding</div><div class="cell value" id="hdr_hoekverhouding"></div>
<div class="cell label">Aanschafdatum</div><div class="cell value" id="hdr_aanschafdatum"></div>
<div class="cell label">Omschrijving</div><div class="cell value" id="hdr_omschrijving"></div>
<div class="cell label">Order-operatie</div><div class="cell value" id="hdr_orderoperatie"></div>
</div>
<div class="header-grid overweg-only">
<div class="cell label">Type bevloering</div><div class="cell value" id="hdr_overwegtype"></div>
<div class="cell label">Bovenliggende overweg</div><div class="cell value" id="hdr_overwegbasis"></div>
</div>
<div class="header-grid spoor-only">
<div class="cell label">Type spoor</div><div class="cell value" id="hdr_spoortype"></div>
<div class="cell label">BVS Code</div><div class="cell value" id="hdr_bvscode"></div>
</div>
<div class="task-section">
<div class="label">Taak omschrijving</div><div class="value" id="hdr_taakomschrijving"></div>
@ -176,7 +188,7 @@
<div class="cell label">Fotonummers</div><div class="cell fotonummers-cell" id="totaal_fotonummers"></div>
</div>
<table class="assessment-table" id="assessmentTable">
<thead><tr>
<thead id="assessmentHead"><tr>
<th class="nr-col">Nr</th><th class="loc-col">Locatie in wissel</th>
<th class="score-col">DWL Score</th><th class="score-col">BAL Score</th>
<th class="foto-col">Foto's</th><th class="opm-col">Opmerkingen / bijzonderheden</th>
@ -198,12 +210,36 @@
<div class="tab-panel" id="panel-overzicht">
<div class="overview-container">
<div class="overview-title" id="overzichtTitle">Gewoon of Symmetrisch wissel</div>
<div class="instruction-box">
<div class="instruction-box wissel-only">
<strong>Instructie:</strong> Eerste foto bevat het bordje met het wisselnummer.
Daarna dienen de foto's buitenom of rechtsom in volgorde te worden gemaakt.
Foto's met bijzonderheden dienen in het opmerkingen-veld begeleid te worden
in combinatie met het fotonummer.
</div>
<div class="instruction-box overweg-only">
<strong>Instructie:</strong> Maak overzichtsfoto's vanuit beide richtingen.
Maak per beoordelingslocatie minimaal een foto van de situatie.
Foto's met bijzonderheden dienen in het opmerkingen-veld begeleid te worden
in combinatie met het fotonummer.
</div>
<div class="instruction-box engelsman-only">
<strong>Instructie:</strong> Eerste foto bevat het bordje met het wisselnummer.
Daarna dienen de foto's buitenom of rechtsom in volgorde te worden gemaakt.
Foto's met bijzonderheden dienen in het opmerkingen-veld begeleid te worden
in combinatie met het fotonummer.
</div>
<div class="instruction-box kruising-only">
<strong>Instructie:</strong> Eerste foto bevat het bordje met het wisselnummer.
Daarna dienen de foto's links of rechtsom in volgorde te worden gemaakt.
Foto's met bijzonderheden dienen in het opmerkingen-veld begeleid te worden
in combinatie met het fotonummer.
</div>
<div class="instruction-box spoor-only">
<strong>Instructie:</strong> Maak overzichtsfoto's vanuit beide richtingen van het spoorvak.
Maak per beoordeeld segment minimaal een foto via de beoordelingstabel.
Foto's met bijzonderheden dienen in het opmerkingen-veld begeleid te worden
in combinatie met het fotonummer.
</div>
<div class="wissel-overview">
<div class="ovz-row">
<label class="capture-label" data-pos="ovz_tl">
@ -253,6 +289,121 @@
</label>
</div>
</div>
<div class="engelsman-overview">
<div class="ovz-row">
<label class="capture-label" data-pos="ovz_eng_tl"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_eng_tl"></span></div><span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">&#8600;</span><input type="file" accept="image/*" capture="environment" data-pos="ovz_eng_tl"></label>
<div class="flex-spacer"></div>
<span class="eng-nr-label">Hoogste wisselnummer</span>
<div class="flex-spacer"></div>
<label class="capture-label" data-pos="ovz_eng_tr"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_eng_tr"></span></div><span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">&#8601;</span><input type="file" accept="image/*" capture="environment" data-pos="ovz_eng_tr"></label>
</div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_eng_s5_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s5_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s5_l"></label></div>
<div class="section-strip puntstuk"><div class="section-name">Puntstuk</div><div class="section-sub">gedeelte</div><div class="section-sub sm">Sectie 5</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_eng_s5_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s5_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s5_r"></label></div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_eng_s4_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s4_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s4_l"></label></div>
<div class="section-strip kruisstuk"><div class="section-name">Kruisstuk</div><div class="section-sub">gedeelte</div><div class="section-sub sm">Sectie 4</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_eng_s4_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s4_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s4_r"></label></div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_eng_s3_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s3_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s3_l"></label></div>
<div class="section-strip midden"><div class="section-name">Midden-</div><div class="section-name">gedeelte</div><div class="section-sub sm">Sectie 3</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_eng_s3_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s3_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s3_r"></label></div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_eng_s2_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s2_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s2_l"></label></div>
<div class="section-strip tong"><div class="section-name">Tong-</div><div class="section-name">beweging</div><div class="section-sub sm">Sectie 2</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_eng_s2_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s2_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s2_r"></label></div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_eng_s1_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s1_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s1_l"></label></div>
<div class="section-strip puntstuk"><div class="section-name">Puntstuk</div><div class="section-sub">gedeelte</div><div class="section-sub sm">Sectie 1</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_eng_s1_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_eng_s1_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_eng_s1_r"></label></div>
<div class="ovz-row">
<label class="capture-label" data-pos="ovz_eng_bl"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_eng_bl"></span></div><span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">&#8599;</span><input type="file" accept="image/*" capture="environment" data-pos="ovz_eng_bl"></label>
<div class="flex-spacer"></div>
<span class="eng-nr-label">Laagste wisselnummer</span>
<div class="flex-spacer"></div>
<label class="capture-label" data-pos="ovz_eng_br"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_eng_br"></span></div><span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">&#8598;</span><input type="file" accept="image/*" capture="environment" data-pos="ovz_eng_br"></label>
</div>
</div>
<div class="kruising-overview">
<div class="ovz-row">
<label class="capture-label" data-pos="ovz_kr_tl"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_kr_tl"></span></div><span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">&#8600;</span><input type="file" accept="image/*" capture="environment" data-pos="ovz_kr_tl"></label>
<div class="flex-spacer"></div>
<span class="eng-nr-label">Laagste wisselnummer</span>
<div class="flex-spacer"></div>
<label class="capture-label" data-pos="ovz_kr_tr"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_kr_tr"></span></div><span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">&#8601;</span><input type="file" accept="image/*" capture="environment" data-pos="ovz_kr_tr"></label>
</div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_kr_s4_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_kr_s4_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_kr_s4_l"></label></div>
<div class="section-strip puntstuk"><div class="section-name">Puntstuk</div><div class="section-sub">gedeelte</div><div class="section-sub sm">Sectie 4</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_kr_s4_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_kr_s4_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_kr_s4_r"></label></div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_kr_s3_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_kr_s3_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_kr_s3_l"></label></div>
<div class="section-strip kruisstuk"><div class="section-name">Kruisstuk</div><div class="section-sub">gedeelte</div><div class="section-sub sm">Sectie 3</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_kr_s3_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_kr_s3_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_kr_s3_r"></label></div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_kr_s2_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_kr_s2_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_kr_s2_l"></label></div>
<div class="section-strip midden"><div class="section-name">Midden-</div><div class="section-name">gedeelte</div><div class="section-sub sm">Sectie 2</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_kr_s2_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_kr_s2_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_kr_s2_r"></label></div>
<div class="side-labels left"><label class="capture-label" data-pos="foto_kr_s1_l"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_kr_s1_l"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_kr_s1_l"></label></div>
<div class="section-strip puntstuk"><div class="section-name">Puntstuk</div><div class="section-sub">gedeelte</div><div class="section-sub sm">Sectie 1</div></div>
<div class="side-labels right"><label class="capture-label" data-pos="foto_kr_s1_r"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_kr_s1_r"></span></div><span class="cap-text">Foto</span><span class="cap-arrow"></span><input type="file" accept="image/*" capture="environment" data-pos="foto_kr_s1_r"></label></div>
<div class="ovz-row">
<label class="capture-label" data-pos="ovz_kr_bl"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_kr_bl"></span></div><span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">&#8599;</span><input type="file" accept="image/*" capture="environment" data-pos="ovz_kr_bl"></label>
<div class="flex-spacer"></div>
<span class="eng-nr-label">Laagste wisselnummer + kruisnummerbord</span>
<div class="flex-spacer"></div>
<label class="capture-label" data-pos="ovz_kr_br"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_kr_br"></span></div><span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">&#8598;</span><input type="file" accept="image/*" capture="environment" data-pos="ovz_kr_br"></label>
</div>
</div>
<div class="overweg-overview">
<div class="ow-special-row">
<label class="capture-label" data-pos="foto_ow_sticker"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_sticker"></span></div><span class="cap-text">Foto 1: Sticker<br>overwegpaal</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_sticker"></label>
<label class="capture-label" data-pos="foto_ow_spoorbord"><div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_spoorbord"></span></div><span class="cap-text">Foto 2: Spoor-<br>naambordje</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_spoorbord"></label>
</div>
<div class="ow-diagram">
<div class="ow-side">
<span class="ow-km-label">Lage kilometrering</span>
<label class="capture-label" data-pos="foto_ow_lk_1"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_lk_1"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_lk_1"></label>
<label class="capture-label" data-pos="foto_ow_lk_2"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_lk_2"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_lk_2"></label>
<label class="capture-label" data-pos="foto_ow_lk_3"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_lk_3"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_lk_3"></label>
<label class="capture-label" data-pos="foto_ow_lk_4"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_lk_4"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_lk_4"></label>
<label class="capture-label" data-pos="foto_ow_lk_5"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_lk_5"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_lk_5"></label>
<label class="capture-label" data-pos="foto_ow_lk_6"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_lk_6"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_lk_6"></label>
</div>
<div class="ow-center">
<svg class="ow-crossing-svg" viewBox="0 0 200 400" preserveAspectRatio="xMidYMid meet">
<line x1="0" y1="180" x2="200" y2="180" stroke="#555" stroke-width="3"/>
<line x1="0" y1="220" x2="200" y2="220" stroke="#555" stroke-width="3"/>
<line x1="0" y1="160" x2="200" y2="160" stroke="#8B7355" stroke-width="2" opacity="0.4"/>
<line x1="0" y1="170" x2="200" y2="170" stroke="#8B7355" stroke-width="2" opacity="0.4"/>
<line x1="0" y1="230" x2="200" y2="230" stroke="#8B7355" stroke-width="2" opacity="0.4"/>
<line x1="0" y1="240" x2="200" y2="240" stroke="#8B7355" stroke-width="2" opacity="0.4"/>
<line x1="90" y1="0" x2="90" y2="400" stroke="#999" stroke-width="1.5" stroke-dasharray="6,4"/>
<line x1="110" y1="0" x2="110" y2="400" stroke="#999" stroke-width="1.5" stroke-dasharray="6,4"/>
<rect x="70" y="160" width="60" height="80" rx="4" fill="rgba(100,180,255,0.3)" stroke="#4a9fd9" stroke-width="1.5"/>
<text x="100" y="204" text-anchor="middle" font-size="11" fill="#555" font-weight="600">Overweg</text>
</svg>
<span class="ow-weg-label ow-weg-top">Weg</span>
<span class="ow-weg-label ow-weg-bottom">Weg</span>
</div>
<div class="ow-side">
<span class="ow-km-label">Hoge kilometrering</span>
<label class="capture-label" data-pos="foto_ow_hk_1"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_hk_1"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_hk_1"></label>
<label class="capture-label" data-pos="foto_ow_hk_2"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_hk_2"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_hk_2"></label>
<label class="capture-label" data-pos="foto_ow_hk_3"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_hk_3"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_hk_3"></label>
<label class="capture-label" data-pos="foto_ow_hk_4"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_hk_4"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_hk_4"></label>
<label class="capture-label" data-pos="foto_ow_hk_5"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_hk_5"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_hk_5"></label>
<label class="capture-label" data-pos="foto_ow_hk_6"><div class="cap-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_foto_ow_hk_6"></span></div><span class="cap-text">Foto</span><input type="file" accept="image/*" capture="environment" data-pos="foto_ow_hk_6"></label>
</div>
</div>
</div>
<div class="spoor-overview">
<div class="ov-photo-grid">
<label class="capture-label" data-pos="ovz_sp_1">
<div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_sp_1"></span></div>
<span class="cap-text">Overzichtsfoto begin</span>
<input type="file" accept="image/*" capture="environment" data-pos="ovz_sp_1">
</label>
<label class="capture-label" data-pos="ovz_sp_2">
<div class="cap-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span class="cap-badge" id="cbadge_ovz_sp_2"></span></div>
<span class="cap-text">Overzichtsfoto einde</span>
<input type="file" accept="image/*" capture="environment" data-pos="ovz_sp_2">
</label>
</div>
</div>
<div class="overview-thumbs" id="overviewThumbs">
<h4>Vastgelegde foto's</h4>
<div class="thumb-grid" id="overviewThumbGrid">
@ -299,6 +450,7 @@
<script src="js/namespace.js"></script>
<script src="../src/Domain/sectionMap.js"></script>
<script src="../src/Domain/scoring.js"></script>
<script src="../src/Domain/inspectionTypes.js"></script>
<script src="../src/Domain/orderParser.js"></script>
<script src="../src/Infrastructure/utils.js"></script>
<script src="../src/Infrastructure/geolocation.js"></script>

View File

@ -1,4 +1,4 @@
const CACHE_VERSION = 'duimstok-v2';
const CACHE_VERSION = 'duimstok-v3';
const APP_SHELL = [
'./',
@ -15,6 +15,7 @@ const APP_SHELL = [
'./js/main.js',
'../src/Domain/sectionMap.js',
'../src/Domain/scoring.js',
'../src/Domain/inspectionTypes.js',
'../src/Domain/orderParser.js',
'../src/Infrastructure/utils.js',
'../src/Infrastructure/geolocation.js',

View File

@ -1,40 +1,57 @@
(function (A, I) {
(function (A, D, I) {
function exportScores(fd, x) {
var type = fd.type;
x.push('<BEOORDELINGEN>');
for (var [nr, data] of Object.entries(fd.scores)) {
var fNrs = (fd.photos[nr] || []).map(function (_, i) { return fd.orderNr + '_loc' + nr + '_foto' + (i + 1); }).join('; ');
if (D.usesDwlBal(type)) {
x.push('<WISSEL_SCORE><NR>' + nr + '</NR><DWL_SCORE>' + I.esc(data.dwl || '') + '</DWL_SCORE><BAL_SCORE>' + I.esc(data.bal || '') + '</BAL_SCORE><FOTO_NR>' + I.esc(fNrs) + '</FOTO_NR><OPMERKING>' + I.esc(data.opm || '') + '</OPMERKING></WISSEL_SCORE>');
} else if (type === D.TYPE_OVERWEG) {
x.push('<OVERWEG_SCORE><LOCATIE>' + I.esc(data.locatie || '') + '</LOCATIE><SCORE>' + I.esc(data.score || '') + '</SCORE></OVERWEG_SCORE>');
} else if (type === D.TYPE_SPOOR) {
x.push('<SPOOR_STUK><NR>' + nr + '</NR><KM_VAN>' + I.esc(data.kmVan || '') + '</KM_VAN><KM_TOT>' + I.esc(data.kmTot || '') + '</KM_TOT><LENGTE>' + I.esc(data.lengte || '') + '</LENGTE><TECH_JAAR>' + I.esc(data.techJaar || '') + '</TECH_JAAR><INSP_JAAR>' + I.esc(data.inspJaar || '') + '</INSP_JAAR><SCORE>' + I.esc(data.score || '') + '</SCORE><FOTO_NR>' + I.esc(fNrs) + '</FOTO_NR></SPOOR_STUK>');
}
}
x.push('</BEOORDELINGEN>');
}
A.exportFormData = async function () {
await A.saveCurrentForm();
const fd = A.state.formData;
const x = ['<?xml version="1.0" encoding="utf-8"?>', '<FORM>'];
x.push(`<AUFNR_VORNR>${I.esc(fd.orderNr)}</AUFNR_VORNR>`);
x.push(`<INSPECTEUR>${I.esc(document.getElementById('inp_inspecteur').value)}</INSPECTEUR>`);
x.push(`<INSP_DATUM>${I.esc(document.getElementById('inp_inspectiedatum').value)}</INSP_DATUM>`);
x.push(`<TECH_JAAR>${I.esc(document.getElementById('inp_techjaar').value)}</TECH_JAAR>`);
x.push(`<INSP_JAAR>${I.esc(document.getElementById('inp_inspjaar').value)}</INSP_JAAR>`);
x.push(`<OPMERKING>${I.esc(document.getElementById('inp_opmerkingen').value)}</OPMERKING>`);
x.push('<OVERZICHTFOTOS>');
for (const [pos, photos] of Object.entries(fd.overviewPhotos)) {
photos.forEach((_, i) => x.push(`<FOTO><POSITIE>${I.esc(pos)}</POSITIE><NAAM>${I.esc(fd.orderNr.replace('/', '-'))}_${pos}_foto${i + 1}</NAAM></FOTO>`));
var fd = A.state.formData;
var x = ['<?xml version="1.0" encoding="utf-8"?>', '<FORM>'];
x.push('<AUFNR_VORNR>' + I.esc(fd.orderNr) + '</AUFNR_VORNR>');
x.push('<INSPECTEUR>' + I.esc(document.getElementById('inp_inspecteur').value) + '</INSPECTEUR>');
x.push('<INSP_DATUM>' + I.esc(document.getElementById('inp_inspectiedatum').value) + '</INSP_DATUM>');
x.push('<TECH_JAAR>' + I.esc(document.getElementById('inp_techjaar').value) + '</TECH_JAAR>');
x.push('<INSP_JAAR>' + I.esc(document.getElementById('inp_inspjaar').value) + '</INSP_JAAR>');
x.push('<OPMERKING>' + I.esc(document.getElementById('inp_opmerkingen').value) + '</OPMERKING>');
var hasOvPhotos = Object.values(fd.overviewPhotos).some(function (p) { return p.length > 0; });
if (hasOvPhotos) {
x.push('<OVERZICHTFOTOS>');
for (var [pos, photos] of Object.entries(fd.overviewPhotos)) {
photos.forEach(function (_, i) {
x.push('<FOTO><POSITIE>' + I.esc(pos) + '</POSITIE><NAAM>' + I.esc(fd.orderNr.replace('/', '-')) + '_' + pos + '_foto' + (i + 1) + '</NAAM></FOTO>');
});
}
x.push('</OVERZICHTFOTOS>');
}
x.push('</OVERZICHTFOTOS>');
x.push('<BEOORDELINGEN>');
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(`<WISSEL_SCORE><NR>${nr}</NR><DWL_SCORE>${I.esc(data.dwl || '')}</DWL_SCORE><BAL_SCORE>${I.esc(data.bal || '')}</BAL_SCORE><FOTO_NR>${I.esc(fNrs)}</FOTO_NR><OPMERKING>${I.esc(data.opm || '')}</OPMERKING></WISSEL_SCORE>`);
}
x.push('</BEOORDELINGEN>', '</FORM>');
exportScores(fd, x);
x.push('</FORM>');
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) => {
fd.orderNr.replace('/', '-') + '_inspectie_' + new Date().toISOString().slice(0, 10) + '.xml');
var pc = 0;
for (var [nr, phs] of Object.entries(fd.photos)) {
phs.forEach(function (p, i) {
pc++;
I.downloadBlob(p.blob, `${fd.orderNr.replace('/', '-')}_loc${nr}_foto${i + 1}.jpg`);
I.downloadBlob(p.blob, fd.orderNr.replace('/', '-') + '_loc' + nr + '_foto' + (i + 1) + '.jpg');
});
}
for (const [pos, photos] of Object.entries(fd.overviewPhotos)) {
photos.forEach((p, i) => {
for (var [pos2, phs2] of Object.entries(fd.overviewPhotos)) {
phs2.forEach(function (p, i) {
pc++;
I.downloadBlob(p.blob, `${fd.orderNr.replace('/', '-')}_${pos}_foto${i + 1}.jpg`);
I.downloadBlob(p.blob, fd.orderNr.replace('/', '-') + '_' + pos2 + '_foto' + (i + 1) + '.jpg');
});
}
alert(`Opgeslagen!\n- 1 XML-bestand\n- ${pc} foto('s)`);
alert('Opgeslagen!\n- 1 XML-bestand\n- ' + pc + ' foto(\'s)');
};
})(window.App.Application, window.App.Infrastructure);
})(window.App.Application, window.App.Domain, window.App.Infrastructure);

View File

@ -1,4 +1,6 @@
(function (A, D, I) {
var CAMERA_SVG = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>';
A.switchTab = function (tabId) {
document.querySelectorAll('.tab-btn').forEach(btn =>
btn.classList.toggle('active', btn.dataset.tab === tabId));
@ -6,8 +8,33 @@
panel.classList.toggle('active', panel.id === 'panel-' + tabId));
};
A.applyFormType = function (type) {
A.state.formData.type = type;
var body = document.body;
body.classList.remove('type-wissel', 'type-overweg', 'type-spoor', 'type-engelsman', 'type-kruising');
body.classList.add('type-' + type);
var thead = document.getElementById('assessmentHead');
var titles = {};
titles[D.TYPE_WISSEL] = 'Duimstokformulier Gewoon of Symmetrisch wissel';
titles[D.TYPE_ENGELSMAN] = 'Duimstokformulier Half of Heel Engelsman';
titles[D.TYPE_KRUISING] = 'Duimstokformulier Kruising';
titles[D.TYPE_OVERWEG] = 'Duimstokformulier Overwegbevloering';
titles[D.TYPE_SPOOR] = 'Duimstokformulier Spoor';
if (D.usesDwlBal(type)) {
thead.innerHTML = '<tr><th class="nr-col">Nr</th><th class="loc-col">Locatie</th><th class="score-col">DWL Score</th><th class="score-col">BAL Score</th><th class="foto-col">Foto\'s</th><th class="opm-col">Opmerkingen</th></tr>';
} else if (type === D.TYPE_OVERWEG) {
thead.innerHTML = '<tr><th class="loc-col">Locatie</th><th class="score-col">Score</th><th class="foto-col">Foto\'s</th><th class="opm-col">Opmerkingen</th></tr>';
} else if (type === D.TYPE_SPOOR) {
thead.innerHTML = '<tr><th class="nr-col">Nr</th><th>KM van</th><th>KM tot</th><th>Lengte</th><th class="score-col">Score</th><th class="foto-col">Foto\'s</th><th class="opm-col">Opmerkingen</th></tr>';
}
document.getElementById('formTitle').textContent = titles[type] || 'Duimstokformulier';
};
A.resetForm = function () {
A.state.formData = A.emptyFormData();
document.body.classList.remove('type-wissel', 'type-overweg', 'type-spoor', 'type-engelsman', 'type-kruising');
document.querySelectorAll('.header-grid .value').forEach(el => el.textContent = '');
document.querySelectorAll('#hdr_taakomschrijving, #hdr_aanleiding').forEach(el => el.textContent = '');
document.getElementById('inp_inspecteur').value = '';
@ -16,7 +43,7 @@
document.getElementById('inp_inspjaar').value = '';
document.getElementById('inp_opmerkingen').value = '';
document.getElementById('assessmentBody').innerHTML = '';
const totaal = document.getElementById('totaalscore');
var totaal = document.getElementById('totaalscore');
totaal.textContent = '-';
totaal.className = 'cell score-cell';
document.getElementById('totaal_fotonummers').textContent = '';
@ -29,6 +56,8 @@
};
A.prefillFromOrder = function (order) {
var type = D.typeFromObjectsoort(order.objectsoort);
A.applyFormType(type);
A.state.formData.orderNr = order.orderKey;
document.getElementById('hdr_omschrijving').textContent = order.omschrijving;
document.getElementById('hdr_eqart').textContent = order.objectsoort;
@ -45,77 +74,168 @@
D.SCORE_VALUES.map(v => `<option value="${v}" ${selected === v ? 'selected' : ''}>${v}</option>`).join('');
}
A.buildAssessmentTable = function (scoreElements) {
const tbody = document.getElementById('assessmentBody');
function photoButton(nr, locatie) {
return `<button class="foto-btn" data-action="open-photo" data-nr="${nr}" data-loc="${locatie.replace(/"/g, '&quot;')}">${CAMERA_SVG}Foto's<span class="badge" id="badge_${nr}"></span></button>`;
}
function wireAssessmentEvents() {
var tbody = document.getElementById('assessmentBody');
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('input[data-field="techJaar"], input[data-field="inspJaar"]').forEach(inp =>
inp.addEventListener('input', () => onSpoorFieldChange(inp)));
tbody.querySelectorAll('button[data-action="open-photo"]').forEach(btn =>
btn.addEventListener('click', () => A.openPhotoModal(+btn.dataset.nr, btn.dataset.loc)));
}
A.buildWisselAssessment = function (scoreElements, type) {
var tbody = document.getElementById('assessmentBody');
tbody.innerHTML = '';
let currentSection = '';
var currentSection = '';
var assessType = type || A.state.formData.type;
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);
var nr = parseInt(el.querySelector('NR').textContent.trim());
var locatie = el.querySelector('LOCATIE').textContent.trim();
var dwlScore = el.querySelector('DWL_SCORE').textContent.trim();
var balScore = el.querySelector('BAL_SCORE').textContent.trim();
var section = D.getSectionForNr(nr, assessType);
if (section && section !== currentSection) {
currentSection = section;
const sr = document.createElement('tr');
var sr = document.createElement('tr');
sr.className = 'section-row';
sr.innerHTML = `<td colspan="6">${section}</td>`;
sr.innerHTML = '<td colspan="6">' + section + '</td>';
tbody.appendChild(sr);
}
const tr = document.createElement('tr');
var tr = document.createElement('tr');
tr.dataset.nr = nr;
tr.innerHTML = `
<td class="nr-col">${nr}</td>
<td class="loc-col">${locatie}</td>
<td class="score-col editable-cell"><select data-nr="${nr}" data-type="dwl">${buildScoreOptions(dwlScore)}</select></td>
<td class="score-col editable-cell"><select data-nr="${nr}" data-type="bal">${buildScoreOptions(balScore)}</select></td>
<td class="foto-col"><button class="foto-btn" data-action="open-photo" data-nr="${nr}" data-loc="${locatie.replace(/"/g, '&quot;')}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>Foto's<span class="badge" id="badge_${nr}"></span></button></td>
<td class="opm-col editable-cell"><input type="text" data-nr="${nr}" data-field="opm" placeholder=""></td>`;
tr.innerHTML =
'<td class="nr-col">' + nr + '</td>' +
'<td class="loc-col">' + locatie + '</td>' +
'<td class="score-col editable-cell"><select data-nr="' + nr + '" data-type="dwl">' + buildScoreOptions(dwlScore) + '</select></td>' +
'<td class="score-col editable-cell"><select data-nr="' + nr + '" data-type="bal">' + buildScoreOptions(balScore) + '</select></td>' +
'<td class="foto-col">' + photoButton(nr, locatie) + '</td>' +
'<td class="opm-col editable-cell"><input type="text" data-nr="' + nr + '" data-field="opm" placeholder=""></td>';
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"]');
var dwlSel = tr.querySelector('select[data-type="dwl"]');
if (dwlScore) dwlSel.className = D.scoreClass(dwlScore);
const balSel = tr.querySelector('select[data-type="bal"]');
var balSel = tr.querySelector('select[data-type="bal"]');
if (balScore) balSel.className = D.scoreClass(balScore);
});
wireAssessmentEvents();
};
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)));
A.buildOverwegAssessment = function (scoreElements) {
var tbody = document.getElementById('assessmentBody');
tbody.innerHTML = '';
scoreElements.forEach((el, idx) => {
var nr = idx + 1;
var locatie = el.querySelector('LOCATIE').textContent.trim();
var score = el.querySelector('SCORE').textContent.trim();
var tr = document.createElement('tr');
tr.dataset.nr = nr;
tr.innerHTML =
'<td class="loc-col">' + locatie + '</td>' +
'<td class="score-col editable-cell"><select data-nr="' + nr + '" data-type="score">' + buildScoreOptions(score) + '</select></td>' +
'<td class="foto-col">' + photoButton(nr, locatie) + '</td>' +
'<td class="opm-col editable-cell"><input type="text" data-nr="' + nr + '" data-field="opm" placeholder=""></td>';
tbody.appendChild(tr);
if (!A.state.formData.scores[nr]) A.state.formData.scores[nr] = { score: score, opm: '' };
if (!A.state.formData.photos[nr]) A.state.formData.photos[nr] = [];
var sel = tr.querySelector('select[data-type="score"]');
if (score) sel.className = D.scoreClass(score);
});
wireAssessmentEvents();
};
A.buildSpoorAssessment = function (scoreElements) {
var tbody = document.getElementById('assessmentBody');
tbody.innerHTML = '';
scoreElements.forEach(el => {
var nr = parseInt(el.querySelector('NR').textContent.trim());
var kmVan = el.querySelector('KM_VAN').textContent.trim();
var kmTot = el.querySelector('KM_TOT').textContent.trim();
var lengte = el.querySelector('LENGTE').textContent.trim();
var score = el.querySelector('SCORE').textContent.trim();
var techJaar = el.querySelector('TECH_JAAR') ? el.querySelector('TECH_JAAR').textContent.trim() : '';
var inspJaar = el.querySelector('INSP_JAAR') ? el.querySelector('INSP_JAAR').textContent.trim() : '';
var tr = document.createElement('tr');
tr.dataset.nr = nr;
tr.innerHTML =
'<td class="nr-col">' + nr + '</td>' +
'<td class="editable-cell">' + kmVan + '</td>' +
'<td class="editable-cell">' + kmTot + '</td>' +
'<td class="editable-cell">' + lengte + ' m</td>' +
'<td class="score-col editable-cell"><select data-nr="' + nr + '" data-type="score">' + buildScoreOptions(score) + '</select></td>' +
'<td class="foto-col">' + photoButton(nr, 'Segment ' + nr) + '</td>' +
'<td class="opm-col editable-cell"><input type="text" data-nr="' + nr + '" data-field="opm" placeholder=""></td>';
tbody.appendChild(tr);
if (!A.state.formData.scores[nr]) A.state.formData.scores[nr] = { score: score, kmVan: kmVan, kmTot: kmTot, lengte: lengte, techJaar: techJaar, inspJaar: inspJaar, opm: '' };
if (!A.state.formData.photos[nr]) A.state.formData.photos[nr] = [];
var sel = tr.querySelector('select[data-type="score"]');
if (score) sel.className = D.scoreClass(score);
});
wireAssessmentEvents();
};
A.buildAssessmentForType = function (type, scoreElements) {
if (D.usesDwlBal(type)) A.buildWisselAssessment(scoreElements, type);
else if (type === D.TYPE_OVERWEG) A.buildOverwegAssessment(scoreElements);
else if (type === D.TYPE_SPOOR) A.buildSpoorAssessment(scoreElements);
};
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;
var nr = parseInt(sel.dataset.nr);
var field = sel.dataset.type;
var val = sel.value;
if (!A.state.formData.scores[nr]) A.state.formData.scores[nr] = {};
A.state.formData.scores[nr][field] = 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: '' };
var nr = parseInt(inp.dataset.nr);
if (!A.state.formData.scores[nr]) A.state.formData.scores[nr] = {};
A.state.formData.scores[nr].opm = inp.value;
A.autoSave();
}
function onSpoorFieldChange(inp) {
var nr = parseInt(inp.dataset.nr);
var field = inp.dataset.field;
if (!A.state.formData.scores[nr]) A.state.formData.scores[nr] = {};
A.state.formData.scores[nr][field] = inp.value;
A.autoSave();
}
A.updateTotaalscore = function () {
const worst = D.computeWorstScore(A.state.formData.scores);
const el = document.getElementById('totaalscore');
var type = A.state.formData.type;
var worst;
if (D.usesDwlBal(type)) {
worst = D.computeWorstScore(A.state.formData.scores);
} else {
var order = ['', ...D.SCORE_VALUES];
var wIdx = 0; var wScore = '';
for (var data of Object.values(A.state.formData.scores)) {
var s = data.score || '';
var m = order.indexOf(s);
if (m > wIdx) { wIdx = m; wScore = order[m]; }
}
worst = wScore;
}
var 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);
var saved = await I.loadInspection(orderNr);
if (!saved) return;
if (saved.type) A.applyFormType(saved.type);
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;
@ -123,12 +243,14 @@
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"]`);
for (var [nr, data] of Object.entries(saved.scores)) {
var dS = document.querySelector('select[data-nr="' + nr + '"][data-type="dwl"]');
var bS = document.querySelector('select[data-nr="' + nr + '"][data-type="bal"]');
var sS = document.querySelector('select[data-nr="' + nr + '"][data-type="score"]');
var 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 (sS && data.score) { sS.value = data.score; sS.className = D.scoreClass(data.score); }
if (oI && data.opm) oI.value = data.opm;
}
A.updateTotaalscore();

View File

@ -13,7 +13,7 @@
function emptyFormData() {
return {
orderNr: '', inspecteur: '', inspectiedatum: '', techJaar: '', inspJaar: '',
orderNr: '', type: 'wissel', inspecteur: '', inspectiedatum: '', techJaar: '', inspJaar: '',
opmerkingen: '', scores: {}, photos: {}, overviewPhotos: {}
};
}

View File

@ -1,42 +1,67 @@
(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() : ''; };
(function (A, D, I) {
function setIfExists(id, value) {
var el = document.getElementById(id);
if (el && value) el.textContent = value;
}
const orderNr = getText('AUFNR_VORNR');
A.parseAndLoadXML = function (xmlText) {
var parser = new DOMParser();
var xml = parser.parseFromString(xmlText, 'text/xml');
var getText = function (tag) { var el = xml.querySelector(tag); return el ? el.textContent.trim() : ''; };
var eqart = getText('EQART');
var type = D.typeFromEqart(eqart);
if (type === D.TYPE_WISSEL) {
type = D.typeFromWisselSoort(getText('SOORT'));
}
A.applyFormType(type);
var 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');
setIfExists('hdr_geo', getText('GEO'));
setIfExists('hdr_geotxt', getText('GEOTXT'));
setIfExists('hdr_equnr', getText('EQUNR'));
setIfExists('hdr_eqart', eqart);
setIfExists('hdr_startpoint', getText('START_POINT'));
setIfExists('hdr_endpoint', getText('END_POINT'));
setIfExists('hdr_omschrijving', getText('EQKTX'));
setIfExists('hdr_orderoperatie', getText('AUFNR_VORNR'));
setIfExists('hdr_spoor', getText('SPOOR'));
setIfExists('hdr_afschr', getText('ZAFSCHR_GRP_CODE'));
setIfExists('hdr_plaatsingsdatum', I.formatDate(getText('DATUM_START')));
setIfExists('hdr_taakomschrijving', getText('LTXA1'));
setIfExists('hdr_aanleiding', getText('KURZTEXT'));
const eqktx = getText('EQKTX');
if (eqktx) document.getElementById('overzichtTitle').textContent = eqktx;
if (D.usesDwlBal(type)) {
setIfExists('hdr_wisselnr', getText('WISSELNR') || getText('EQFNR_EQUI'));
setIfExists('hdr_soort', getText('SOORT'));
setIfExists('hdr_hoekverhouding', getText('HOEKVERHOUDING'));
setIfExists('hdr_profiel', getText('PROFIEL'));
setIfExists('hdr_dwarsligger', getText('DWARSLIGGER') || '-');
setIfExists('hdr_afwijking', getText('AFWIJKING') || '-');
setIfExists('hdr_hergebruikt', '-');
setIfExists('hdr_wisselverw', '-');
setIfExists('hdr_meetegengebogen', '-');
setIfExists('hdr_classificatie', '-');
setIfExists('hdr_voegloos', '-');
setIfExists('hdr_aanschafdatum', '-');
setIfExists('hdr_generatie', '-');
}
if (type === D.TYPE_OVERWEG) {
setIfExists('hdr_overwegtype', getText('TYPE'));
setIfExists('hdr_overwegbasis', getText('EQUKTX_BASIS'));
}
if (type === D.TYPE_SPOOR) {
setIfExists('hdr_spoortype', getText('TYPE'));
setIfExists('hdr_bvscode', getText('BVS_CODE'));
}
var eqktx = getText('EQKTX');
if (eqktx) setIfExists('overzichtTitle', eqktx);
document.getElementById('formToolbarTitle').textContent = eqktx || orderNr;
if (getText('INSPECTEUR')) document.getElementById('inp_inspecteur').value = getText('INSPECTEUR');
@ -45,8 +70,11 @@
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'));
if (D.usesDwlBal(type)) A.buildWisselAssessment(xml.querySelectorAll('WISSEL_SCORE'), type);
else if (type === D.TYPE_OVERWEG) A.buildOverwegAssessment(xml.querySelectorAll('OVERWEG_SCORE'));
else if (type === D.TYPE_SPOOR) A.buildSpoorAssessment(xml.querySelectorAll('SPOOR_STUK'));
document.getElementById('statusOrder').textContent = 'Order: ' + orderNr + ' | ' + eqktx;
A.loadSavedData(orderNr);
};
})(window.App.Application, window.App.Infrastructure);
})(window.App.Application, window.App.Domain, window.App.Infrastructure);

View File

@ -0,0 +1,52 @@
(function (D) {
D.TYPE_WISSEL = 'wissel';
D.TYPE_OVERWEG = 'overweg';
D.TYPE_SPOOR = 'spoor';
D.TYPE_ENGELSMAN = 'engelsman';
D.TYPE_KRUISING = 'kruising';
D.EQART_TO_TYPE = {
'WISSEL': D.TYPE_WISSEL,
'OVERWBEVL': D.TYPE_OVERWEG,
'SPOORDWL': D.TYPE_SPOOR,
'ENGELSMAN': D.TYPE_ENGELSMAN,
'KRUISING': D.TYPE_KRUISING
};
D.OBJECTSOORT_TO_TYPE = {
'Wissel': D.TYPE_WISSEL,
'Overwegbevloering': D.TYPE_OVERWEG,
'Spoor': D.TYPE_SPOOR,
'Engelsman': D.TYPE_ENGELSMAN,
'Half Engelsman': D.TYPE_ENGELSMAN,
'Heel Engelsman': D.TYPE_ENGELSMAN,
'Kruising': D.TYPE_KRUISING,
'Ingesloten kruising': D.TYPE_KRUISING
};
D.typeFromEqart = function (eqart) {
return D.EQART_TO_TYPE[eqart] || D.TYPE_WISSEL;
};
D.typeFromObjectsoort = function (objectsoort) {
return D.OBJECTSOORT_TO_TYPE[objectsoort] || D.TYPE_WISSEL;
};
D.typeFromWisselSoort = function (soort) {
soort = (soort || '').toLowerCase();
if (soort.indexOf('engelsman') >= 0) return D.TYPE_ENGELSMAN;
if (soort.indexOf('kruising') >= 0) return D.TYPE_KRUISING;
return D.TYPE_WISSEL;
};
D.OVERWEG_LOCATIONS = [
'Ligging spoor',
'Ligging bevloering',
'Constructieve kwaliteit',
'Wegverharding'
];
D.usesDwlBal = function (type) {
return type === D.TYPE_WISSEL || type === D.TYPE_ENGELSMAN || type === D.TYPE_KRUISING;
};
})(window.App.Domain);

View File

@ -1,12 +1,34 @@
(function (D) {
D.sectionMap = {
D.wisselSectionMap = {
'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)) {
D.engelsmanSectionMap = {
'Sectie 1: Puntstuk gedeelte (boven)': [1, 2, 3, 4, 5],
'Sectie 2: Kruisstuk gedeelte': [6, 7, 8, 9, 10],
'Sectie 3: Middengedeelte': [11, 12, 13, 14, 15, 16],
'Sectie 4: Tongbeweging': [17, 18, 19, 20, 21, 22],
'Sectie 5: Puntstuk gedeelte (onder)': [23, 24, 25, 26, 27, 28, 29, 30]
};
D.kruisingSectionMap = {
'Sectie 1: Puntstuk gedeelte (boven)': [1, 2, 3, 4, 5, 6],
'Sectie 2: Kruisstuk gedeelte': [7, 8, 9, 10, 11, 12],
'Sectie 3: Middengedeelte': [13, 14, 15, 16, 17, 18],
'Sectie 4: Puntstuk gedeelte (onder)': [19, 20, 21, 22, 23, 24]
};
D.sectionMapForType = function (type) {
if (type === 'engelsman') return D.engelsmanSectionMap;
if (type === 'kruising') return D.kruisingSectionMap;
return D.wisselSectionMap;
};
D.getSectionForNr = function (nr, type) {
var map = D.sectionMapForType(type);
for (const [section, nrs] of Object.entries(map)) {
if (nrs.includes(nr)) return section;
}
return null;
@ -17,6 +39,27 @@
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'
foto_s1_l: 'Foto Tong L', foto_s1_r: 'Foto Tong R',
foto_ow_sticker: 'Sticker overwegpaal', foto_ow_spoorbord: 'Spoornaambordje',
foto_ow_lk_1: 'Lage km foto 1', foto_ow_lk_2: 'Lage km foto 2',
foto_ow_lk_3: 'Lage km foto 3', foto_ow_lk_4: 'Lage km foto 4',
foto_ow_lk_5: 'Lage km foto 5', foto_ow_lk_6: 'Lage km foto 6',
foto_ow_hk_1: 'Hoge km foto 1', foto_ow_hk_2: 'Hoge km foto 2',
foto_ow_hk_3: 'Hoge km foto 3', foto_ow_hk_4: 'Hoge km foto 4',
foto_ow_hk_5: 'Hoge km foto 5', foto_ow_hk_6: 'Hoge km foto 6',
ovz_sp_1: 'Overzichtsfoto begin', ovz_sp_2: 'Overzichtsfoto einde',
ovz_eng_tl: 'Overzichtsfoto LB', ovz_eng_tr: 'Overzichtsfoto RB',
ovz_eng_bl: 'Overzichtsfoto LO', ovz_eng_br: 'Overzichtsfoto RO',
foto_eng_s5_l: 'Foto Puntstuk boven L', foto_eng_s5_r: 'Foto Puntstuk boven R',
foto_eng_s4_l: 'Foto Kruisstuk L', foto_eng_s4_r: 'Foto Kruisstuk R',
foto_eng_s3_l: 'Foto Midden L', foto_eng_s3_r: 'Foto Midden R',
foto_eng_s2_l: 'Foto Tong L', foto_eng_s2_r: 'Foto Tong R',
foto_eng_s1_l: 'Foto Puntstuk onder L', foto_eng_s1_r: 'Foto Puntstuk onder R',
ovz_kr_tl: 'Overzichtsfoto LB', ovz_kr_tr: 'Overzichtsfoto RB',
ovz_kr_bl: 'Overzichtsfoto LO', ovz_kr_br: 'Overzichtsfoto RO',
foto_kr_s4_l: 'Foto Puntstuk boven L', foto_kr_s4_r: 'Foto Puntstuk boven R',
foto_kr_s3_l: 'Foto Kruisstuk L', foto_kr_s3_r: 'Foto Kruisstuk R',
foto_kr_s2_l: 'Foto Midden L', foto_kr_s2_r: 'Foto Midden R',
foto_kr_s1_l: 'Foto Puntstuk onder L', foto_kr_s1_r: 'Foto Puntstuk onder R'
};
})(window.App.Domain);