Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Jobs/Intake.cshtml
T
2026-04-23 21:38:24 -04:00

289 lines
13 KiB
Plaintext

@model (PowderCoating.Application.DTOs.Job.JobDto Job, PowderCoating.Application.DTOs.Job.IntakeJobDto Form)
@{
ViewData["Title"] = $"Part Intake — {Model.Job.JobNumber}";
ViewData["PageIcon"] = "bi-box-seam";
var job = Model.Job;
var form = Model.Form;
var expectedCount = (int)(ViewBag.ExpectedPartCount ?? 0);
var isReintake = job.IntakeDate.HasValue;
}
<div class="row justify-content-center">
<div class="col-lg-7 col-xl-6">
<!-- Header -->
<div class="d-flex justify-content-end mb-4">
<a asp-action="Details" asp-route-id="@job.Id" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back
</a>
</div>
@if (isReintake)
{
<div class="alert alert-warning alert-permanent mb-4">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Parts were previously checked in</strong> on @job.IntakeDate!.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d, yyyy h:mm tt")
@if (!string.IsNullOrEmpty(job.IntakeCheckedByName))
{
<span> by @job.IntakeCheckedByName</span>
}
. Submitting this form will update the intake record.
</div>
}
<!-- Job summary card -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-light fw-semibold">
<i class="bi bi-info-circle me-2"></i>Job Summary
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-6">
<small class="text-muted d-block">Status</small>
<span class="badge bg-@job.StatusColorClass fs-6">@job.StatusDisplayName</span>
</div>
<div class="col-6">
<small class="text-muted d-block">Expected Parts</small>
<span class="fs-4 fw-bold text-primary">@expectedCount</span>
</div>
@if (!string.IsNullOrEmpty(job.CustomerPO))
{
<div class="col-6">
<small class="text-muted d-block">Customer PO</small>
<span>@job.CustomerPO</span>
</div>
}
@if (job.DueDate.HasValue)
{
<div class="col-6">
<small class="text-muted d-block">Due Date</small>
<span class="@(job.DueDate < DateTime.Now ? "text-danger fw-semibold" : "")">
@job.DueDate.Value.ToString("MMM d, yyyy")
</span>
</div>
}
</div>
@if (job.Items.Any())
{
<hr class="my-3" />
<small class="text-muted d-block mb-2">Items</small>
<ul class="list-unstyled mb-0">
@foreach (var item in job.Items)
{
<li class="d-flex align-items-start gap-2 mb-1">
<span class="badge bg-secondary">×@item.Quantity</span>
<span class="small">@item.Description</span>
</li>
}
</ul>
}
@if (!string.IsNullOrEmpty(job.SpecialInstructions))
{
<hr class="my-3" />
<small class="text-muted d-block">Special Instructions</small>
<p class="mb-0 small text-warning-emphasis">
<i class="bi bi-exclamation-circle me-1"></i>@job.SpecialInstructions
</p>
}
</div>
</div>
<!-- Intake form -->
<form asp-action="Intake" asp-route-id="@job.Id" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="jobId" value="@form.JobId" />
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-light fw-semibold">
<i class="bi bi-clipboard-check me-2"></i>Check-In Details
</div>
<div class="card-body">
<!-- Part count -->
<div class="mb-4">
<label asp-for="Form.ActualPartCount" class="form-label fw-semibold">
Actual Part Count
@if (expectedCount > 0)
{
<span class="text-muted fw-normal ms-1">(expected: @expectedCount)</span>
}
</label>
<div class="input-group input-group-lg">
<span class="input-group-text"><i class="bi bi-hash"></i></span>
<input name="actualPartCount" class="form-control form-control-lg"
type="number" min="0" max="10000"
value="@(form.ActualPartCount.HasValue ? form.ActualPartCount.Value.ToString() : "")"
placeholder="@expectedCount" />
</div>
@if (expectedCount > 0)
{
<div id="countMismatchAlert" class="alert alert-warning mt-2 py-2 d-none">
<i class="bi bi-exclamation-triangle me-1"></i>
Count doesn't match expected — note the discrepancy in condition notes.
</div>
}
</div>
<!-- Condition notes -->
<div class="mb-4">
<label asp-for="Form.ConditionNotes" class="form-label fw-semibold">
Condition Notes
</label>
<textarea name="conditionNotes" class="form-control" rows="4"
placeholder="Describe the condition of parts at drop-off: scratches, rust, pre-existing damage, any missing pieces, special handling notes...">@form.ConditionNotes</textarea>
</div>
<!-- Advance status checkbox -->
@if (job.StatusCode != "IN_PREPARATION" && !job.StatusIsTerminal)
{
<div class="form-check form-switch">
<input type="hidden" name="advanceToInPreparation" value="false" />
<input type="checkbox" name="advanceToInPreparation" value="true" class="form-check-input" role="switch"
id="advanceSwitch" style="width:3em; height:1.5em;"
@(form.AdvanceToInPreparation ? "checked" : "") />
<label class="form-check-label ms-2" for="advanceSwitch">
Advance job to <strong>In Preparation</strong>
</label>
</div>
<small class="text-muted d-block mt-1 ms-5">
Uncheck if parts were dropped off but work hasn't started yet.
</small>
}
</div>
</div>
<!-- Before photos -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-light fw-semibold">
<i class="bi bi-camera me-2"></i>Before Photos
<span class="badge bg-secondary ms-1">Optional</span>
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Upload photos documenting the condition of parts at drop-off. These are saved as "Before" photos on the job.
</p>
<div id="photoDropZone"
class="border border-2 border-dashed rounded-3 p-4 text-center text-muted"
style="cursor:pointer; border-color: #dee2e6 !important;"
onclick="document.getElementById('photoInput').click()">
<i class="bi bi-cloud-upload fs-2 d-block mb-2"></i>
<span>Tap to add photos</span>
<small class="d-block mt-1">JPG, PNG up to 10 MB each</small>
</div>
<input type="file" id="photoInput" accept="image/jpeg,image/png,image/gif"
multiple class="d-none" />
<div id="photoPreviewArea" class="row g-2 mt-2"></div>
<div id="photoUploadStatus" class="mt-2"></div>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-circle me-2"></i>
@(isReintake ? "Update Intake Record" : "Complete Intake")
</button>
<a asp-action="Details" asp-route-id="@job.Id" class="btn btn-outline-secondary">
Cancel
</a>
</div>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
// Part count mismatch warning
const expectedCount = @expectedCount;
const countInput = document.querySelector('input[name="actualPartCount"]');
const mismatchAlert = document.getElementById('countMismatchAlert');
if (countInput && mismatchAlert && expectedCount > 0) {
countInput.addEventListener('input', function () {
const val = parseInt(this.value);
if (!isNaN(val) && val !== expectedCount) {
mismatchAlert.classList.remove('d-none');
} else {
mismatchAlert.classList.add('d-none');
}
});
}
// Before photo upload (reuses the existing /Jobs/UploadPhoto endpoint)
const photoInput = document.getElementById('photoInput');
const previewArea = document.getElementById('photoPreviewArea');
const statusDiv = document.getElementById('photoUploadStatus');
const jobId = @job.Id;
const dropZone = document.getElementById('photoDropZone');
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-primary'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('border-primary'));
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('border-primary');
handleFiles(e.dataTransfer.files);
});
photoInput.addEventListener('change', function () {
handleFiles(this.files);
this.value = '';
});
async function handleFiles(files) {
for (const file of files) {
if (!file.type.startsWith('image/')) continue;
await uploadPhoto(file);
}
}
async function uploadPhoto(file) {
const formData = new FormData();
formData.append('jobId', jobId);
formData.append('photo', file);
formData.append('caption', 'Intake — before');
formData.append('photoType', '0'); // JobPhotoType.Before = 0
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
try {
const resp = await fetch('/Jobs/UploadPhoto', {
method: 'POST',
headers: { 'RequestVerificationToken': token },
body: formData
});
const data = await resp.json();
if (data.success) {
addPreview(file, data.photoId);
} else {
showStatus('Upload failed: ' + (data.message ?? 'Unknown error'), 'danger');
}
} catch (e) {
showStatus('Upload error: ' + e.message, 'danger');
}
}
function addPreview(file, photoId) {
const reader = new FileReader();
reader.onload = e => {
const col = document.createElement('div');
col.className = 'col-4 col-sm-3 position-relative';
col.innerHTML = `
<img src="${e.target.result}" class="img-fluid rounded" style="aspect-ratio:1;object-fit:cover;" />
<span class="position-absolute top-0 end-0 badge bg-success m-1"><i class="bi bi-check"></i></span>`;
previewArea.appendChild(col);
};
reader.readAsDataURL(file);
}
function showStatus(msg, type) {
statusDiv.innerHTML = `<div class="alert alert-${type} alert-permanent py-2">${msg}</div>`;
}
</script>
}