diff --git a/src/PowderCoating.Web/Controllers/JobsController.cs b/src/PowderCoating.Web/Controllers/JobsController.cs index bf5fe07..2ec0741 100644 --- a/src/PowderCoating.Web/Controllers/JobsController.cs +++ b/src/PowderCoating.Web/Controllers/JobsController.cs @@ -2643,6 +2643,22 @@ public class JobsController : Controller #region Job Completion + /// + /// Returns the Mark Complete modal partial view populated with full job data. + /// Called via AJAX from the Job Board when a card is dragged to the Completed column. + /// + [HttpGet] + public async Task CompleteJobModal(int id) + { + var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id); + if (job == null) return NotFound(); + var currentUser = await _userManager.GetUserAsync(User); + if (currentUser != null) + await PopulateEmailNotificationDefaultsAsync(currentUser.CompanyId); + var dto = _mapper.Map(job); + return PartialView("_CompleteJobModal", dto); + } + /// /// Marks a job as completed, recording actual time spent and final price adjustments. /// Updates the CompletedDate, ActualTimeSpentHours, and FinalPrice fields, transitions to diff --git a/src/PowderCoating.Web/Views/Jobs/Board.cshtml b/src/PowderCoating.Web/Views/Jobs/Board.cshtml index d44afcb..c729707 100644 --- a/src/PowderCoating.Web/Views/Jobs/Board.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/Board.cshtml @@ -404,6 +404,9 @@ + +
+ @section Scripts { } diff --git a/src/PowderCoating.Web/Views/Jobs/Details.cshtml b/src/PowderCoating.Web/Views/Jobs/Details.cshtml index 771ae98..99e7c91 100644 --- a/src/PowderCoating.Web/Views/Jobs/Details.cshtml +++ b/src/PowderCoating.Web/Views/Jobs/Details.cshtml @@ -1726,154 +1726,7 @@ - +@await Html.PartialAsync("_CompleteJobModal", Model) @if ((bool)(ViewBag.IsAdminOrManager ?? false) && (bool)(ViewBag.SmsEnabled ?? false)) @@ -2440,12 +2293,6 @@ document.addEventListener('DOMContentLoaded', function () { jobPhotoModule.init(@Model.Id, @Html.Raw(ViewBag.PhotoTagSuggestions ?? "[]")); - // Auto-open the Mark Complete modal when arriving from the job board - if (window.location.hash === '#completeModal') { - const modal = document.getElementById('completeJobModal'); - if (modal) new bootstrap.Modal(modal).show(); - history.replaceState(null, '', window.location.pathname + window.location.search); - } // ── Auto-submit after wizard saves an item ──────────────────────── let itemsModified = false; diff --git a/src/PowderCoating.Web/Views/Jobs/_CompleteJobModal.cshtml b/src/PowderCoating.Web/Views/Jobs/_CompleteJobModal.cshtml new file mode 100644 index 0000000..f38fa4e --- /dev/null +++ b/src/PowderCoating.Web/Views/Jobs/_CompleteJobModal.cshtml @@ -0,0 +1,146 @@ +@model PowderCoating.Application.DTOs.Job.JobDto +@{ + var emailDefault = ViewBag.EmailDefaultOnComplete == true; +} + diff --git a/src/PowderCoating.Web/wwwroot/js/install-app.js b/src/PowderCoating.Web/wwwroot/js/install-app.js index 07ed346..2e9fb33 100644 --- a/src/PowderCoating.Web/wwwroot/js/install-app.js +++ b/src/PowderCoating.Web/wwwroot/js/install-app.js @@ -1,12 +1,15 @@ // Browser-aware install UX for supported PWAs. // Shows a real install button only when the browser exposes the install prompt, // and falls back to platform-specific instructions for iOS Safari. +// After install the button is hidden; if the user later uninstalls, the browser +// re-fires beforeinstallprompt which clears the flag and shows the button again. (function () { 'use strict'; const installBtn = document.getElementById('installAppBtn'); if (!installBtn) return; + const INSTALLED_KEY = 'pclAppInstalled'; const ua = navigator.userAgent || ''; const isIos = /iphone|ipad|ipod/i.test(ua); const isIosSafari = isIos && /safari/i.test(ua) && !/crios|fxios|edgios/i.test(ua); @@ -51,6 +54,13 @@ return; } + // Hide if already installed and browser hasn't re-offered the prompt + // (flag is cleared automatically when beforeinstallprompt fires again after uninstall) + if (localStorage.getItem(INSTALLED_KEY) === '1') { + hideInstallButton(); + return; + } + if (deferredPrompt) { showInstallButton('prompt', 'Install App', 'bi-download'); return; @@ -64,14 +74,18 @@ hideInstallButton(); } + // Browser fires this when the app becomes installable (including after an uninstall). + // Clearing the flag here ensures the button reappears if the user previously uninstalled. window.addEventListener('beforeinstallprompt', function (event) { event.preventDefault(); deferredPrompt = event; + localStorage.removeItem(INSTALLED_KEY); refreshInstallUi(); }); window.addEventListener('appinstalled', function () { deferredPrompt = null; + localStorage.setItem(INSTALLED_KEY, '1'); hideInstallButton(); }); @@ -84,7 +98,10 @@ deferredPrompt.prompt(); try { - await deferredPrompt.userChoice; + const choice = await deferredPrompt.userChoice; + if (choice.outcome === 'accepted') { + localStorage.setItem(INSTALLED_KEY, '1'); + } } catch { // Ignore prompt result errors; the browser owns the final install flow. }