/* ============================================================ photo-upload.js — Reusable photo upload widget Shared by lapor.html (public reports) and index.html (households) ============================================================ */ 'use strict'; const PhotoUpload = { MAX_FILES: 5, MAX_MB: 5, /** * Validate a FileList against rules. * Returns { valid: true } or { valid: false, message: '...' } */ validate(fileList, existingCount = 0) { const allowed = ['image/jpeg', 'image/png']; const allowedExt = ['jpg', 'jpeg', 'png']; const files = Array.from(fileList); if (files.length === 0) return { valid: true }; if (existingCount + files.length > this.MAX_FILES) { return { valid: false, message: `Maksimal ${this.MAX_FILES} foto. Sudah ada ${existingCount}, pilih paling banyak ${this.MAX_FILES - existingCount} foto lagi.` }; } for (const f of files) { const ext = f.name.split('.').pop().toLowerCase(); if (!allowedExt.includes(ext)) { return { valid: false, message: `File "${f.name}" tidak diizinkan. Gunakan JPG atau PNG.` }; } if (!allowed.includes(f.type)) { return { valid: false, message: `File "${f.name}" bukan gambar yang valid.` }; } if (f.size > this.MAX_MB * 1024 * 1024) { return { valid: false, message: `File "${f.name}" melebihi batas ${this.MAX_MB} MB.` }; } } return { valid: true }; }, /** * Build a preview strip of thumbnails for a FileList. * Returns a DocumentFragment. */ buildPreviewStrip(fileList) { const frag = document.createDocumentFragment(); Array.from(fileList).forEach(file => { const url = URL.createObjectURL(file); const img = document.createElement('img'); img.src = url; img.className = 'photo-thumb'; img.alt = file.name; img.title = file.name; img.onload = () => URL.revokeObjectURL(url); frag.appendChild(img); }); return frag; }, /** * Build a preview strip from saved filenames (already uploaded). * baseUrl e.g. 'uploads/reports/' or 'uploads/houses/' */ buildSavedStrip(filenames, baseUrl, removable = false, onRemove = null) { const frag = document.createDocumentFragment(); if (!Array.isArray(filenames)) return frag; filenames.forEach(name => { const wrap = document.createElement('div'); wrap.style.cssText = 'position:relative;display:inline-block;'; const img = document.createElement('img'); img.src = baseUrl + name; img.className = 'photo-thumb'; img.alt = name; img.title = 'Klik untuk perbesar'; img.style.cursor = 'pointer'; img.addEventListener('click', () => PhotoUpload.lightbox(img.src)); wrap.appendChild(img); if (removable && onRemove) { const btn = document.createElement('button'); btn.type = 'button'; btn.innerHTML = '×'; btn.title = 'Hapus foto'; btn.style.cssText = 'position:absolute;top:2px;right:2px;width:18px;height:18px;border-radius:50%;border:none;background:rgba(0,0,0,0.6);color:#fff;font-size:12px;line-height:1;cursor:pointer;padding:0;'; btn.addEventListener('click', (e) => { e.stopPropagation(); onRemove(name, wrap); }); wrap.appendChild(btn); } frag.appendChild(wrap); }); return frag; }, /** Simple lightbox */ lightbox(src) { const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.88);z-index:99999;display:flex;align-items:center;justify-content:center;cursor:zoom-out;'; const img = document.createElement('img'); img.src = src; img.style.cssText = 'max-width:92vw;max-height:88vh;border-radius:8px;box-shadow:0 8px 40px rgba(0,0,0,.6);'; overlay.appendChild(img); overlay.addEventListener('click', () => document.body.removeChild(overlay)); document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { document.body.removeChild(overlay); document.removeEventListener('keydown', esc); } }); document.body.appendChild(overlay); }, /** * Upload photos to the upload endpoint after a record has been saved. * @param {string} target 'report' | 'house' * @param {number} id The record's database id * @param {FileList} fileList * @returns {Promise<{ok: boolean, data: object}>} */ async upload(target, id, fileList) { if (!fileList || fileList.length === 0) return { ok: true, data: { data: { all_photos: [] } } }; const form = new FormData(); Array.from(fileList).forEach(f => form.append('photos[]', f)); try { const res = await fetch(`api/public/upload.php?target=${target}&id=${id}`, { method: 'POST', body: form, // ⚠️ Do NOT set Content-Type header — browser sets it with boundary automatically }); const data = await res.json(); return { ok: res.ok, data }; } catch (err) { console.error('[PhotoUpload] Upload error:', err); return { ok: false, data: { success: false, message: 'Upload gagal: ' + err.message } }; } }, /** * Delete a single saved photo via API. * @param {string} target 'report' | 'house' * @param {number} id Record's database id * @param {string} filename The filename to delete * @returns {Promise<{ok: boolean, data: object}>} */ async deletePhoto(target, id, filename) { const endpointMap = { report: 'api/public/report.php', house: 'api/houses/index.php', }; const url = endpointMap[target]; if (!url) return { ok: false, data: { success: false, message: 'Target tidak valid.' } }; try { const res = await fetch(`${url}?action=delete_photo&id=${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename }), }); const data = await res.json(); return { ok: res.ok, data }; } catch (err) { console.error('[PhotoUpload] deletePhoto error:', err); return { ok: false, data: { success: false, message: 'Koneksi gagal: ' + err.message } }; } }, };