diff --git a/efile_app/efile/middleware.py b/efile_app/efile/middleware.py index 86c59036..eef8cf14 100644 --- a/efile_app/efile/middleware.py +++ b/efile_app/efile/middleware.py @@ -1,7 +1,6 @@ from django.contrib.auth import logout from django.http import JsonResponse from django.shortcuts import render -from django.urls import reverse from django.utils.deprecation import MiddlewareMixin from efile.models import FilingDraft @@ -43,27 +42,17 @@ def process_exception(self, request, exception): def process_response(self, request, response): draft = getattr(request, "filing_draft", None) if draft is not None: - # A targeted edit opened from the interview handoff returns to its - # missing-details list, instead of re-asking completed later steps. - targeted = request.GET.get("return_to") == "handoff" or request.POST.get("return_to") == "handoff" - destination = None - if ( - targeted - and request.method == "POST" - and response.status_code < 400 - and draft.status == FilingDraft.Status.DRAFT - ): - destination = reverse("handoff_review", args=[draft.pk]) + # Where a step goes next, including back to a detour's origin, is + # the step's own decision (efile.workflow.continue_url). This only + # keeps the filing named in the URL it chose. if response.has_header("Location"): - if destination: - response["Location"] = destination response["Location"] = draft_url(response["Location"], draft.pk) elif isinstance(response, JsonResponse): import json payload = json.loads(response.content) if isinstance(payload, dict) and isinstance(payload.get("redirect_url"), str): - payload["redirect_url"] = destination or draft_url(payload["redirect_url"], draft.pk) + payload["redirect_url"] = draft_url(payload["redirect_url"], draft.pk) response.content = json.dumps(payload) return response diff --git a/efile_app/efile/services/draft_urls.py b/efile_app/efile/services/draft_urls.py index 215fc203..05290656 100644 --- a/efile_app/efile/services/draft_urls.py +++ b/efile_app/efile/services/draft_urls.py @@ -21,6 +21,7 @@ "case_questions", "payment", "waiver_documents", + "document_checks", "case_review", "filing_confirmation", "expert_form", diff --git a/efile_app/efile/static/js/document-checklist.js b/efile_app/efile/static/js/document-checklist.js index fbe6bc3f..749aa07e 100644 --- a/efile_app/efile/static/js/document-checklist.js +++ b/efile_app/efile/static/js/document-checklist.js @@ -1,4 +1,11 @@ (function() { + // New files are checked on this page, so come back to them after a change. + const reloadAtChecks = () => { + window.history.replaceState(window.history.state, "", "#document-checks"); + window.location.reload(); + }; + document.getElementById("document-checks")?.addEventListener("document-checks:change", reloadAtChecks); + const form = document.getElementById("checklist-upload-form"); if (!form) return; const state = document.getElementById("checklist-upload-state"); @@ -24,7 +31,7 @@ } const result = await response.json(); if (!response.ok || !result.success) throw new Error(result.error || "Could not add documents."); - window.location.reload(); + reloadAtChecks(); } catch (error) { state.hidden = true; errorBox.textContent = error.message; diff --git a/efile_app/efile/static/js/document-checks.js b/efile_app/efile/static/js/document-checks.js new file mode 100644 index 00000000..10ecdc04 --- /dev/null +++ b/efile_app/efile/static/js/document-checks.js @@ -0,0 +1,66 @@ +/* Confirm or remove a newly prepared copy without leaving the page it was added on. + * + * Markup: a [data-document-checks] container (data-url: the document-checks + * endpoint) holding [data-document-check] blocks. Each finished action fires + * "document-checks:change" on the container with {action, data}, so the page + * can update whatever depends on its documents. + */ +const DocumentChecks = { + pending(root = document) { + return Boolean(root.querySelector("[data-document-check]")); + }, + + add(container, html) { + const template = document.createElement("template"); + template.innerHTML = html.trim(); + const check = template.content.firstElementChild; + container.appendChild(check); + check.querySelectorAll("[data-pdf-preview]").forEach((details) => window.attachPdfPreview?.(details)); + return check; + }, + + async submit(button) { + const container = button.closest("[data-document-checks]"); + const check = button.closest("[data-document-check]"); + const action = button.hasAttribute("data-document-remove") ? "remove" : "confirm"; + const status = check.querySelector("[data-document-check-status]"); + const buttons = check.querySelectorAll("button"); + const body = new FormData(); + body.append("action", action); + body.append("document_id", check.dataset.documentId); + body.append("preview_fingerprint", check.dataset.fingerprint); + body.append("csrfmiddlewaretoken", apiUtils.getCSRFToken()); + buttons.forEach((element) => { + element.disabled = true; + }); + status.textContent = action === "remove" ? gettext("Removing document…") : gettext("Saving…"); + try { + const response = await fetch(window.withFilingDraft(container.dataset.url), { + method: "POST", + body, + credentials: "same-origin" + }); + const data = await response.json(); + if (!response.ok || !data.success) throw new Error(data.error || gettext("That did not work. Try again.")); + check.remove(); + container.dispatchEvent(new CustomEvent("document-checks:change", { + bubbles: true, + detail: { + action, + data + } + })); + } catch (error) { + status.textContent = error.message; + buttons.forEach((element) => { + element.disabled = false; + }); + } + } +}; +window.DocumentChecks = DocumentChecks; + +document.addEventListener("click", (event) => { + const button = event.target.closest("[data-document-checks] [data-document-confirm], [data-document-checks] [data-document-remove]"); + if (button) DocumentChecks.submit(button); +}); \ No newline at end of file diff --git a/efile_app/efile/static/js/document-preview.js b/efile_app/efile/static/js/document-preview.js index d47a397c..76aaa9fc 100644 --- a/efile_app/efile/static/js/document-preview.js +++ b/efile_app/efile/static/js/document-preview.js @@ -95,7 +95,12 @@ status.textContent = gettext("The PDF did not load. Download it or close and reopen this view."); } } - document.querySelectorAll("[data-pdf-preview]").forEach((details) => { + + function attach(details) { details.addEventListener("toggle", () => openPreview(details)); - }); + // Previews added after load, already open, may never fire a toggle. + if (details.open) openPreview(details); + } + window.attachPdfPreview = attach; + document.querySelectorAll("[data-pdf-preview]").forEach(attach); })(); \ No newline at end of file diff --git a/efile_app/efile/static/js/payment.js b/efile_app/efile/static/js/payment.js index ce56450b..722d0925 100644 --- a/efile_app/efile/static/js/payment.js +++ b/efile_app/efile/static/js/payment.js @@ -1,3 +1,4 @@ +/* global DocumentChecks */ const PAYMENT_URLS = { accounts: "/api/payment-accounts/", accountTypes: "/api/payment-account-types/", @@ -57,7 +58,30 @@ const PaymentPage = { setFeesState(loading) { document.getElementById("loadingSpinner").style.display = loading ? "block" : "none"; - document.getElementById("submitButton").disabled = loading || this.removingAccount || this.waiverUploading || !this.feeQuoteReady || !document.getElementById("selected-payment-account").value; + document.getElementById("submitButton").disabled = loading || this.removingAccount || this.waiverUploading || DocumentChecks.pending() || !this.feeQuoteReady || !document.getElementById("selected-payment-account").value; + }, + + // A copy added on this page is confirmed here before Review. + async onDocumentCheck({ + action, + data + }) { + document.getElementById("fee-inputs-token").textContent = JSON.stringify(data.fee_inputs_token); + const confirmation = document.getElementById("waiver-upload-confirmation"); + if (action === "remove") { + if (confirmation) confirmation.hidden = true; + const upload = document.getElementById("waiver-upload-required"); + if (upload) upload.hidden = false; + document.getElementById("add-waiver-document")?.focus(); + paymentMessages.showSuccess(gettext("Document removed. You can upload a different file.")); + // The removed document no longer counts toward fees. + await this.chooseIntent(); + } else { + if (confirmation && !confirmation.hidden) confirmation.textContent = gettext("Fee waiver document added and checked."); + const next = document.querySelector("[data-document-check]") || document.querySelector('input[name="paymentIntent"]:checked') || document.getElementById("submitButton"); + next.focus(); + } + this.setFeesState(false); }, async loadAccountTypes() { @@ -318,6 +342,7 @@ const PaymentPage = { document.querySelectorAll('input[name="paymentIntent"]').forEach((input) => { input.addEventListener("change", () => this.chooseIntent()); }); + document.getElementById("document-checks").addEventListener("document-checks:change", (event) => this.onDocumentCheck(event.detail)); await this.loadAccountTypes(); this.loadAccounts().catch(() => paymentMessages.showError(gettext("We could not load payment methods."))); } diff --git a/efile_app/efile/static/js/waiver-upload.js b/efile_app/efile/static/js/waiver-upload.js index a32bc705..4087c99d 100644 --- a/efile_app/efile/static/js/waiver-upload.js +++ b/efile_app/efile/static/js/waiver-upload.js @@ -1,4 +1,4 @@ -/* global PaymentPage, paymentJSON */ +/* global DocumentChecks, PaymentPage, paymentJSON */ /* Add a supporting waiver PDF without leaving payment or changing the lead. */ document.addEventListener("DOMContentLoaded", () => { const open = document.getElementById("add-waiver-document"); @@ -83,14 +83,15 @@ document.addEventListener("DOMContentLoaded", () => { }); const data = await response.json(); if (!response.ok || !data.success) throw new Error(data.error || gettext("The upload failed. Try again.")); - if (data.preview_url) { - window.location.assign(window.withFilingDraft(data.preview_url)); - return; - } document.getElementById("fee-inputs-token").textContent = JSON.stringify(data.fee_inputs_token); document.getElementById("waiver-upload-required").hidden = true; + file.value = ""; + status.textContent = ""; const confirmation = document.getElementById("waiver-upload-confirmation"); confirmation.hidden = false; + // The filer checks the prepared copy here, without leaving Fees. + DocumentChecks.add(document.getElementById("document-checks"), data.check_html); + PaymentPage.setFeesState(false); confirmation.focus(); // Old requests and quotes described a different set of documents. await PaymentPage.chooseIntent(); diff --git a/efile_app/efile/templates/efile/case_confirmation.html b/efile_app/efile/templates/efile/case_confirmation.html index 520cd2c6..12e7603e 100644 --- a/efile_app/efile/templates/efile/case_confirmation.html +++ b/efile_app/efile/templates/efile/case_confirmation.html @@ -64,6 +64,7 @@