Add LOT model mapping management, UI, and first-seen tracking
This commit is contained in:
207
internal/api/ui_lots.tmpl
Normal file
207
internal/api/ui_lots.tmpl
Normal file
@@ -0,0 +1,207 @@
|
||||
{{define "lots"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<body>
|
||||
{{template "topbar" .}}
|
||||
{{template "breadcrumbs" .}}
|
||||
|
||||
<main class="container">
|
||||
<section class="card">
|
||||
<h2>Create or Update Mapping</h2>
|
||||
<div class="grid cols-3">
|
||||
<div>
|
||||
<label for="modelInput">Model</label>
|
||||
<input id="modelInput" class="input" type="text" placeholder="e.g. ST12000NM0008" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="lotSelect">LOT (existing)</label>
|
||||
<select id="lotSelect" class="input">
|
||||
<option value="">Select LOT</option>
|
||||
{{range .Lots}}
|
||||
<option value="{{.Code}}">{{.Code}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lotCodeInput">LOT (manual code)</label>
|
||||
<input id="lotCodeInput" class="input" type="text" placeholder="e.g. LOT-HDD-12TB" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls" style="margin-top: 12px;">
|
||||
<button id="saveMapping" class="button" type="button">Save Mapping</button>
|
||||
</div>
|
||||
<div id="statusMessage" class="meta" style="margin-top: 12px;"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Current Mappings</h2>
|
||||
{{if .LotMappings}}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th>LOT code</th>
|
||||
<th>Updated</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LotMappings}}
|
||||
<tr>
|
||||
<td><button class="button ghost" type="button" onclick='openEditModal({{printf "%q" .Model}}, {{printf "%q" .LotCode}})'>{{.Model}}</button></td>
|
||||
<td><button class="button ghost" type="button" onclick='openEditModal({{printf "%q" .Model}}, {{printf "%q" .LotCode}})'>{{.LotCode}}</button></td>
|
||||
<td title="{{formatTimeFull .UpdatedAt}}">{{formatTime .UpdatedAt}}</td>
|
||||
<td>
|
||||
<button class="button ghost" type="button" onclick='openEditModal({{printf "%q" .Model}}, {{printf "%q" .LotCode}})'>Edit</button>
|
||||
<button class="button ghost" type="button" onclick='deleteMapping({{printf "%q" .Model}})'>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="meta">No mappings yet.</div>
|
||||
{{end}}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="editModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.35); z-index:1000;">
|
||||
<div class="card" style="max-width:980px; margin:60px auto; position:relative;">
|
||||
<h2>Edit Mapping</h2>
|
||||
<div class="grid cols-2">
|
||||
<div>
|
||||
<label for="modalModelInput">Model</label>
|
||||
<input id="modalModelInput" class="input" type="text" style="width:100%; min-height:52px; font-size:28px; line-height:1.2;" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="modalLotCodeInput">LOT code</label>
|
||||
<input id="modalLotCodeInput" class="input" type="text" style="width:100%; min-height:52px; font-size:28px; line-height:1.2;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls" style="margin-top:12px;">
|
||||
<button class="button" type="button" onclick="saveModalMapping()">Save</button>
|
||||
<button class="button ghost" type="button" onclick="closeEditModal()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let editingOriginalModel = "";
|
||||
function normalizeValue(value) {
|
||||
const trimmed = (value || "").trim();
|
||||
if (trimmed.length >= 2) {
|
||||
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
||||
return trimmed.slice(1, -1).trim();
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function mappingModel() {
|
||||
return normalizeValue(document.getElementById("modelInput").value);
|
||||
}
|
||||
|
||||
function mappingLotCode() {
|
||||
const manual = normalizeValue(document.getElementById("lotCodeInput").value);
|
||||
if (manual !== "") return manual;
|
||||
return normalizeValue(document.getElementById("lotSelect").value);
|
||||
}
|
||||
|
||||
function setStatus(text, isError) {
|
||||
const el = document.getElementById("statusMessage");
|
||||
el.textContent = text;
|
||||
el.style.color = isError ? "#c81d25" : "#4f772d";
|
||||
}
|
||||
|
||||
async function saveMapping() {
|
||||
const model = mappingModel();
|
||||
const lotCode = mappingLotCode();
|
||||
if (!model) {
|
||||
setStatus("Model is required.", true);
|
||||
return;
|
||||
}
|
||||
if (!lotCode) {
|
||||
setStatus("LOT code is required.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/lot-mappings/" + encodeURIComponent(model), {
|
||||
method: "PUT",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({lot_code: lotCode})
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
setStatus(payload.error || "Failed to save mapping.", true);
|
||||
return;
|
||||
}
|
||||
setStatus("Saved. Updated parts: " + (payload.affected_parts_count || 0), false);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function openEditModal(model, lotCode) {
|
||||
editingOriginalModel = normalizeValue(model);
|
||||
document.getElementById("modalModelInput").value = normalizeValue(model);
|
||||
document.getElementById("modalLotCodeInput").value = normalizeValue(lotCode);
|
||||
document.getElementById("editModal").style.display = "block";
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById("editModal").style.display = "none";
|
||||
editingOriginalModel = "";
|
||||
}
|
||||
|
||||
async function saveModalMapping() {
|
||||
const nextModel = normalizeValue(document.getElementById("modalModelInput").value);
|
||||
const nextLotCode = normalizeValue(document.getElementById("modalLotCodeInput").value);
|
||||
if (!nextModel || !nextLotCode) {
|
||||
alert("Model and LOT code are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const putResp = await fetch("/lot-mappings/" + encodeURIComponent(nextModel), {
|
||||
method: "PUT",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({lot_code: nextLotCode})
|
||||
});
|
||||
const putPayload = await putResp.json().catch(() => ({}));
|
||||
if (!putResp.ok) {
|
||||
alert(putPayload.error || "Failed to save mapping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingOriginalModel && editingOriginalModel !== nextModel) {
|
||||
const delResp = await fetch("/lot-mappings/" + encodeURIComponent(editingOriginalModel), {
|
||||
method: "DELETE"
|
||||
});
|
||||
if (!delResp.ok) {
|
||||
const delPayload = await delResp.json().catch(() => ({}));
|
||||
alert(delPayload.error || "Failed to replace old mapping.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
closeEditModal();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
async function deleteMapping(model) {
|
||||
if (!confirm("Delete mapping for model '" + model + "'?")) {
|
||||
return;
|
||||
}
|
||||
const response = await fetch("/lot-mappings/" + encodeURIComponent(model), {method: "DELETE"});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
setStatus(payload.error || "Failed to delete mapping.", true);
|
||||
return;
|
||||
}
|
||||
setStatus("Deleted. Updated parts: " + (payload.affected_parts_count || 0), false);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
document.getElementById("saveMapping").addEventListener("click", saveMapping);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user