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.
}