Add photo management for models and employees; fix stale detail navigation
Some checks failed
CI / backend (push) Failing after 9s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

Model photos: upload/replace/delete on /api/models/<id>/image (admin),
stored under instance/modelimages/ with a public serve route; thumbnail
plus Upload/Replace/Remove controls in the Models settings modal; the
URL field remains as a manual alternative.

Employee photos, mode-aware: self-hosted directory employees get
upload/replace/delete (photo-<sso> under instance/employeephotos/,
employees plugin migration 0002); external directory mode passes the
HR-supplied picture URL through read-only (writes 409). One resolver
feeds both consumers - the shopfloor recognition/recert kiosk cards and
the employee detail hero - in either mode.

Navigation fix: router-view is keyed on route path, so following a
relationship link between two assets of the same type (machine ->
dualpath machine) reloads the page instead of showing stale content;
query-only URL changes still avoid a remount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-11 21:00:37 -04:00
parent 7dae281993
commit 1d21bf0206
19 changed files with 926 additions and 29 deletions

View File

@@ -133,6 +133,29 @@
/>
</div>
<div class="form-group">
<label>Image</label>
<div class="image-manage">
<img v-if="form.imageurl" :src="form.imageurl" alt="Model image" class="image-thumb" />
<div class="image-actions">
<template v-if="editingModel">
<input
ref="imageFileInput"
type="file"
accept=".png,.jpg,.jpeg,.gif,.webp,.svg"
style="display: none"
@change="onImageSelected"
/>
<button type="button" class="btn btn-secondary btn-sm" :disabled="uploadingImage" @click="triggerImageUpload">
{{ uploadingImage ? 'Uploading...' : (form.imageurl ? 'Replace' : 'Upload') }}
</button>
<button v-if="form.imageurl" type="button" class="btn btn-danger btn-sm" @click="removeImage">Remove</button>
</template>
<small v-else class="text-muted">Save the model first, then upload a photo.</small>
</div>
</div>
</div>
<div class="form-group">
<label for="imageurl">Image URL</label>
<input
@@ -142,6 +165,7 @@
class="form-control"
placeholder="https://..."
/>
<small class="text-muted">Manual alternative. Uploading a photo overwrites this URL.</small>
</div>
<div class="form-group">
@@ -205,6 +229,9 @@ const error = ref('')
const showDeleteModal = ref(false)
const modelToDelete = ref(null)
const imageFileInput = ref(null)
const uploadingImage = ref(false)
const form = ref({
modelnumber: '',
vendorid: '',
@@ -335,6 +362,43 @@ async function saveModel() {
}
}
function triggerImageUpload() {
imageFileInput.value?.click()
}
async function onImageSelected(event) {
const file = event.target.files?.[0]
if (!file || !editingModel.value) return
uploadingImage.value = true
try {
const response = await modelsApi.uploadImage(editingModel.value.modelnumberid, file)
// Backend returns the updated model with imageurl set to the served URL.
form.value.imageurl = response.data.data.imageurl || ''
toast.success('Image uploaded')
loadModels()
} catch (err) {
console.error('Error uploading image:', err)
toast.error(apiError(err, 'Failed to upload image'))
} finally {
uploadingImage.value = false
if (imageFileInput.value) imageFileInput.value.value = ''
}
}
async function removeImage() {
if (!editingModel.value) return
if (!confirm('Remove this model image?')) return
try {
await modelsApi.removeImage(editingModel.value.modelnumberid)
form.value.imageurl = ''
toast.success('Image removed')
loadModels()
} catch (err) {
console.error('Error removing image:', err)
toast.error(apiError(err, 'Failed to remove image'))
}
}
function confirmDelete(m) {
modelToDelete.value = m
showDeleteModal.value = true
@@ -368,4 +432,25 @@ async function deleteModel() {
color: var(--text-light);
font-size: 0.85rem;
}
.image-manage {
display: flex;
align-items: center;
gap: 1rem;
}
.image-thumb {
width: 80px;
height: 80px;
object-fit: contain;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
}
.image-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>