Compare commits
2 Commits
367bbca688
...
e86aa5bae2
| Author | SHA1 | Date |
|---|---|---|
|
|
e86aa5bae2 | |
|
|
776bd6366d |
|
|
@ -0,0 +1,26 @@
|
||||||
|
:root {
|
||||||
|
--prorail-blue: #003082;
|
||||||
|
--prorail-light: #e8edf4;
|
||||||
|
--header-bg: #d4dce8;
|
||||||
|
--editable-bg: #f0f0f0;
|
||||||
|
--border: #999;
|
||||||
|
--section-header: #b8c8dc;
|
||||||
|
--score-green: #92d050;
|
||||||
|
--score-yellow: #ffc000;
|
||||||
|
--score-red: #ff4444;
|
||||||
|
--font: 'Segoe UI', Calibri, Arial, sans-serif;
|
||||||
|
--section-tong: #ef9a9a;
|
||||||
|
--section-midden: #90caf9;
|
||||||
|
--section-punt: #a5d6a7;
|
||||||
|
--status-open: #ffc107;
|
||||||
|
--status-done: #4caf50;
|
||||||
|
--status-ready: #8bc34a;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: var(--font); font-size: 11px; background: #f5f5f5; padding: 0; }
|
||||||
|
|
||||||
|
.screen { display: none; }
|
||||||
|
.screen.active { display: block; }
|
||||||
|
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
.flex-spacer { flex: 1; }
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
.form-toolbar {
|
||||||
|
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
|
||||||
|
background: var(--prorail-blue); color: #fff; flex-wrap: wrap;
|
||||||
|
position: sticky; top: 0; z-index: 100;
|
||||||
|
}
|
||||||
|
.form-toolbar button, .form-toolbar label {
|
||||||
|
background: #fff; color: var(--prorail-blue); border: none; padding: 6px 14px;
|
||||||
|
border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer;
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.form-toolbar button:hover, .form-toolbar label:hover { background: var(--prorail-light); }
|
||||||
|
.form-toolbar .title { flex: 1; font-size: 11px; opacity: 0.8; }
|
||||||
|
.form-toolbar input[type="file"] { display: none; }
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
background: transparent !important; color: #fff !important;
|
||||||
|
border: 1px solid rgba(255,255,255,0.4) !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
.btn-back:hover { background: rgba(255,255,255,0.15) !important; }
|
||||||
|
|
||||||
|
.btn-opname-gereed {
|
||||||
|
background: var(--status-done) !important; color: #fff !important;
|
||||||
|
font-size: 13px !important; padding: 8px 18px !important;
|
||||||
|
border-radius: 6px !important; font-weight: 700 !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(76,175,80,0.4);
|
||||||
|
transition: transform 0.1s, box-shadow 0.1s;
|
||||||
|
}
|
||||||
|
.btn-opname-gereed:hover {
|
||||||
|
background: #43a047 !important; transform: scale(1.02);
|
||||||
|
box-shadow: 0 3px 10px rgba(76,175,80,0.5);
|
||||||
|
}
|
||||||
|
.btn-opname-gereed:active { transform: scale(0.98); }
|
||||||
|
|
||||||
|
.tab-bar {
|
||||||
|
display: flex; background: #e0e7f0; border-bottom: 2px solid var(--prorail-blue);
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1; padding: 12px 20px; border: none; background: #d0d8e4; color: #555;
|
||||||
|
font-family: var(--font); font-size: 13px; font-weight: 600; cursor: pointer;
|
||||||
|
border-right: 1px solid #bbb; transition: background 0.2s, color 0.2s;
|
||||||
|
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||||
|
}
|
||||||
|
.tab-btn:last-child { border-right: none; }
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #fff; color: var(--prorail-blue);
|
||||||
|
border-bottom: 3px solid var(--prorail-blue); margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
.tab-btn:not(.active):hover { background: #c8d2e0; }
|
||||||
|
.tab-btn .tab-badge {
|
||||||
|
background: var(--prorail-blue); color: #fff; border-radius: 10px;
|
||||||
|
padding: 1px 6px; font-size: 10px; min-width: 18px; text-align: center;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-btn .tab-badge.visible { display: inline-block; }
|
||||||
|
.tab-btn.active .tab-badge { background: var(--score-green); color: #000; }
|
||||||
|
.tab-panel { display: none; }
|
||||||
|
.tab-panel.active { display: block; }
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
max-width: 1100px; margin: 0 auto; background: #fff;
|
||||||
|
border: 2px solid var(--prorail-blue); border-top: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.form-title {
|
||||||
|
background: var(--prorail-blue); color: #fff; text-align: center;
|
||||||
|
font-size: 16px; font-weight: 700; padding: 10px; letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-grid {
|
||||||
|
display: grid; grid-template-columns: 180px 1fr 180px 1fr;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.header-grid .cell {
|
||||||
|
padding: 3px 6px; border: 1px solid #ccc; font-size: 10.5px;
|
||||||
|
min-height: 22px; display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.header-grid .label { background: var(--header-bg); font-weight: 600; color: #333; }
|
||||||
|
.header-grid .value { background: #fff; color: #000; }
|
||||||
|
|
||||||
|
.task-section {
|
||||||
|
display: grid; grid-template-columns: 180px 1fr;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.task-section .label {
|
||||||
|
background: var(--header-bg); font-weight: 600; padding: 3px 6px;
|
||||||
|
border: 1px solid #ccc; font-size: 10.5px;
|
||||||
|
}
|
||||||
|
.task-section .value { padding: 3px 6px; border: 1px solid #ccc; font-size: 10.5px; }
|
||||||
|
|
||||||
|
.inspecteur-grid {
|
||||||
|
display: grid; grid-template-columns: 180px 1fr 180px 1fr;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.inspecteur-grid .cell {
|
||||||
|
padding: 3px 6px; border: 1px solid #ccc; font-size: 10.5px;
|
||||||
|
min-height: 24px; display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.inspecteur-grid .label { background: var(--header-bg); font-weight: 600; }
|
||||||
|
.inspecteur-grid .editable { background: var(--editable-bg); }
|
||||||
|
.inspecteur-grid input {
|
||||||
|
width: 100%; border: none; background: transparent;
|
||||||
|
font-family: var(--font); font-size: 10.5px; outline: none; padding: 2px 0;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background: #fff3cd; color: #856404; font-size: 10px; font-style: italic;
|
||||||
|
padding: 3px 6px; text-align: center; border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.totaal-grid {
|
||||||
|
display: grid; grid-template-columns: 180px 1fr 180px 1fr;
|
||||||
|
border-bottom: 2px solid var(--prorail-blue);
|
||||||
|
}
|
||||||
|
.totaal-grid .cell {
|
||||||
|
padding: 4px 6px; border: 1px solid #ccc; font-size: 11px;
|
||||||
|
min-height: 26px; display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.totaal-grid .label { background: var(--section-header); font-weight: 700; }
|
||||||
|
.totaal-grid .score-cell {
|
||||||
|
background: var(--editable-bg); font-weight: 700; font-size: 14px; justify-content: center;
|
||||||
|
}
|
||||||
|
.totaal-grid .fotonummers-cell { font-size: 9px; }
|
||||||
|
|
||||||
|
.assessment-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.assessment-table th {
|
||||||
|
background: var(--section-header); font-weight: 700; font-size: 10.5px;
|
||||||
|
padding: 5px 4px; border: 1px solid var(--border); text-align: center;
|
||||||
|
position: sticky; top: 44px; z-index: 2;
|
||||||
|
}
|
||||||
|
.assessment-table td {
|
||||||
|
border: 1px solid #ccc; padding: 3px 4px; font-size: 10.5px; vertical-align: middle;
|
||||||
|
}
|
||||||
|
.assessment-table .nr-col { width: 35px; text-align: center; font-weight: 600; }
|
||||||
|
.assessment-table .loc-col { min-width: 200px; }
|
||||||
|
.assessment-table .score-col { width: 80px; text-align: center; }
|
||||||
|
.assessment-table .foto-col { width: 80px; text-align: center; }
|
||||||
|
.assessment-table .opm-col { min-width: 180px; }
|
||||||
|
.assessment-table .section-row td {
|
||||||
|
background: var(--prorail-light); font-weight: 700; font-size: 11px;
|
||||||
|
color: var(--prorail-blue); padding: 6px 4px;
|
||||||
|
}
|
||||||
|
.assessment-table .editable-cell { background: var(--editable-bg); }
|
||||||
|
.assessment-table select {
|
||||||
|
width: 100%; border: 1px solid #ccc; background: #fff; font-family: var(--font);
|
||||||
|
font-size: 10.5px; padding: 2px; border-radius: 2px; text-align: center; cursor: pointer;
|
||||||
|
}
|
||||||
|
.assessment-table input[type="text"] {
|
||||||
|
width: 100%; border: none; background: transparent; font-family: var(--font);
|
||||||
|
font-size: 10px; outline: none; padding: 1px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-btn {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center; gap: 3px;
|
||||||
|
background: var(--prorail-blue); color: #fff; border: none; padding: 4px 8px;
|
||||||
|
border-radius: 3px; font-size: 10px; cursor: pointer; width: 100%; position: relative;
|
||||||
|
}
|
||||||
|
.foto-btn:hover { background: #004db3; }
|
||||||
|
.foto-btn .badge {
|
||||||
|
position: absolute; top: -4px; right: -4px; background: var(--score-red); color: #fff;
|
||||||
|
border-radius: 50%; width: 16px; height: 16px; font-size: 9px;
|
||||||
|
display: flex; align-items: center; justify-content: center; font-weight: 700;
|
||||||
|
}
|
||||||
|
.foto-btn .badge:empty { display: none; }
|
||||||
|
|
||||||
|
.score-b1,.score-b2,.score-b3 { background-color: var(--score-green) !important; color: #000; }
|
||||||
|
.score-g1,.score-g2 { background-color: var(--score-green) !important; color: #000; }
|
||||||
|
.score-g3,.score-g4 { background-color: var(--score-yellow) !important; color: #000; }
|
||||||
|
.score-e3,.score-e4 { background-color: var(--score-yellow) !important; color: #000; }
|
||||||
|
.score-e5,.score-e6 { background-color: var(--score-red) !important; color: #fff; }
|
||||||
|
|
||||||
|
.opmerkingen-section { border-top: 2px solid var(--prorail-blue); }
|
||||||
|
.opmerkingen-section .section-label {
|
||||||
|
background: var(--section-header); font-weight: 700; padding: 5px 6px;
|
||||||
|
font-size: 11px; border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.opmerkingen-section textarea {
|
||||||
|
width: 100%; min-height: 80px; border: none; background: var(--editable-bg);
|
||||||
|
font-family: var(--font); font-size: 10.5px; padding: 6px; resize: vertical; outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
padding: 4px 12px; background: var(--prorail-light); font-size: 10px;
|
||||||
|
color: #555; border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-container { max-width: 800px; margin: 0 auto; padding: 16px 8px; background: #fff; }
|
||||||
|
.overview-title {
|
||||||
|
text-align: center; font-size: 18px; font-weight: 700;
|
||||||
|
color: var(--prorail-blue); margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.instruction-box {
|
||||||
|
background: #fff; border: 2px solid #333; border-radius: 8px;
|
||||||
|
padding: 12px 16px; margin: 0 auto 20px auto; max-width: 500px;
|
||||||
|
font-size: 11px; line-height: 1.5; color: #333;
|
||||||
|
}
|
||||||
|
.instruction-box strong { color: var(--prorail-blue); }
|
||||||
|
|
||||||
|
.wissel-overview {
|
||||||
|
display: grid; grid-template-columns: 110px 1fr 110px;
|
||||||
|
grid-template-rows: auto auto auto auto auto;
|
||||||
|
gap: 0; max-width: 700px; margin: 0 auto 16px auto; align-items: stretch;
|
||||||
|
}
|
||||||
|
.ovz-row { grid-column: 1 / -1; display: flex; justify-content: space-between; padding: 8px 0; }
|
||||||
|
.side-labels { display: flex; flex-direction: column; justify-content: space-around; padding: 4px 0; }
|
||||||
|
.section-strip {
|
||||||
|
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.midden { background: var(--section-midden); }
|
||||||
|
.section-strip.tong { background: var(--section-tong); border-radius: 0 0 4px 4px; }
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
.section-strip .section-sub { font-size: 10px; color: #444; text-align: center; z-index: 1; }
|
||||||
|
.section-strip .section-sub.sm { font-size: 9px; margin-top: 4px; color: #666; }
|
||||||
|
.track-overlay { position: absolute; inset: 0; pointer-events: none; }
|
||||||
|
|
||||||
|
.capture-label {
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
||||||
|
cursor: pointer; padding: 8px 4px; border-radius: 6px;
|
||||||
|
transition: background 0.15s, transform 0.1s; position: relative;
|
||||||
|
user-select: none; -webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.capture-label:hover { background: rgba(0,48,130,0.08); }
|
||||||
|
.capture-label:active { transform: scale(0.95); background: rgba(0,48,130,0.15); }
|
||||||
|
.capture-label input[type="file"] { display: none; }
|
||||||
|
.capture-label .cap-icon {
|
||||||
|
width: 44px; height: 44px; background: var(--prorail-blue); border-radius: 50%;
|
||||||
|
display: flex; align-items: center; justify-content: center; color: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,48,130,0.4); position: relative;
|
||||||
|
}
|
||||||
|
.capture-label .cap-icon .cap-badge {
|
||||||
|
position: absolute; top: -3px; right: -3px; background: var(--score-green); color: #000;
|
||||||
|
border-radius: 50%; min-width: 18px; height: 18px; font-size: 10px; font-weight: 700;
|
||||||
|
display: flex; align-items: center; justify-content: center; border: 2px solid #fff;
|
||||||
|
}
|
||||||
|
.capture-label .cap-icon .cap-badge:empty { display: none; }
|
||||||
|
.capture-label .cap-text { font-size: 10px; font-weight: 600; color: var(--prorail-blue); text-align: center; }
|
||||||
|
.capture-label .cap-arrow { font-size: 16px; color: var(--prorail-blue); line-height: 1; }
|
||||||
|
.capture-label .cap-arrow.lg { font-size: 14px; }
|
||||||
|
.side-labels.left .capture-label .cap-arrow::after { content: "\2192"; }
|
||||||
|
.side-labels.right .capture-label .cap-arrow::after { content: "\2190"; }
|
||||||
|
|
||||||
|
.overview-thumbs {
|
||||||
|
max-width: 700px; margin: 10px auto; padding: 8px;
|
||||||
|
background: var(--prorail-light); border-radius: 6px; min-height: 40px;
|
||||||
|
}
|
||||||
|
.overview-thumbs h4 { font-size: 11px; color: var(--prorail-blue); margin-bottom: 6px; }
|
||||||
|
.overview-thumbs .thumb-grid { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.overview-thumbs .thumb-item { width: 72px; position: relative; }
|
||||||
|
.overview-thumbs .thumb-item img {
|
||||||
|
width: 72px; height: 54px; object-fit: cover; border-radius: 4px;
|
||||||
|
border: 2px solid #ccc; cursor: pointer;
|
||||||
|
}
|
||||||
|
.overview-thumbs .thumb-item img:hover { border-color: var(--prorail-blue); }
|
||||||
|
.overview-thumbs .thumb-item .thumb-label {
|
||||||
|
font-size: 8px; text-align: center; color: #555; margin-top: 1px;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.overview-thumbs .thumb-item .thumb-del {
|
||||||
|
position: absolute; top: -4px; right: -4px; background: rgba(255,0,0,0.85); color: #fff;
|
||||||
|
border: none; border-radius: 50%; width: 18px; height: 18px; font-size: 11px;
|
||||||
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.overview-thumbs .no-photos { font-size: 11px; color: #888; font-style: italic; padding: 4px 0; }
|
||||||
|
|
||||||
|
.svg-link-row { text-align: center; margin: 12px 0; }
|
||||||
|
.svg-link-btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px; background: var(--prorail-blue);
|
||||||
|
color: #fff; border: none; padding: 10px 20px; border-radius: 6px;
|
||||||
|
font-size: 13px; font-weight: 600; cursor: pointer; font-family: var(--font);
|
||||||
|
}
|
||||||
|
.svg-link-btn:hover { background: #004db3; }
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
.install-toolbar {
|
||||||
|
display: flex; align-items: center; gap: 8px; padding: 10px 16px;
|
||||||
|
background: var(--prorail-blue); color: #fff;
|
||||||
|
position: sticky; top: 0; z-index: 100;
|
||||||
|
}
|
||||||
|
.install-toolbar h1 { flex: 1; font-size: 16px; font-weight: 700; }
|
||||||
|
.install-toolbar .btn-back {
|
||||||
|
background: transparent; color: #fff; border: 1px solid rgba(255,255,255,0.4);
|
||||||
|
padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer;
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.install-toolbar .btn-back:hover { background: rgba(255,255,255,0.15); }
|
||||||
|
|
||||||
|
.install-body {
|
||||||
|
max-width: 640px; margin: 0 auto; padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-hero {
|
||||||
|
background: #fff; border-radius: 12px; padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08); text-align: center; margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.install-hero img { width: 96px; height: 96px; margin-bottom: 12px; }
|
||||||
|
.install-hero h2 { color: var(--prorail-blue); font-size: 20px; margin-bottom: 8px; }
|
||||||
|
.install-hero p { color: #555; font-size: 13px; line-height: 1.5; }
|
||||||
|
|
||||||
|
.install-status {
|
||||||
|
padding: 10px 14px; border-radius: 6px; font-size: 12px; margin-bottom: 16px;
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
}
|
||||||
|
.install-status.ok { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
|
||||||
|
.install-status.warn { background: #fff8e1; color: #f57f17; border: 1px solid #ffe082; }
|
||||||
|
.install-status.info { background: #e3f2fd; color: #1565c0; border: 1px solid #90caf9; }
|
||||||
|
|
||||||
|
.install-steps {
|
||||||
|
background: #fff; border-radius: 10px; padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.install-steps h3 { color: var(--prorail-blue); font-size: 15px; margin-bottom: 12px; }
|
||||||
|
.install-steps ol { padding-left: 20px; }
|
||||||
|
.install-steps li {
|
||||||
|
font-size: 13px; line-height: 1.7; color: #333; margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.install-steps code {
|
||||||
|
background: #f0f0f0; padding: 1px 6px; border-radius: 3px; font-size: 12px;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
.install-steps .key {
|
||||||
|
display: inline-block; padding: 2px 6px; border: 1px solid #bbb;
|
||||||
|
border-bottom-width: 2px; border-radius: 3px; background: #f8f8f8;
|
||||||
|
font-size: 11px; font-family: 'Segoe UI', sans-serif; margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-cta {
|
||||||
|
display: block; width: 100%; background: var(--prorail-blue); color: #fff;
|
||||||
|
border: none; padding: 14px 20px; border-radius: 8px;
|
||||||
|
font-size: 15px; font-weight: 700; cursor: pointer;
|
||||||
|
font-family: var(--font); margin-top: 12px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,48,130,0.3);
|
||||||
|
}
|
||||||
|
.install-cta:hover { background: #004db3; }
|
||||||
|
.install-cta:disabled { background: #aaa; cursor: not-allowed; box-shadow: none; }
|
||||||
|
|
||||||
|
.install-benefits {
|
||||||
|
background: #fff; border-radius: 10px; padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.install-benefits h3 { color: var(--prorail-blue); font-size: 15px; margin-bottom: 10px; }
|
||||||
|
.install-benefits ul { list-style: none; padding: 0; }
|
||||||
|
.install-benefits li {
|
||||||
|
font-size: 13px; color: #333; padding: 6px 0; padding-left: 28px;
|
||||||
|
position: relative; line-height: 1.5;
|
||||||
|
}
|
||||||
|
.install-benefits li::before {
|
||||||
|
content: '\2713'; position: absolute; left: 6px; top: 6px;
|
||||||
|
color: var(--status-done); font-weight: 700;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
.modal-overlay {
|
||||||
|
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7);
|
||||||
|
z-index: 1000; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.modal-overlay.active { display: flex; }
|
||||||
|
.modal {
|
||||||
|
background: #fff; border-radius: 8px; max-width: 600px; width: 95%;
|
||||||
|
max-height: 90vh; overflow-y: auto; padding: 16px; position: relative;
|
||||||
|
}
|
||||||
|
.modal h3 { color: var(--prorail-blue); margin-bottom: 10px; font-size: 14px; }
|
||||||
|
.modal-close {
|
||||||
|
position: absolute; top: 8px; right: 12px; background: none;
|
||||||
|
border: none; font-size: 22px; cursor: pointer; color: #666;
|
||||||
|
}
|
||||||
|
.modal-actions { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||||
|
.modal-actions button, .modal-actions label {
|
||||||
|
background: var(--prorail-blue); color: #fff; border: none; padding: 10px 16px;
|
||||||
|
border-radius: 4px; font-size: 13px; cursor: pointer;
|
||||||
|
display: inline-flex; align-items: center; gap: 6px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.modal-actions button:hover, .modal-actions label:hover { background: #004db3; }
|
||||||
|
.modal-actions input[type="file"] { display: none; }
|
||||||
|
.photo-grid {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 8px; margin-top: 10px;
|
||||||
|
}
|
||||||
|
.photo-grid .empty {
|
||||||
|
color: #888; font-size: 12px; padding: 10px;
|
||||||
|
}
|
||||||
|
.photo-thumb {
|
||||||
|
position: relative; aspect-ratio: 4/3; border: 1px solid #ccc;
|
||||||
|
border-radius: 4px; overflow: hidden; cursor: pointer;
|
||||||
|
}
|
||||||
|
.photo-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.photo-thumb .delete-btn {
|
||||||
|
position: absolute; top: 2px; right: 2px; background: rgba(255,0,0,0.8);
|
||||||
|
color: #fff; border: none; border-radius: 50%; width: 20px; height: 20px;
|
||||||
|
font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.photo-thumb .photo-time {
|
||||||
|
position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.6);
|
||||||
|
color: #fff; font-size: 8px; text-align: center; padding: 1px;
|
||||||
|
}
|
||||||
|
.lightbox {
|
||||||
|
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.9);
|
||||||
|
z-index: 2000; align-items: center; justify-content: center; cursor: zoom-out;
|
||||||
|
}
|
||||||
|
.lightbox.active { display: flex; }
|
||||||
|
.lightbox img { max-width: 95%; max-height: 95vh; object-fit: contain; }
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
.overview-toolbar {
|
||||||
|
display: flex; align-items: center; gap: 8px; padding: 10px 16px;
|
||||||
|
background: var(--prorail-blue); color: #fff; flex-wrap: wrap;
|
||||||
|
position: sticky; top: 0; z-index: 100;
|
||||||
|
}
|
||||||
|
.overview-toolbar h1 {
|
||||||
|
flex: 1; font-size: 16px; font-weight: 700; letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
.overview-toolbar button, .overview-toolbar label {
|
||||||
|
background: #fff; color: var(--prorail-blue); border: none; padding: 6px 14px;
|
||||||
|
border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer;
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.overview-toolbar button:hover, .overview-toolbar label:hover { background: var(--prorail-light); }
|
||||||
|
.overview-toolbar input[type="file"] { display: none; }
|
||||||
|
|
||||||
|
.overview-body { max-width: 1100px; margin: 0 auto; padding: 16px; }
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px; margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.summary-card {
|
||||||
|
background: #fff; border-radius: 10px; padding: 16px 20px;
|
||||||
|
border-left: 5px solid var(--prorail-blue);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
display: flex; align-items: center; gap: 14px;
|
||||||
|
}
|
||||||
|
.summary-card.wissel { border-left-color: #7b1fa2; }
|
||||||
|
.summary-card.overweg { border-left-color: #0288d1; }
|
||||||
|
.summary-card.spoor { border-left-color: #e65100; }
|
||||||
|
.summary-card.totaal { border-left-color: var(--prorail-blue); }
|
||||||
|
.summary-card .card-icon {
|
||||||
|
width: 44px; height: 44px; border-radius: 50%; display: flex;
|
||||||
|
align-items: center; justify-content: center; flex-shrink: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.summary-card.wissel .card-icon { background: #f3e5f5; color: #7b1fa2; }
|
||||||
|
.summary-card.overweg .card-icon { background: #e1f5fe; color: #0288d1; }
|
||||||
|
.summary-card.spoor .card-icon { background: #fff3e0; color: #e65100; }
|
||||||
|
.summary-card.totaal .card-icon { background: var(--prorail-light); color: var(--prorail-blue); }
|
||||||
|
.summary-card .card-info { flex: 1; }
|
||||||
|
.summary-card .card-value { font-size: 26px; font-weight: 800; color: #222; line-height: 1; }
|
||||||
|
.summary-card .card-label { font-size: 11px; color: #777; margin-top: 2px; }
|
||||||
|
|
||||||
|
.progress-section { margin-bottom: 16px; }
|
||||||
|
.progress-bar-bg {
|
||||||
|
height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%; width: 0%; background: var(--status-done); border-radius: 4px;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
}
|
||||||
|
.progress-text { font-size: 11px; color: #666; margin-top: 4px; text-align: right; }
|
||||||
|
|
||||||
|
.order-table-wrap {
|
||||||
|
background: #fff; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.order-table-header {
|
||||||
|
display: flex; align-items: center; padding: 12px 16px; gap: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.order-table-header h2 {
|
||||||
|
flex: 1; font-size: 14px; color: var(--prorail-blue);
|
||||||
|
}
|
||||||
|
.filter-btns { display: flex; gap: 4px; flex-wrap: wrap; }
|
||||||
|
.filter-btn {
|
||||||
|
border: 1px solid #ccc; background: #fff; padding: 4px 10px;
|
||||||
|
border-radius: 12px; font-size: 10px; cursor: pointer; font-family: var(--font);
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.filter-btn.active { background: var(--prorail-blue); color: #fff; border-color: var(--prorail-blue); }
|
||||||
|
.filter-btn:hover:not(.active) { background: #f0f0f0; }
|
||||||
|
|
||||||
|
.order-table-scroll { overflow-x: auto; }
|
||||||
|
.order-table {
|
||||||
|
width: 100%; border-collapse: collapse; table-layout: fixed;
|
||||||
|
}
|
||||||
|
.order-table col.col-order { width: 120px; }
|
||||||
|
.order-table col.col-type { width: 130px; }
|
||||||
|
.order-table col.col-omschr { width: auto; }
|
||||||
|
.order-table col.col-hoevh { width: 110px; }
|
||||||
|
.order-table col.col-status { width: 130px; }
|
||||||
|
.order-table th {
|
||||||
|
background: var(--prorail-light); font-size: 10px; font-weight: 700;
|
||||||
|
padding: 6px 10px; text-align: left; color: #555;
|
||||||
|
border-bottom: 1px solid #ddd; white-space: nowrap;
|
||||||
|
position: relative; user-select: none; overflow: hidden;
|
||||||
|
}
|
||||||
|
.order-table th .th-content {
|
||||||
|
display: flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.order-table th .col-resize {
|
||||||
|
position: absolute; top: 0; right: -3px; width: 6px; height: 100%;
|
||||||
|
cursor: col-resize; z-index: 2;
|
||||||
|
}
|
||||||
|
.order-table th .col-resize::after {
|
||||||
|
content: ''; position: absolute; top: 25%; bottom: 25%; right: 2px;
|
||||||
|
width: 2px; background: transparent; border-radius: 1px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.order-table th:hover .col-resize::after { background: #bbb; }
|
||||||
|
.order-table th .col-resize:active::after,
|
||||||
|
.order-table th .col-resize.dragging::after { background: var(--prorail-blue); }
|
||||||
|
|
||||||
|
.order-table thead tr.filter-row th {
|
||||||
|
background: #fff; padding: 4px 6px; border-bottom: 2px solid #ddd;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.order-table .col-filter {
|
||||||
|
width: 100%; border: 1px solid #ddd; border-radius: 3px;
|
||||||
|
padding: 4px 6px; font-size: 10px; font-family: var(--font);
|
||||||
|
background: #fafafa; outline: none; transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.order-table .col-filter:focus { border-color: var(--prorail-blue); background: #fff; }
|
||||||
|
.order-table .col-filter::placeholder { color: #bbb; }
|
||||||
|
.order-table .col-filter.has-value { background: #fffde7; border-color: #f9a825; }
|
||||||
|
|
||||||
|
.order-table td {
|
||||||
|
padding: 10px 10px; font-size: 11px; border-bottom: 1px solid #f0f0f0;
|
||||||
|
vertical-align: middle; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.order-table tbody tr {
|
||||||
|
cursor: pointer; transition: background 0.1s;
|
||||||
|
}
|
||||||
|
.order-table tbody tr:hover { background: #f5f8ff; }
|
||||||
|
.order-table tbody tr:active { background: #e8edf4; }
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
display: inline-block; padding: 2px 8px; border-radius: 10px;
|
||||||
|
font-size: 10px; font-weight: 600; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.type-badge.wissel { background: #f3e5f5; color: #7b1fa2; }
|
||||||
|
.type-badge.overweg { background: #e1f5fe; color: #0288d1; }
|
||||||
|
.type-badge.spoor { background: #fff3e0; color: #e65100; }
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
padding: 3px 10px; border-radius: 10px; font-size: 10px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.status-badge.open { background: #fff8e1; color: #f57f17; }
|
||||||
|
.status-badge.done { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.status-badge .status-dot {
|
||||||
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
|
}
|
||||||
|
.status-badge.open .status-dot { background: var(--status-open); }
|
||||||
|
.status-badge.done .status-dot { background: var(--status-done); }
|
||||||
|
|
||||||
|
.order-table .hoevh-cell { text-align: right; font-weight: 600; }
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
@media print {
|
||||||
|
.form-toolbar, .tab-bar, .foto-btn, .modal-overlay, .lightbox, .status-bar,
|
||||||
|
.capture-label, .overview-thumbs, .svg-link-row, .btn-opname-gereed, .btn-back { display: none !important; }
|
||||||
|
body { padding: 0; background: #fff; }
|
||||||
|
.form-container { border: 1px solid #000; box-shadow: none; }
|
||||||
|
.tab-panel { display: block !important; }
|
||||||
|
.screen { display: block !important; }
|
||||||
|
#screen-overview { display: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-grid { grid-template-columns: 120px 1fr 120px 1fr; }
|
||||||
|
.header-grid .cell { font-size: 9.5px; padding: 2px 4px; }
|
||||||
|
.foto-btn { padding: 8px; font-size: 12px; }
|
||||||
|
.assessment-table select { padding: 6px; font-size: 12px; }
|
||||||
|
.wissel-overview { grid-template-columns: 80px 1fr 80px; }
|
||||||
|
.capture-label .cap-icon { width: 38px; height: 38px; }
|
||||||
|
.section-strip { min-height: 110px; }
|
||||||
|
.summary-row { grid-template-columns: 1fr 1fr; }
|
||||||
|
.order-table td, .order-table th { padding: 8px 6px; font-size: 10px; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" rx="96" fill="#003082"/>
|
||||||
|
<g stroke="#ffffff" stroke-width="18" stroke-linecap="round" fill="none">
|
||||||
|
<line x1="160" y1="80" x2="160" y2="432"/>
|
||||||
|
<line x1="352" y1="80" x2="352" y2="432"/>
|
||||||
|
<line x1="100" y1="140" x2="412" y2="140"/>
|
||||||
|
<line x1="100" y1="220" x2="412" y2="220"/>
|
||||||
|
<line x1="100" y1="300" x2="412" y2="300"/>
|
||||||
|
<line x1="100" y1="380" x2="412" y2="380"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#ffc000">
|
||||||
|
<circle cx="256" cy="256" r="44"/>
|
||||||
|
</g>
|
||||||
|
<text x="256" y="478" text-anchor="middle" fill="#ffffff" font-family="Segoe UI, Arial, sans-serif" font-size="44" font-weight="800" letter-spacing="2">DUIMSTOK</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 734 B |
|
|
@ -0,0 +1,320 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||||
|
<meta name="theme-color" content="#003082">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Duimstok">
|
||||||
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
|
<link rel="apple-touch-icon" href="icons/icon.svg">
|
||||||
|
<title>Duimstok-inspecties Bovenbouw</title>
|
||||||
|
<link rel="stylesheet" href="css/base.css">
|
||||||
|
<link rel="stylesheet" href="css/overview.css">
|
||||||
|
<link rel="stylesheet" href="css/form.css">
|
||||||
|
<link rel="stylesheet" href="css/modals.css">
|
||||||
|
<link rel="stylesheet" href="css/install.css">
|
||||||
|
<link rel="stylesheet" href="css/responsive.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="screen active" id="screen-overview">
|
||||||
|
|
||||||
|
<div class="overview-toolbar">
|
||||||
|
<h1>Duimstok-inspecties Bovenbouw</h1>
|
||||||
|
<button id="btnOpenInstall" title="Installeren als app">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><rect x="3" y="17" width="18" height="4" rx="1"/></svg>
|
||||||
|
Installeren
|
||||||
|
</button>
|
||||||
|
<label>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
|
CSV laden
|
||||||
|
<input type="file" id="csvFileInput" accept=".csv,.txt">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overview-body">
|
||||||
|
|
||||||
|
<div class="summary-row" id="summaryCards"></div>
|
||||||
|
|
||||||
|
<div class="progress-section">
|
||||||
|
<div class="progress-bar-bg"><div class="progress-bar-fill" id="progressFill"></div></div>
|
||||||
|
<div class="progress-text" id="progressText"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-table-wrap">
|
||||||
|
<div class="order-table-header">
|
||||||
|
<h2>Orders</h2>
|
||||||
|
<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="Overwegbevloering">Overwegen</button>
|
||||||
|
<button class="filter-btn" data-filter="Spoor">Spoor</button>
|
||||||
|
<button class="filter-btn" data-filter="open">Nog open</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-table-scroll">
|
||||||
|
<table class="order-table" id="orderTable">
|
||||||
|
<colgroup>
|
||||||
|
<col class="col-order"><col class="col-type"><col class="col-omschr">
|
||||||
|
<col class="col-hoevh"><col class="col-status">
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr class="label-row">
|
||||||
|
<th><span class="th-content">Order</span><div class="col-resize" data-col="0"></div></th>
|
||||||
|
<th><span class="th-content">Type</span><div class="col-resize" data-col="1"></div></th>
|
||||||
|
<th><span class="th-content">Omschrijving</span><div class="col-resize" data-col="2"></div></th>
|
||||||
|
<th><span class="th-content">Hoeveelheid</span><div class="col-resize" data-col="3"></div></th>
|
||||||
|
<th><span class="th-content">Status</span></th>
|
||||||
|
</tr>
|
||||||
|
<tr class="filter-row">
|
||||||
|
<th><input class="col-filter" type="text" data-col="order" placeholder="Filter..."></th>
|
||||||
|
<th><input class="col-filter" type="text" data-col="type" placeholder="Filter..."></th>
|
||||||
|
<th><input class="col-filter" type="text" data-col="omschrijving" placeholder="Filter..."></th>
|
||||||
|
<th><input class="col-filter" type="text" data-col="hoeveelheid" placeholder="Filter..."></th>
|
||||||
|
<th><input class="col-filter" type="text" data-col="status" placeholder="Filter..."></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="orderTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="screen" id="screen-form">
|
||||||
|
|
||||||
|
<div class="form-toolbar">
|
||||||
|
<button class="btn-back" id="btnBack">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||||
|
Overzicht
|
||||||
|
</button>
|
||||||
|
<span class="title" id="formToolbarTitle">ProRail Duimstok-inspectie</span>
|
||||||
|
<label>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
|
XML laden
|
||||||
|
<input type="file" id="xmlFileInput" accept=".xml">
|
||||||
|
</label>
|
||||||
|
<button id="btnExport">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||||||
|
Opslaan
|
||||||
|
</button>
|
||||||
|
<button id="btnPrint">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
||||||
|
Afdrukken
|
||||||
|
</button>
|
||||||
|
<button class="btn-opname-gereed" id="btnOpnameGereed">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
|
Opname gereed
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-bar">
|
||||||
|
<button class="tab-btn active" data-tab="inspectie">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
Inspectie
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="overzicht">
|
||||||
|
<svg width="16" height="16" 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>
|
||||||
|
Overzicht
|
||||||
|
<span class="tab-badge" id="tabBadgeOvz">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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="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">Afschrijvingsgroep</div><div class="cell value" id="hdr_afschr"></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="task-section">
|
||||||
|
<div class="label">Taak omschrijving</div><div class="value" id="hdr_taakomschrijving"></div>
|
||||||
|
</div>
|
||||||
|
<div class="task-section">
|
||||||
|
<div class="label">Aanleiding probleem</div><div class="value" id="hdr_aanleiding"></div>
|
||||||
|
</div>
|
||||||
|
<div class="inspecteur-grid">
|
||||||
|
<div class="cell label">Inspecteur</div>
|
||||||
|
<div class="cell editable"><input type="text" id="inp_inspecteur" placeholder="Naam inspecteur"></div>
|
||||||
|
<div class="cell label">Techn. verv. jaar</div>
|
||||||
|
<div class="cell editable"><input type="text" id="inp_techjaar" placeholder=""></div>
|
||||||
|
</div>
|
||||||
|
<div class="inspecteur-grid">
|
||||||
|
<div class="cell label">Inspectiedatum</div>
|
||||||
|
<div class="cell editable"><input type="date" id="inp_inspectiedatum"></div>
|
||||||
|
<div class="cell label">(Nieuw) Inspectiejaar</div>
|
||||||
|
<div class="cell editable"><input type="text" id="inp_inspjaar" placeholder=""></div>
|
||||||
|
</div>
|
||||||
|
<div class="warning">Zowel de naam van de inspecteur als de inspectiedatum dienen ingevuld te worden!</div>
|
||||||
|
<div class="totaal-grid">
|
||||||
|
<div class="cell label">Totaalscore</div><div class="cell score-cell" id="totaalscore">-</div>
|
||||||
|
<div class="cell label">Fotonummers</div><div class="cell fotonummers-cell" id="totaal_fotonummers"></div>
|
||||||
|
</div>
|
||||||
|
<table class="assessment-table" id="assessmentTable">
|
||||||
|
<thead><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>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody id="assessmentBody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="opmerkingen-section">
|
||||||
|
<div class="section-label">Overige opmerkingen (Enter voor nieuwe regel)</div>
|
||||||
|
<textarea id="inp_opmerkingen" placeholder="Opmerkingen hier invoeren..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="status-bar">
|
||||||
|
<span id="statusOrder">Geen order geladen</span>
|
||||||
|
<span id="statusFotos">Foto's: 0</span>
|
||||||
|
<span id="statusSaved"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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="wissel-overview">
|
||||||
|
<div class="ovz-row">
|
||||||
|
<label class="capture-label" data-pos="ovz_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_tl"></span></div>
|
||||||
|
<span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">↘</span>
|
||||||
|
<input type="file" accept="image/*" capture="environment" data-pos="ovz_tl">
|
||||||
|
</label>
|
||||||
|
<div class="flex-spacer"></div>
|
||||||
|
<label class="capture-label" data-pos="ovz_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_tr"></span></div>
|
||||||
|
<span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">↙</span>
|
||||||
|
<input type="file" accept="image/*" capture="environment" data-pos="ovz_tr">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="side-labels left"><label class="capture-label" data-pos="foto_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_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_s3_l"></label></div>
|
||||||
|
<div class="section-strip puntstuk">
|
||||||
|
<svg class="track-overlay" viewBox="0 0 200 140" preserveAspectRatio="none"><line x1="90" y1="0" x2="90" y2="140" stroke="#555" stroke-width="2.5"/><line x1="110" y1="0" x2="110" y2="140" stroke="#555" stroke-width="2.5"/><line x1="110" y1="140" x2="155" y2="0" stroke="#555" stroke-width="2.5"/><line x1="115" y1="140" x2="165" y2="0" stroke="#555" stroke-width="2.5"/><line x1="70" y1="20" x2="140" y2="20" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="45" x2="148" y2="45" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="70" x2="152" y2="70" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="95" x2="155" y2="95" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="120" x2="158" y2="120" stroke="#8B7355" stroke-width="3" opacity="0.5"/></svg>
|
||||||
|
<div class="section-name">Puntstuk</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_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_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_s3_r"></label></div>
|
||||||
|
<div class="side-labels left"><label class="capture-label" data-pos="foto_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_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_s2_l"></label></div>
|
||||||
|
<div class="section-strip midden">
|
||||||
|
<svg class="track-overlay" viewBox="0 0 200 140" preserveAspectRatio="none"><line x1="90" y1="0" x2="90" y2="140" stroke="#555" stroke-width="2.5"/><line x1="110" y1="0" x2="110" y2="140" stroke="#555" stroke-width="2.5"/><line x1="130" y1="0" x2="115" y2="140" stroke="#555" stroke-width="2.5"/><line x1="140" y1="0" x2="120" y2="140" stroke="#555" stroke-width="2.5"/><line x1="70" y1="25" x2="148" y2="25" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="55" x2="143" y2="55" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="85" x2="138" y2="85" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="115" x2="133" y2="115" stroke="#8B7355" stroke-width="3" opacity="0.5"/></svg>
|
||||||
|
<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_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_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_s2_r"></label></div>
|
||||||
|
<div class="side-labels left"><label class="capture-label" data-pos="foto_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_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_s1_l"></label></div>
|
||||||
|
<div class="section-strip tong">
|
||||||
|
<svg class="track-overlay" viewBox="0 0 200 140" preserveAspectRatio="none"><line x1="90" y1="0" x2="90" y2="140" stroke="#555" stroke-width="2.5"/><line x1="110" y1="0" x2="110" y2="140" stroke="#555" stroke-width="2.5"/><line x1="113" y1="0" x2="110" y2="90" stroke="#555" stroke-width="2"/><line x1="117" y1="0" x2="114" y2="80" stroke="#555" stroke-width="2"/><circle cx="112" cy="95" r="3" fill="#c00"/><line x1="70" y1="15" x2="130" y2="15" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="45" x2="130" y2="45" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="75" x2="130" y2="75" stroke="#8B7355" stroke-width="3" opacity="0.5"/><line x1="70" y1="105" x2="130" y2="105" stroke="#8B7355" stroke-width="3" opacity="0.5"/><rect x="85" y="125" width="4" height="12" fill="#ffc000"/><rect x="111" y="125" width="4" height="12" fill="#ffc000"/></svg>
|
||||||
|
<div class="section-name">Tong-</div><div class="section-name">beweging</div>
|
||||||
|
<div class="section-sub sm">Sectie 1</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-labels right"><label class="capture-label" data-pos="foto_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_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_s1_r"></label></div>
|
||||||
|
<div class="ovz-row">
|
||||||
|
<label class="capture-label" data-pos="ovz_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_bl"></span></div>
|
||||||
|
<span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">↗</span>
|
||||||
|
<input type="file" accept="image/*" capture="environment" data-pos="ovz_bl">
|
||||||
|
</label>
|
||||||
|
<div class="flex-spacer"></div>
|
||||||
|
<label class="capture-label" data-pos="ovz_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_br"></span></div>
|
||||||
|
<span class="cap-text">Overzichtsfoto</span><span class="cap-arrow lg">↖</span>
|
||||||
|
<input type="file" accept="image/*" capture="environment" data-pos="ovz_br">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overview-thumbs" id="overviewThumbs">
|
||||||
|
<h4>Vastgelegde foto's</h4>
|
||||||
|
<div class="thumb-grid" id="overviewThumbGrid">
|
||||||
|
<div class="no-photos">Nog geen foto's gemaakt. Tik op een Foto- of Overzichtsfoto-knop hierboven.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="svg-link-row">
|
||||||
|
<button class="svg-link-btn" id="btnOpenSvg">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||||
|
Technische tekening openen (SVG)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="screen" id="screen-install">
|
||||||
|
<div class="install-toolbar">
|
||||||
|
<button class="btn-back" id="btnInstallBack">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||||
|
Terug
|
||||||
|
</button>
|
||||||
|
<h1>App installeren</h1>
|
||||||
|
</div>
|
||||||
|
<div class="install-body" id="installBody"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="photoModal">
|
||||||
|
<div class="modal">
|
||||||
|
<button class="modal-close" id="photoModalClose">×</button>
|
||||||
|
<h3 id="photoModalTitle">Foto's</h3>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<label><svg width="16" height="16" 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 maken<input type="file" accept="image/*" capture="environment" id="cameraInput"></label>
|
||||||
|
<label><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>Uit galerij<input type="file" accept="image/*" multiple id="galleryInput"></label>
|
||||||
|
</div>
|
||||||
|
<div class="photo-grid" id="photoGrid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lightbox" id="lightbox">
|
||||||
|
<img id="lightboxImg" src="" alt="Foto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/namespace.js"></script>
|
||||||
|
<script src="../src/Domain/sectionMap.js"></script>
|
||||||
|
<script src="../src/Domain/scoring.js"></script>
|
||||||
|
<script src="../src/Domain/orderParser.js"></script>
|
||||||
|
<script src="../src/Infrastructure/utils.js"></script>
|
||||||
|
<script src="../src/Infrastructure/geolocation.js"></script>
|
||||||
|
<script src="../src/Infrastructure/db.js"></script>
|
||||||
|
<script src="../src/Infrastructure/seedOrders.js"></script>
|
||||||
|
<script src="../src/Infrastructure/csvLoader.js"></script>
|
||||||
|
<script src="../src/Application/state.js"></script>
|
||||||
|
<script src="../src/Application/persistence.js"></script>
|
||||||
|
<script src="../src/Application/screens.js"></script>
|
||||||
|
<script src="../src/Application/photoService.js"></script>
|
||||||
|
<script src="../src/Application/inspectionForm.js"></script>
|
||||||
|
<script src="../src/Application/orderOverview.js"></script>
|
||||||
|
<script src="../src/Application/xmlImport.js"></script>
|
||||||
|
<script src="../src/Application/exportService.js"></script>
|
||||||
|
<script src="../src/Application/pwa.js"></script>
|
||||||
|
<script src="../src/Application/installScreen.js"></script>
|
||||||
|
<script src="js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
window.App = window.App || { Domain: {}, Application: {}, Infrastructure: {} };
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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' });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
(function (A, I) {
|
||||||
|
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>`));
|
||||||
|
}
|
||||||
|
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>');
|
||||||
|
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(p.blob, `${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(p.blob, `${fd.orderNr.replace('/', '-')}_${pos}_foto${i + 1}.jpg`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
alert(`Opgeslagen!\n- 1 XML-bestand\n- ${pc} foto('s)`);
|
||||||
|
};
|
||||||
|
})(window.App.Application, window.App.Infrastructure);
|
||||||
|
|
@ -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 '<option value="">-</option>' +
|
||||||
|
D.SCORE_VALUES.map(v => `<option value="${v}" ${selected === v ? 'selected' : ''}>${v}</option>`).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 = `<td colspan="6">${section}</td>`;
|
||||||
|
tbody.appendChild(sr);
|
||||||
|
}
|
||||||
|
const 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, '"')}"><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>`;
|
||||||
|
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);
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
(function (A) {
|
||||||
|
function statusBlock() {
|
||||||
|
if (A.pwa.isFileProtocol()) {
|
||||||
|
return `<div class="install-status warn">
|
||||||
|
<strong>Let op:</strong> de app is nu geopend vanaf uw bestandssysteem
|
||||||
|
(<code>file://</code>). Installeren als PWA werkt alleen via een webserver
|
||||||
|
(bijv. <code>http://localhost</code> of een gedeploydde versie).
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
if (A.pwa.isStandalone()) {
|
||||||
|
return `<div class="install-status ok">
|
||||||
|
<strong>Geïnstalleerd.</strong> U gebruikt de app al in PWA-modus.
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
if (!A.pwa.isSupported()) {
|
||||||
|
return `<div class="install-status warn">
|
||||||
|
Deze browser ondersteunt PWA-installatie niet volledig. Probeer het in
|
||||||
|
Chrome, Edge, Safari (iOS 16.4+) of een Chromium-gebaseerde browser.
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
return `<div class="install-status info">
|
||||||
|
Volg onderstaande stappen om de app op dit apparaat te installeren.
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stepsForPlatform(platform) {
|
||||||
|
if (platform === 'ios') {
|
||||||
|
return `<ol>
|
||||||
|
<li>Tik onderin het scherm op het <strong>Delen</strong>-pictogram
|
||||||
|
(vierkant met pijl omhoog).</li>
|
||||||
|
<li>Scrol omlaag en tik op <strong>Zet op beginscherm</strong>.</li>
|
||||||
|
<li>Bevestig met <strong>Voeg toe</strong>. Het Duimstok-icoon verschijnt
|
||||||
|
tussen uw apps.</li>
|
||||||
|
<li>Open de app vanaf het beginscherm voor volledige werking offline.</li>
|
||||||
|
</ol>
|
||||||
|
<p style="font-size:12px;color:#666;margin-top:10px;">
|
||||||
|
iOS ondersteunt geen automatische installatieknop. Installatie gebeurt
|
||||||
|
altijd via het Delen-menu van Safari.
|
||||||
|
</p>`;
|
||||||
|
}
|
||||||
|
if (platform === 'android') {
|
||||||
|
return `<ol>
|
||||||
|
<li>Gebruik de knop <em>App installeren</em> hieronder, of</li>
|
||||||
|
<li>Tik rechtsboven op <strong>\u22EE</strong> (menu) en kies
|
||||||
|
<strong>App installeren</strong> / <strong>Toevoegen aan startscherm</strong>.</li>
|
||||||
|
<li>Bevestig met <strong>Installeren</strong>.</li>
|
||||||
|
</ol>`;
|
||||||
|
}
|
||||||
|
if (platform === 'desktop-chromium') {
|
||||||
|
return `<ol>
|
||||||
|
<li>Gebruik de knop <em>App installeren</em> hieronder, of</li>
|
||||||
|
<li>Klik op het installeerpictogram <strong>\u2B07</strong> rechts in de
|
||||||
|
adresbalk.</li>
|
||||||
|
<li>Als alternatief: menu <span class="key">\u22EE</span>
|
||||||
|
→ <strong>Apps</strong> → <strong>Deze site installeren</strong>.</li>
|
||||||
|
</ol>`;
|
||||||
|
}
|
||||||
|
if (platform === 'firefox') {
|
||||||
|
return `<ol>
|
||||||
|
<li>Firefox desktop ondersteunt PWA-installatie niet standaard.</li>
|
||||||
|
<li>Maak een snelkoppeling op het bureaublad via
|
||||||
|
<strong>Bestand</strong> → <strong>Pagina opslaan als...</strong>
|
||||||
|
of gebruik Firefox op Android, waar <em>Toevoegen aan startscherm</em>
|
||||||
|
wel werkt.</li>
|
||||||
|
</ol>`;
|
||||||
|
}
|
||||||
|
if (platform === 'desktop-safari') {
|
||||||
|
return `<ol>
|
||||||
|
<li>Ga naar het menu <strong>Archief</strong> →
|
||||||
|
<strong>Voeg toe aan Dock</strong> (macOS Sonoma of nieuwer).</li>
|
||||||
|
<li>De app verschijnt in het Dock en werkt offline.</li>
|
||||||
|
</ol>`;
|
||||||
|
}
|
||||||
|
return `<ol>
|
||||||
|
<li>Uw browser is niet herkend. Zoek in het menu naar
|
||||||
|
<em>"Installeren"</em> of <em>"Toevoegen aan startscherm"</em>.</li>
|
||||||
|
</ol>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function benefitsBlock() {
|
||||||
|
return `<div class="install-benefits">
|
||||||
|
<h3>Voordelen van installatie</h3>
|
||||||
|
<ul>
|
||||||
|
<li>App opent vanaf uw startscherm, zonder browseradresbalk.</li>
|
||||||
|
<li>Werkt offline \u2014 inspecties zijn ook zonder dekking in te vullen.</li>
|
||||||
|
<li>Foto's en gegevens blijven lokaal opgeslagen op het apparaat.</li>
|
||||||
|
<li>Snellere start omdat bestanden in de cache staan.</li>
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mainCTA() {
|
||||||
|
if (A.pwa.isFileProtocol()) return '';
|
||||||
|
if (A.pwa.isStandalone()) return '';
|
||||||
|
return `<button class="install-cta" id="installCta"
|
||||||
|
${A.pwa.canPromptInstall() ? '' : 'disabled'}>
|
||||||
|
${A.pwa.canPromptInstall() ? 'App installeren' : 'Installatie niet beschikbaar via knop'}
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
A.renderInstallScreen = function () {
|
||||||
|
const body = document.getElementById('installBody');
|
||||||
|
const platform = A.pwa.getPlatform();
|
||||||
|
body.innerHTML = `
|
||||||
|
<div class="install-hero">
|
||||||
|
<img src="icons/icon.svg" alt="Duimstok app-icoon">
|
||||||
|
<h2>Duimstok-inspecties als app</h2>
|
||||||
|
<p>Installeer deze applicatie op uw telefoon, tablet of desktop
|
||||||
|
voor offline gebruik in het veld.</p>
|
||||||
|
</div>
|
||||||
|
${statusBlock()}
|
||||||
|
<div class="install-steps">
|
||||||
|
<h3>Installatie op uw apparaat</h3>
|
||||||
|
${stepsForPlatform(platform)}
|
||||||
|
${mainCTA()}
|
||||||
|
</div>
|
||||||
|
${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);
|
||||||
|
|
@ -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 = `
|
||||||
|
<div class="summary-card wissel">
|
||||||
|
<div class="card-icon">☷</div>
|
||||||
|
<div class="card-info"><div class="card-value">${wissels}</div><div class="card-label">Wissels nog op te nemen</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card overweg">
|
||||||
|
<div class="card-icon">▩</div>
|
||||||
|
<div class="card-info"><div class="card-value">${overwegen}</div><div class="card-label">Overwegbevloeringen nog op te nemen</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card spoor">
|
||||||
|
<div class="card-icon">⋮</div>
|
||||||
|
<div class="card-info"><div class="card-value">${spoorMeters} m</div><div class="card-label">Meter spoor nog op te nemen</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card totaal">
|
||||||
|
<div class="card-icon">∑</div>
|
||||||
|
<div class="card-info"><div class="card-value">${remaining.length} / ${orders.length}</div><div class="card-label">Totaal nog uit te voeren</div></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 `<tr data-order-key="${o.orderKey}">
|
||||||
|
<td title="${d.orderText}"><strong>${o.order}</strong>/${o.operatie}</td>
|
||||||
|
<td title="${d.typeText}"><span class="type-badge ${typeClass}">${o.objectsoort}</span></td>
|
||||||
|
<td title="${d.omschrText}">${o.omschrijving}</td>
|
||||||
|
<td class="hoevh-cell" title="${d.hoevhText}">${o.hoeveelheid} ${o.eenheid}</td>
|
||||||
|
<td title="${d.statusText}"><span class="status-badge ${statusClass}"><span class="status-dot"></span>${d.statusText}</span></td>
|
||||||
|
</tr>`;
|
||||||
|
}).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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
(function (A, D, I) {
|
||||||
|
function photoSrc(p) {
|
||||||
|
return URL.createObjectURL(p.blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
||||||
|
blob: 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 = '<p class="empty">Nog geen foto\'s.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
grid.innerHTML = photos.map((p, i) => `
|
||||||
|
<div class="photo-thumb">
|
||||||
|
<img data-r="${row}" data-i="${i}">
|
||||||
|
<button class="delete-btn" data-action="delete-photo" data-r="${row}" data-i="${i}">×</button>
|
||||||
|
<div class="photo-time">${new Date(p.timestamp).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' })}${p.gps ? ' \u2316' : ''}</div>
|
||||||
|
</div>`).join('');
|
||||||
|
|
||||||
|
photos.forEach((p, i) => {
|
||||||
|
const img = grid.querySelector(`img[data-r="${row}"][data-i="${i}"]`);
|
||||||
|
if (img) img.src = photoSrc(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(photoSrc(ph));
|
||||||
|
}));
|
||||||
|
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({
|
||||||
|
blob: 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 = '<div class="no-photos">Nog geen foto\'s gemaakt.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
grid.innerHTML = all.map(it => `
|
||||||
|
<div class="thumb-item">
|
||||||
|
<img data-pos="${it.pos}" data-idx="${it.index}">
|
||||||
|
<button class="thumb-del" data-action="delete-ov-photo" data-pos="${it.pos}" data-idx="${it.index}">×</button>
|
||||||
|
<div class="thumb-label">${it.label}</div>
|
||||||
|
</div>`).join('');
|
||||||
|
|
||||||
|
all.forEach(it => {
|
||||||
|
const img = grid.querySelector(`img[data-pos="${it.pos}"][data-idx="${it.index}"]`);
|
||||||
|
if (img) img.src = photoSrc(it.photo);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(photoSrc(ph));
|
||||||
|
}));
|
||||||
|
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) {
|
||||||
|
const img = document.getElementById('lightboxImg');
|
||||||
|
const prev = img.src;
|
||||||
|
img.src = src;
|
||||||
|
document.getElementById('lightbox').classList.add('active');
|
||||||
|
if (prev && prev.startsWith('blob:')) URL.revokeObjectURL(prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
A.closeLightbox = function () {
|
||||||
|
const img = document.getElementById('lightboxImg');
|
||||||
|
const src = img.src;
|
||||||
|
document.getElementById('lightbox').classList.remove('active');
|
||||||
|
img.src = '';
|
||||||
|
if (src && src.startsWith('blob:')) URL.revokeObjectURL(src);
|
||||||
|
};
|
||||||
|
})(window.App.Application, window.App.Domain, window.App.Infrastructure);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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(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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
(function (I) {
|
||||||
|
I.esc = function (s) {
|
||||||
|
return String(s).replace(/&/g, '&').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.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);
|
||||||
Loading…
Reference in New Issue