diff --git a/image-import.js b/image-import.js index b3b78ff..4cdd20e 100644 --- a/image-import.js +++ b/image-import.js @@ -349,12 +349,93 @@ class ImageImporter { } /** - * Placeholder until Task 9 implements full wiring. Idempotent. + * Attach DOM event listeners. Called lazily on first openImportDialog. + * Safe to call multiple times (idempotent via this._wired flag). */ wireEventsOnce() { if (this._wired) return; this._wired = true; - // Real handlers added in Task 9. + + const closeBtn = document.getElementById('image-import-close-btn'); + const cancel1 = document.getElementById('image-import-cancel-1-btn'); + const cancel3 = document.getElementById('image-import-cancel-3-btn'); + [closeBtn, cancel1, cancel3].forEach(b => { + if (b) b.addEventListener('click', () => this.close()); + }); + + const cancel2 = document.getElementById('image-import-cancel-2-btn'); + if (cancel2) cancel2.addEventListener('click', () => { + if (this.abortController) { + try { this.abortController.abort(); } catch (e) { /* ignore */ } + } + this.close(); + }); + + const fileInput = document.getElementById('image-import-file-input'); + const pickBtn = document.getElementById('image-import-pick-btn'); + if (pickBtn) pickBtn.addEventListener('click', () => fileInput && fileInput.click()); + if (fileInput) fileInput.addEventListener('change', (e) => { + const f = e.target.files && e.target.files[0]; + if (f) this.onFileSelected(f); + }); + + const cameraInput = document.getElementById('image-import-camera-input'); + const cameraBtn = document.getElementById('image-import-camera-btn'); + if (cameraBtn) cameraBtn.addEventListener('click', () => cameraInput && cameraInput.click()); + if (cameraInput) cameraInput.addEventListener('change', (e) => { + const f = e.target.files && e.target.files[0]; + if (f) this.onFileSelected(f); + }); + + const dz = document.getElementById('image-import-dropzone'); + if (dz) { + dz.addEventListener('dragover', (e) => { e.preventDefault(); dz.classList.add('drag-over'); }); + dz.addEventListener('dragleave', () => dz.classList.remove('drag-over')); + dz.addEventListener('drop', (e) => { + e.preventDefault(); + dz.classList.remove('drag-over'); + const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; + if (f) this.onFileSelected(f); + }); + } + + const recognizeBtn = document.getElementById('image-import-recognize-btn'); + if (recognizeBtn) recognizeBtn.addEventListener('click', () => this.runRecognition()); + + const confirmBtn = document.getElementById('image-import-confirm-btn'); + if (confirmBtn) confirmBtn.addEventListener('click', () => this.commitImport()); + } + + /** + * Validate file (type + size), set into session, render thumbnail, enable Erkennen. + * @param {File} file + */ + onFileSelected(file) { + if (!file.type || !file.type.startsWith('image/')) { + if (this.app) this.app.showToast('Nur Bildformate werden unterstuetzt', 'error'); + return; + } + const MAX = 20 * 1024 * 1024; + if (file.size > MAX) { + if (this.app) this.app.showToast('Bild zu gross (max. 20 MB)', 'error'); + return; + } + + if (this.session.thumbnailUrl) URL.revokeObjectURL(this.session.thumbnailUrl); + this.session.file = file; + this.session.thumbnailUrl = URL.createObjectURL(file); + + const wrap = document.getElementById('image-import-thumb-wrap'); + const img = document.getElementById('image-import-thumb'); + const nameEl = document.getElementById('image-import-thumb-name'); + const sizeEl = document.getElementById('image-import-thumb-size'); + if (img) img.src = this.session.thumbnailUrl; + if (nameEl) nameEl.textContent = file.name; + if (sizeEl) sizeEl.textContent = `${Math.round(file.size / 1024)} KB`; + if (wrap) wrap.hidden = false; + + const recognizeBtn = document.getElementById('image-import-recognize-btn'); + if (recognizeBtn) recognizeBtn.disabled = false; } } diff --git a/index.html b/index.html index 3d111d2..cec8175 100644 --- a/index.html +++ b/index.html @@ -209,6 +209,72 @@ + + +
diff --git a/styles.css b/styles.css index e268e00..d40b24b 100644 --- a/styles.css +++ b/styles.css @@ -550,3 +550,221 @@ header h1 { padding: 20px 0; } } + +/* ============================================================ + Bild-Import Modal (Feature A) + ============================================================ */ +.modal { + position: fixed; + inset: 0; + z-index: 1500; + display: flex; + align-items: center; + justify-content: center; +} + +.modal[hidden] { display: none; } + +.modal-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.5); +} + +.modal-content { + position: relative; + background: white; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + padding: 30px; + max-width: 800px; + width: 90%; + max-height: 90vh; + overflow-y: auto; +} + +.modal-close { + position: absolute; + top: 10px; + right: 10px; + width: 32px; + height: 32px; + border: none; + background: transparent; + font-size: 24px; + cursor: pointer; + color: #666; +} + +.modal-stage { + animation: fadeIn 0.2s ease; +} + +.modal-stage[hidden] { display: none; } + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + flex-wrap: wrap; +} + +.privacy-notice { + font-size: 0.875rem; + color: #6c757d; + margin-bottom: 15px; + font-style: italic; +} + +.drag-drop-zone { + border: 2px dashed #c0c0c8; + border-radius: 8px; + padding: 30px; + text-align: center; + transition: all 0.2s ease; + margin-bottom: 15px; +} + +.drag-drop-zone.drag-over { + border-color: #667eea; + background: rgba(102, 126, 234, 0.05); +} + +.drag-drop-zone p { + margin-bottom: 15px; + color: #555; +} + +.thumbnail-preview { + display: flex; + gap: 15px; + align-items: center; + padding: 15px; + background: #f8f9fa; + border-radius: 6px; + margin-bottom: 15px; +} + +.thumbnail-preview img { + max-width: 240px; + max-height: 240px; + border-radius: 4px; + border: 1px solid #e0e0e0; +} + +.thumbnail-meta { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.875rem; + color: #555; +} + +.spinner { + width: 48px; + height: 48px; + margin: 30px auto; + border: 4px solid #e0e0e0; + border-top-color: #667eea; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.unknown-names-box { + background: #fff3cd; + border-left: 4px solid #ffc107; + border-radius: 6px; + padding: 15px; + margin-bottom: 20px; +} + +.unknown-name-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + flex-wrap: wrap; +} + +.unknown-name-row:last-child { border-bottom: none; } + +.unknown-name-row .unknown-candidate { + font-weight: 600; + flex: 1; + min-width: 140px; +} + +.unknown-name-row select { + flex: 2; + min-width: 180px; + padding: 6px 10px; + border: 2px solid #e0e0e0; + border-radius: 4px; +} + +.unknown-name-row .fuzzy-hint { + flex-basis: 100%; + font-size: 0.8rem; + color: #856404; + padding-left: 4px; +} + +.preview-employee-group { + margin-bottom: 20px; + background: #f8f9fa; + border-radius: 6px; + padding: 15px; +} + +.preview-employee-group h3 { + color: #667eea; + margin-bottom: 10px; + font-size: 1.1rem; +} + +.preview-table { + width: 100%; + border-collapse: collapse; +} + +.preview-table th, +.preview-table td { + padding: 6px 8px; + border-bottom: 1px solid #e0e0e0; + text-align: left; + font-size: 0.9rem; +} + +.preview-table th { background: #ececf3; } + +.preview-row.outside-month { background: #fff0f0; } + +.preview-row .row-remove-btn { + background: transparent; + border: none; + cursor: pointer; + font-size: 1rem; +} + +.slot-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + color: white; +} + +.slot-badge.slot-fr { background: #fd7e14; } +.slot-badge.slot-sa { background: #dc3545; } +.slot-badge.slot-so { background: #dc3545; } +.slot-badge.slot-weekday { background: #6c757d; } + +.api-key-status-ok { color: #28a745; font-weight: 500; } +.api-key-status-none { color: #6c757d; font-style: italic; }