printedparts stage 17: gage-lab asset tag + print-files redesign
Some checks failed
CI / backend (push) Successful in 1m45s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s

The gage lab assigns real WJRP asset numbers, so identity splits: the
internal itemcode stays auto-minted and a new optional unique
gagelabtag (migration 0003) carries the lab's number - settable on
create/edit, searchable, and resolved by the kiosk for scans and bare
keypad digits against the numeric tail of either identifier
(unique-match only). The print-files table becomes stacked revision
cards - filename with rev/current badges, one meta line, delete pinned
right - ending the horizontal scroll in that column.
This commit is contained in:
cproudlock
2026-07-17 14:01:09 -04:00
parent 6160a5142a
commit 3a3dff285e
8 changed files with 185 additions and 52 deletions

View File

@@ -47,6 +47,10 @@
<span class="info-label">Item code</span>
<span class="info-value">{{ item.itemcode }}</span>
</div>
<div class="info-row" v-if="item.gagelabtag">
<span class="info-label">Gage lab tag</span>
<span class="info-value">{{ item.gagelabtag }}</span>
</div>
<div class="info-row">
<span class="info-label">Bin location</span>
<span class="info-value">{{ item.binlocation || '-' }}</span>
@@ -81,42 +85,30 @@
</button>
</div>
<div v-if="fileError" class="error-message">{{ fileError }}</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Rev</th>
<th>File</th>
<th>Size</th>
<th>By</th>
<th>Note</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="revision in files" :key="revision.fileid"
:class="{ 'current-revision': revision === files[0] }">
<td>{{ revision.revision }}</td>
<td>
<a :href="withBase(`/api/printedparts/files/${revision.fileid}/download`)">
{{ revision.filename }}
</a>
<span v-if="revision === files[0]" class="badge badge-success">current</span>
</td>
<td>{{ formatSize(revision.filesize) }}</td>
<td :title="revision.uploadeddate">{{ revision.uploadedby }}</td>
<td>{{ revision.uploadnote || '-' }}</td>
<td class="actions">
<button class="btn btn-secondary btn-sm"
@click="removeRevision(revision)">Delete</button>
</td>
</tr>
<tr v-if="files.length === 0">
<td colspan="6" class="empty-state">No print file uploaded yet</td>
</tr>
</tbody>
</table>
</div>
<ul class="file-revision-list">
<li v-for="revision in files" :key="revision.fileid"
:class="{ 'current-revision': revision === files[0] }">
<div class="file-main">
<a :href="withBase(`/api/printedparts/files/${revision.fileid}/download`)"
class="file-name">
{{ revision.filename }}
</a>
<span class="badge badge-secondary">rev {{ revision.revision }}</span>
<span v-if="revision === files[0]" class="badge badge-success">current</span>
</div>
<div class="file-meta">
{{ formatSize(revision.filesize) }} -
{{ revision.uploadedby }} -
{{ formatDate(revision.uploadeddate) }}
<span v-if="revision.uploadnote"> - {{ revision.uploadnote }}</span>
</div>
<button class="btn btn-secondary btn-sm file-delete"
@click="removeRevision(revision)">Delete</button>
</li>
<li v-if="files.length === 0" class="empty-state">
No print file uploaded yet
</li>
</ul>
</div>
<div class="section-card">
@@ -344,6 +336,40 @@ function formatDate(value) {
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.current-revision td { font-weight: 600; }
.file-revision-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.file-revision-list li {
position: relative;
border: 1px solid var(--border);
border-radius: 0.45rem;
padding: 0.6rem 5.5rem 0.6rem 0.8rem;
}
.file-revision-list li.current-revision { border-color: var(--primary); }
.file-main {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.file-name {
font-weight: 600;
overflow-wrap: anywhere;
}
.file-meta {
color: var(--text-light);
font-size: 0.85rem;
margin-top: 0.2rem;
}
.file-delete {
position: absolute;
top: 0.55rem;
right: 0.6rem;
}
.qty-in { color: var(--success); }
</style>

View File

@@ -32,9 +32,14 @@
<input v-model.number="form.lowstockthreshold" type="number" min="0"
class="form-control" />
</div>
<div v-if="isEdit" class="form-group">
<label>Item code</label>
<input :value="itemcode" type="text" class="form-control" disabled />
<div class="form-group">
<label>Gage lab asset tag</label>
<input v-model="form.gagelabtag" type="text" class="form-control"
placeholder="e.g. WJRP0117" />
<p class="form-hint">
Assigned by the gage lab; optional. The kiosk finds parts by
this tag or by the internal code.
</p>
</div>
</div>
@@ -81,12 +86,12 @@ const cancelTarget = computed(() =>
const form = ref({
itemname: '',
gagelabtag: '',
itemdescription: '',
lowstockthreshold: null,
binlocation: '',
printnotes: ''
})
const itemcode = ref('')
const imageurl = ref(null)
const saving = ref(false)
const error = ref('')
@@ -99,7 +104,6 @@ onMounted(async () => {
for (const key of Object.keys(form.value)) {
form.value[key] = item[key]
}
itemcode.value = item.itemcode
imageurl.value = item.imageurl
} catch (loadError) {
error.value = 'Could not load the item'
@@ -115,6 +119,7 @@ async function save() {
if (payload.lowstockthreshold === null || payload.lowstockthreshold === '') {
delete payload.lowstockthreshold
}
if (!payload.gagelabtag) payload.gagelabtag = ''
let printeditemid
if (isEdit.value) {
await printedpartsApi.update(route.params.id, payload)