Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Inventory/Scan.cshtml
T
2026-04-29 20:26:39 -04:00

418 lines
16 KiB
Plaintext

@using PowderCoating.Application.DTOs.Inventory
@using PowderCoating.Web.Controllers
@{
var item = ViewBag.ItemDto as InventoryItemDto;
var myJobs = ViewBag.MyJobs as List<ScanJobOption> ?? new();
var otherJobs = ViewBag.OtherJobs as List<ScanJobOption> ?? new();
var preselectedJobId = ViewBag.PreselectedJobId as int?;
var scanError = ViewBag.ScanError as string;
ViewData["Title"] = $"Log Usage — {item?.Name}";
Layout = null; // mobile-first standalone page
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>Log Usage — @item?.Name</title>
<style>
:root {
--purple: #6f42c1;
--purple-dark: #5a32a3;
--danger: #dc3545;
--success: #198754;
--muted: #6c757d;
--border: #dee2e6;
--bg: #f8f9fa;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
background: var(--bg);
min-height: 100vh;
padding-bottom: 32px;
}
/* ── Header ──────────────────────────────────── */
.page-header {
background: var(--purple);
color: #fff;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.page-header .logo { font-weight: 700; font-size: 13px; letter-spacing: .05em; opacity: .85; }
.page-header h1 { font-size: 18px; font-weight: 700; line-height: 1.2; }
.page-header .sub { font-size: 13px; opacity: .85; }
/* ── Item card ───────────────────────────────── */
.item-card {
background: #fff;
margin: 16px;
border-radius: 12px;
padding: 16px;
box-shadow: 0 1px 4px rgba(0,0,0,.1);
display: flex;
align-items: center;
gap: 14px;
}
.item-qr img { width: 70px; height: 70px; border-radius: 6px; border: 1px solid var(--border); }
.item-info .item-name { font-size: 16px; font-weight: 700; }
.item-info .item-sku { font-size: 13px; color: var(--muted); margin-top: 2px; }
.item-info .item-stock {
margin-top: 6px;
font-size: 13px;
font-weight: 600;
}
.stock-ok { color: var(--success); }
.stock-low { color: var(--danger); }
.stock-zero { color: #343a40; }
/* ── Form card ───────────────────────────────── */
.form-card {
background: #fff;
margin: 0 16px 16px;
border-radius: 12px;
padding: 20px 16px;
box-shadow: 0 1px 4px rgba(0,0,0,.1);
}
.form-card h2 { font-size: 15px; font-weight: 700; margin-bottom: 16px; color: #333; }
.field { margin-bottom: 18px; }
.field label {
display: block;
font-size: 13px;
font-weight: 600;
color: #444;
margin-bottom: 6px;
}
.field label .req { color: var(--danger); }
.field input[type=number],
.field select,
.field textarea {
width: 100%;
padding: 12px 14px;
border: 1.5px solid var(--border);
border-radius: 8px;
font-size: 16px; /* prevents iOS zoom */
background: #fff;
appearance: none;
-webkit-appearance: none;
}
.field input[type=number]:focus,
.field select:focus,
.field textarea:focus {
outline: none;
border-color: var(--purple);
box-shadow: 0 0 0 3px rgba(111,66,193,.15);
}
/* ── Job picker ──────────────────────────────── */
.job-tabs { display: flex; gap: 8px; margin-bottom: 12px; }
.job-tab {
flex: 1;
padding: 8px;
border: 1.5px solid var(--border);
border-radius: 8px;
background: #fff;
font-size: 13px;
font-weight: 600;
color: var(--muted);
cursor: pointer;
text-align: center;
}
.job-tab.active { border-color: var(--purple); color: var(--purple); background: #f3effe; }
.job-list { display: flex; flex-direction: column; gap: 8px; max-height: 220px; overflow-y: auto; }
.job-option {
padding: 10px 12px;
border: 1.5px solid var(--border);
border-radius: 8px;
cursor: pointer;
background: #fff;
display: flex;
flex-direction: column;
gap: 2px;
}
.job-option:hover, .job-option.selected { border-color: var(--purple); background: #f3effe; }
.job-option .jn { font-size: 14px; font-weight: 700; }
.job-option .cn { font-size: 12px; color: var(--muted); }
.no-job-opt {
padding: 10px 12px;
border: 1.5px dashed var(--border);
border-radius: 8px;
cursor: pointer;
background: #fff;
font-size: 13px;
color: var(--muted);
text-align: center;
}
.no-job-opt.selected { border-color: var(--muted); background: #f8f9fa; color: #333; }
#jobIdInput { display: none; }
/* ── Reason pills ────────────────────────────── */
.reason-pills { display: flex; flex-wrap: wrap; gap: 8px; }
.reason-pill {
padding: 8px 14px;
border: 1.5px solid var(--border);
border-radius: 20px;
font-size: 13px;
cursor: pointer;
background: #fff;
color: #444;
white-space: nowrap;
}
.reason-pill.selected { border-color: var(--purple); background: #f3effe; color: var(--purple); font-weight: 600; }
/* ── Submit / Cancel ─────────────────────────── */
.btn-submit {
width: 100%;
padding: 16px;
background: var(--purple);
color: #fff;
border: none;
border-radius: 10px;
font-size: 17px;
font-weight: 700;
cursor: pointer;
margin-top: 4px;
}
.btn-submit:disabled { opacity: .6; }
.btn-submit:active { background: var(--purple-dark); }
.btn-cancel {
display: block;
width: 100%;
padding: 14px;
background: #fff;
color: var(--muted);
border: 1.5px solid var(--border);
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-top: 10px;
text-align: center;
text-decoration: none;
}
.btn-cancel:active { background: #f0f0f0; }
.error-banner {
margin: 0 16px 16px;
background: #f8d7da;
color: #842029;
border: 1px solid #f5c2c7;
border-radius: 8px;
padding: 12px 14px;
font-size: 14px;
}
.hint { font-size: 12px; color: var(--muted); margin-top: 4px; }
</style>
</head>
<body>
<div class="page-header">
<div>
<div class="logo">Powder Coating Logix</div>
<h1>Log Usage</h1>
<div class="sub">Record powder used from inventory</div>
</div>
</div>
@if (!string.IsNullOrEmpty(scanError))
{
<div class="error-banner">⚠ @scanError</div>
}
<!-- Item Info -->
<div class="item-card">
<div class="item-qr">
<img src="/Inventory/QrCode/@item!.Id?size=4" alt="QR" />
</div>
<div class="item-info">
<div class="item-name">@item.Name</div>
<div class="item-sku">@item.SKU</div>
@if (!string.IsNullOrEmpty(item.ColorName))
{
<div class="item-sku">@item.ColorName@(item.Finish != null ? " · " + item.Finish : "")</div>
}
<div class="item-stock @(item.IsOutOfStock ? "stock-zero" : item.IsLowStock ? "stock-low" : "stock-ok")">
@item.QuantityOnHand.ToString("N2") @item.UnitOfMeasure on hand
@if (item.IsOutOfStock) { <span>· Out of Stock</span> }
else if (item.IsLowStock) { <span>· Low Stock</span> }
</div>
</div>
</div>
<!-- Usage Form -->
<form method="post" action="/Inventory/LogUsage" id="usageForm">
@Html.AntiForgeryToken()
<input type="hidden" name="inventoryItemId" value="@item.Id" />
<input type="hidden" name="jobId" id="jobIdInput" />
<input type="hidden" name="transactionType" id="transactionTypeInput" value="Adjustment" />
<div class="form-card">
<h2>1. Select Job (optional)</h2>
@if (myJobs.Any() || otherJobs.Any())
{
<div class="job-tabs">
@if (myJobs.Any())
{
<div class="job-tab active" id="tabMine" onclick="showTab('mine')">My Jobs (@myJobs.Count)</div>
}
@if (otherJobs.Any())
{
<div class="job-tab @(!myJobs.Any() ? "active" : "")" id="tabOther" onclick="showTab('other')">Other Jobs</div>
}
<div class="job-tab" id="tabNone" onclick="showTab('none')">No Job</div>
</div>
<div id="listMine" class="job-list" style="@(!myJobs.Any() ? "display:none" : "")">
@foreach (var j in myJobs)
{
<div class="job-option @(preselectedJobId == j.Id ? "selected" : "")"
data-jobid="@j.Id" onclick="selectJob(this)">
<span class="jn">@j.JobNumber</span>
<span class="cn">@j.CustomerName</span>
</div>
}
</div>
<div id="listOther" class="job-list" style="display:none">
@foreach (var j in otherJobs)
{
<div class="job-option" data-jobid="@j.Id" onclick="selectJob(this)">
<span class="jn">@j.JobNumber</span>
<span class="cn">@j.CustomerName</span>
</div>
}
</div>
<div id="listNone" style="display:none">
<div class="no-job-opt selected" onclick="selectNoJob(this)">
No job — log as general usage
</div>
</div>
}
else
{
<p style="font-size:13px;color:var(--muted)">No active jobs found. Usage will be logged without a job reference.</p>
}
<div class="hint" id="jobHint" style="margin-top:8px"></div>
</div>
<div class="form-card">
<h2>2. Enter Quantity</h2>
<div class="field">
<label for="quantityInput">Amount Used (@item.UnitOfMeasure) <span class="req">*</span></label>
<input type="number" id="quantityInput" name="quantity"
min="0" step="any" required placeholder="0" inputmode="decimal" />
<div class="hint" id="balanceHint"></div>
</div>
</div>
<div class="form-card">
<h2>3. Reason</h2>
<div class="field">
<div class="reason-pills">
<div class="reason-pill selected" data-val="JobUsage" onclick="selectReason(this)">Job Usage</div>
<div class="reason-pill" data-val="Waste" onclick="selectReason(this)">Waste / Spillage</div>
<div class="reason-pill" data-val="Adjustment" onclick="selectReason(this)">Correction</div>
<div class="reason-pill" data-val="Transfer" onclick="selectReason(this)">Transfer Out</div>
</div>
</div>
<div class="field">
<label for="notesInput">Notes <span style="font-weight:400;color:var(--muted)">(optional)</span></label>
<textarea id="notesInput" name="notes" rows="2"
placeholder="Any additional details…" maxlength="500"
style="font-size:16px;resize:none"></textarea>
</div>
</div>
<div style="margin: 0 16px 8px">
<button type="submit" class="btn-submit" id="submitBtn">
Save Usage Log
</button>
<a href="/Inventory/Details/@item!.Id" class="btn-cancel">Cancel</a>
</div>
</form>
<script>
var currentQty = @item.QuantityOnHand;
var uom = '@item.UnitOfMeasure';
// ── Job selection ────────────────────────────────
function showTab(tab) {
['mine','other','none'].forEach(function(t) {
var list = document.getElementById('list' + t.charAt(0).toUpperCase() + t.slice(1));
var tabEl = document.getElementById('tab' + t.charAt(0).toUpperCase() + t.slice(1));
if (list) list.style.display = t === tab ? '' : 'none';
if (tabEl) tabEl.classList.toggle('active', t === tab);
});
if (tab === 'none') {
document.getElementById('jobIdInput').value = '';
document.getElementById('jobHint').textContent = 'Usage will be logged without a job reference.';
} else {
document.getElementById('jobIdInput').value = '';
document.getElementById('jobHint').textContent = '';
}
}
function selectJob(el) {
document.querySelectorAll('.job-option').forEach(function(e) { e.classList.remove('selected'); });
el.classList.add('selected');
document.getElementById('jobIdInput').value = el.dataset.jobid;
document.getElementById('jobHint').textContent = 'Job selected: ' + el.querySelector('.jn').textContent;
}
function selectNoJob(el) {
document.querySelectorAll('.job-option').forEach(function(e) { e.classList.remove('selected'); });
document.getElementById('jobIdInput').value = '';
document.getElementById('jobHint').textContent = 'Usage will be logged without a job reference.';
}
// ── Reason selection ─────────────────────────────
function selectReason(el) {
document.querySelectorAll('.reason-pill').forEach(function(e) { e.classList.remove('selected'); });
el.classList.add('selected');
document.getElementById('transactionTypeInput').value = el.dataset.val;
}
// ── Balance hint ─────────────────────────────────
document.getElementById('quantityInput').addEventListener('input', function() {
var qty = parseFloat(this.value) || 0;
if (!this.value) { document.getElementById('balanceHint').textContent = ''; return; }
var newBal = currentQty - qty;
var col = newBal < 0 ? 'var(--danger)' : newBal === 0 ? '#343a40' : 'var(--success)';
document.getElementById('balanceHint').innerHTML =
'New balance: <strong style="color:' + col + '">' + newBal.toFixed(2) + ' ' + uom + '</strong>';
});
// ── Preselect job if coming from success page ────
@if (preselectedJobId.HasValue)
{
<text>
(function() {
var preId = '@preselectedJobId';
var el = document.querySelector('[data-jobid="' + preId + '"]');
if (el) { selectJob(el); el.scrollIntoView({block:'nearest'}); }
}());
</text>
}
// ── Submit spinner ───────────────────────────────
document.getElementById('usageForm').addEventListener('submit', function() {
var btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.textContent = 'Saving…';
});
</script>
</body>
</html>