feat: inline machine ID editing in Previous Photos

Tap any machine ID in the Previous Photos list to edit it inline.
- Replaces static text with editable input on click/tap
- Enter/blur saves via /api/assign-machine-id
- Esc restores original value
- Shows ✏️ indicator on hover
- Saves new ID to DB, refreshes asset info and badge instantly
This commit is contained in:
2026-05-25 20:55:07 -04:00
parent db45b386fa
commit 6c54912cfc
+102 -1
View File
@@ -278,6 +278,21 @@ if ('serviceWorker' in navigator) {
border-radius: var(--radius-sm); margin-bottom: 8px;
cursor: zoom-in; display: block; background: var(--card2);
}
/* Inline machine ID editing in Previous Photos */
.prev-mid-editable {
cursor: text; border-bottom: 1px dashed var(--text3);
transition: border-color 0.15s, color 0.15s;
padding: 0 2px;
}
.prev-mid-editable:hover { border-color: var(--accent); color: var(--text); }
.prev-mid-editable::after { content: ' ✏️'; font-size: 9px; opacity: 0.5; }
.prev-mid-editable:hover::after { opacity: 1; }
.prev-mid-input {
background: var(--card2); color: var(--text); border: 1px solid var(--accent);
border-radius: 6px; padding: 2px 6px; font-size: 11px; font-weight: 700;
width: 100px; outline: none;
}
.prev-mid-input:focus { border-color: var(--accent2); box-shadow: 0 0 0 2px rgba(59,130,246,0.3); }
.export-modal-backdrop { position: fixed; inset: 0; z-index: 999; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; }
.export-modal { background: var(--card); border-radius: var(--radius); padding: 20px; max-width: 320px; width: 90%; text-align: center; }
@@ -568,7 +583,7 @@ function renderPrevCard(p) {
<span class="prev-time">${time}</span>
</div>
<div class="prev-details">
Machine: <strong>${p.machine_id ? esc(p.machine_id) : '—'}</strong>
Machine: <span id="midSpan${p.id}" class="prev-mid-editable" data-mid="${esc(p.machine_id || '')}" onclick="editMachineIdInline(${p.id})">${esc(p.machine_id || '—')}</span>
<span class="match-badge ${hasMatch}">${esc(matchText)}</span>
<span class="engine-badge ${engCls}">${esc(engine)}${p.ocr_model ? ' ' + esc(p.ocr_model) : ''}</span>
${colorHtml}
@@ -603,6 +618,92 @@ function renderPrevCard(p) {
</div>`;
}
/* ── Inline machine ID editing in Previous Photos ── */
function editMachineIdInline(photoId) {
const span = document.getElementById('midSpan' + photoId);
if (!span) return;
const currentVal = span.dataset.mid || '';
const input = document.createElement('input');
input.type = 'text';
input.className = 'prev-mid-input';
input.value = currentVal;
input.placeholder = 'Enter machine ID';
input.dataset.photoId = photoId;
input.dataset.original = currentVal;
span.replaceWith(input);
input.focus();
input.select();
input.addEventListener('blur', () => saveMachineIdInline(input, photoId));
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') input.blur();
if (e.key === 'Escape') { saveMachineIdInline(input, photoId, true); }
});
}
function saveMachineIdInline(input, photoId, cancel) {
const orig = input.dataset.original || '';
const val = cancel ? orig : input.value.trim();
// Restore span first so UI is consistent
function restoreSpan(mid, label) {
const span = document.createElement('span');
span.id = 'midSpan' + photoId;
span.className = 'prev-mid-editable';
span.dataset.mid = mid || '';
span.textContent = label || mid || '—';
span.onclick = () => editMachineIdInline(photoId);
input.replaceWith(span);
return span;
}
if (cancel || val === orig) {
restoreSpan(orig, orig || '—');
return;
}
fetch('/api/assign-machine-id', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ photo_id: photoId, machine_id: val })
})
.then(r => r.json())
.then(data => {
restoreSpan(val, val);
// Update the badge
const card = document.getElementById('midSpan' + photoId)?.closest('.prev-photo-card');
if (card) {
const badge = card.querySelector('.match-badge');
if (badge) {
badge.textContent = val;
badge.className = 'match-badge matched';
}
}
// Update asset info
const assetEl = document.getElementById('prevAsset' + photoId);
const infoEl = document.getElementById('prevMachineInfo' + photoId);
if (data.asset) {
const a = data.asset;
if (assetEl) {
assetEl.innerHTML = '🆔 ' + esc(a.machine_id) + ' · <strong>' + esc(a.name) + '</strong>' +
(a.category ? ' · 📦 ' + esc(a.category) : '') +
(a.status === 'active' ? ' 🟢 Active' : '');
}
if (infoEl) {
const parts = [a.location_name, a.building, a.floor, a.zone].filter(Boolean);
infoEl.innerHTML = parts.join(' · ') || '(No location details)';
}
} else {
if (assetEl) assetEl.innerHTML = '⚠️ No asset match for <strong>' + esc(val) + '</strong>';
}
})
.catch(() => {
restoreSpan(orig, orig || '—');
// Show error toast
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:60px;left:50%;transform:translateX(-50%);background:var(--red);color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;z-index:9999;animation:fadeIn 0.2s;';
toast.textContent = 'Failed to save machine ID';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
});
}
async function resetPhoto(photoId) {
if (!confirm('Reset this entry and re-process from scratch?')) return;