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:
+102
-1
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user