diff --git a/docs/screenshots/reorganized-flow/04-case-lookup.png b/docs/screenshots/reorganized-flow/04-case-lookup.png new file mode 100644 index 0000000..b0f4e80 Binary files /dev/null and b/docs/screenshots/reorganized-flow/04-case-lookup.png differ diff --git a/docs/screenshots/reorganized-flow/05-case-confirmation.png b/docs/screenshots/reorganized-flow/05-case-confirmation.png new file mode 100644 index 0000000..6b1d00b Binary files /dev/null and b/docs/screenshots/reorganized-flow/05-case-confirmation.png differ diff --git a/efile_app/efile/api/dropdown_views.py b/efile_app/efile/api/dropdown_views.py index c595bdd..43e5403 100644 --- a/efile_app/efile/api/dropdown_views.py +++ b/efile_app/efile/api/dropdown_views.py @@ -427,7 +427,7 @@ def _prioritize_courts_by_location(courts, guessed_court="", user_zip=None, user return courts # Determine user county from zip code if provided - target_county = user_county + target_county = user_county or guessed_court if user_zip and not target_county: target_county = get_county_by_zip(user_zip) diff --git a/efile_app/efile/api/suffolk_api_views.py b/efile_app/efile/api/suffolk_api_views.py index de75c5a..b20494a 100644 --- a/efile_app/efile/api/suffolk_api_views.py +++ b/efile_app/efile/api/suffolk_api_views.py @@ -156,7 +156,7 @@ def lookup_case(request): api_url = f"{settings.EFSP_URL}/jurisdictions/{state}/codes/courts/{court}/case_types/{case_info['caseTypeCode']}" case_resp = requests.get(api_url, headers=headers, timeout=30) if case_resp.ok: - data = category_resp.json() + data = case_resp.json() case_info["caseTypeName"] = data["name"] break else: diff --git a/efile_app/efile/migrations/0004_filingdraft_case_title.py b/efile_app/efile/migrations/0004_filingdraft_case_title.py new file mode 100644 index 0000000..1f52729 --- /dev/null +++ b/efile_app/efile/migrations/0004_filingdraft_case_title.py @@ -0,0 +1,13 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("efile", "0003_branch_aware_workflow")] + + operations = [ + migrations.AddField( + model_name="filingdraft", + name="case_title", + field=models.CharField(blank=True, max_length=500), + ), + ] diff --git a/efile_app/efile/models.py b/efile_app/efile/models.py index 6195062..7ffe872 100644 --- a/efile_app/efile/models.py +++ b/efile_app/efile/models.py @@ -76,6 +76,7 @@ class Status(models.TextChoices): previous_case_id = models.CharField(max_length=255, blank=True) docket_number = models.CharField(max_length=255, blank=True) + case_title = models.CharField(max_length=500, blank=True) selected_payment_account_id = models.CharField(max_length=255, blank=True) selected_payment_account_name = models.CharField(max_length=255, blank=True) diff --git a/efile_app/efile/services/drafts.py b/efile_app/efile/services/drafts.py index 6a66100..421b3e7 100644 --- a/efile_app/efile/services/drafts.py +++ b/efile_app/efile/services/drafts.py @@ -109,6 +109,7 @@ def set_current_step(draft: FilingDraft, current_step: WorkflowStepKey | str) -> "existing_case": ("existing_case",), "previous_case_id": ("case_tracking_id", "previous_case_id"), "docket_number": ("case_docket_id", "case_number", "docket_number"), + "case_title": ("case_title", "case_title_text"), "selected_payment_account_id": ("selected_payment_account", "payment_account_id"), "selected_payment_account_name": ("selected_payment_account_name",), "name_change_reason": ("reason_for_name_change", "reason_for_change"), @@ -268,6 +269,7 @@ def read_case_data(draft: FilingDraft | None) -> dict[str, Any]: _put(data, "document_type_name", draft.document_type_name) _put(data, "previous_case_id", draft.previous_case_id) _put(data, "docket_number", draft.docket_number) + _put(data, "case_title", draft.case_title) _put(data, "selected_payment_account", draft.selected_payment_account_id) _put(data, "selected_payment_account_name", draft.selected_payment_account_name) _put(data, "optional_services", list(draft.optional_services or [])) @@ -497,6 +499,7 @@ def draft_snapshot(draft: FilingDraft | None) -> dict[str, Any] | None: "document_type_name": draft.document_type_name, "previous_case_id": draft.previous_case_id, "docket_number": draft.docket_number, + "case_title": draft.case_title, "selected_payment_account_id": draft.selected_payment_account_id, "selected_payment_account_name": draft.selected_payment_account_name, "optional_services": draft.optional_services, diff --git a/efile_app/efile/static/css/reorganized-flow.css b/efile_app/efile/static/css/reorganized-flow.css index beafb19..53d3e88 100644 --- a/efile_app/efile/static/css/reorganized-flow.css +++ b/efile_app/efile/static/css/reorganized-flow.css @@ -414,6 +414,108 @@ margin: 0.35rem 0 0; } +.lookup-fields { + display: grid; + gap: 1.25rem; +} + +.lookup-state { + align-items: center; + background: #eef5ff; + border-radius: 10px; + color: #234d84; + display: flex; + gap: 0.8rem; + margin-top: 1rem; + padding: 1rem; +} + +.lookup-state strong, +.lookup-state small { + display: block; +} + +.lookup-state small { + color: #61748f; +} + +.found-heading { + align-items: flex-start; + display: flex; + gap: 1rem; +} + +.found-heading__icon { + align-items: center; + background: #e2f5e9; + border-radius: 50%; + color: #18733d; + display: flex; + flex: 0 0 auto; + height: 46px; + justify-content: center; + margin-top: 0.25rem; + width: 46px; +} + +.case-summary { + border: 1px solid #dce3ec; + border-radius: 12px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 0; + overflow: hidden; +} + +.case-summary>div { + border-top: 1px solid #e3e8ef; + padding: 1rem 1.2rem; +} + +.case-summary>div:nth-child(even):not(.case-summary__primary) { + border-left: 1px solid #e3e8ef; +} + +.case-summary__primary { + background: #f6f9fd; + border-top: 0 !important; + grid-column: 1 / -1; +} + +.case-summary dt { + color: #6b788c; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.04em; + margin-bottom: 0.2rem; + text-transform: uppercase; +} + +.case-summary dd { + color: #1c314d; + font-size: 1rem; + font-weight: 650; + margin: 0; +} + +.case-summary__primary dd { + font-size: 1.2rem; +} + +.confirmation-question { + margin-top: 1.5rem; +} + +.confirmation-question strong { + color: #263c58; + font-size: 1.05rem; +} + +.confirmation-question p { + color: #69768a; + margin: 0.25rem 0 0; +} + @media (max-width: 700px) { .review-grid { grid-template-columns: 1fr; @@ -428,6 +530,14 @@ grid-template-columns: auto 1fr auto; } + .case-summary { + grid-template-columns: 1fr; + } + + .case-summary>div:nth-child(even):not(.case-summary__primary) { + border-left: 0; + } + .status-pill { grid-column: 2; } diff --git a/efile_app/efile/static/js/case-lookup.js b/efile_app/efile/static/js/case-lookup.js new file mode 100644 index 0000000..6a321a2 --- /dev/null +++ b/efile_app/efile/static/js/case-lookup.js @@ -0,0 +1,86 @@ +(function() { + const form = document.getElementById("case-lookup-form"); + if (!form) return; + + const courtSelect = document.getElementById("court"); + const caseNumber = document.getElementById("case-number"); + const state = document.getElementById("lookup-state"); + const errorBox = document.getElementById("lookup-error"); + const submitButton = document.getElementById("find-case-button"); + const guessedCourt = JSON.parse(document.getElementById("guessed-court").textContent || '""'); + const selectedCourtCode = JSON.parse(document.getElementById("selected-court-code").textContent || '""'); + + async function loadCourts() { + try { + const response = await apiUtils.fetchJSON("/api/dropdowns/courts/", "GET", { + jurisdiction: apiUtils.getCurrentJurisdiction(), + guessed_court: guessedCourt, + }); + if (!response.success) throw new Error(response.error || "Could not load courts."); + courtSelect.innerHTML = ''; + response.data.forEach((court) => { + const option = document.createElement("option"); + option.value = court.value; + option.textContent = court.text; + if (court.value === selectedCourtCode || (!selectedCourtCode && (court.selected || court.default))) { + option.selected = true; + } + courtSelect.appendChild(option); + }); + } catch (error) { + courtSelect.innerHTML = ''; + errorBox.textContent = error.message; + errorBox.hidden = false; + } + } + + form.addEventListener("submit", async (event) => { + event.preventDefault(); + errorBox.hidden = true; + state.hidden = false; + submitButton.disabled = true; + + try { + const jurisdiction = apiUtils.getCurrentJurisdiction(); + const lookup = await apiUtils.fetchJSON("/api/suffolk/lookup-case/", "GET", { + court: courtSelect.value, + caseNumber: caseNumber.value.trim(), + jurisdiction, + }); + if (!lookup.success || !lookup.caseInfo?.caseTrackingID) { + throw new Error(lookup.error || "We could not find a matching case. Check the court and case number."); + } + + const selectedCourt = courtSelect.selectedOptions[0]; + const caseInfo = lookup.caseInfo; + const saveResponse = await fetch(window.location.href, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": apiUtils.getCSRFToken(), + }, + body: JSON.stringify({ + court: courtSelect.value, + court_name: selectedCourt?.textContent?.replace(" (Recommended)", "") || "", + case_tracking_id: caseInfo.caseTrackingID, + case_docket_id: caseInfo.caseDocketID || caseNumber.value.trim(), + case_title: caseInfo.caseTitle || "", + case_category_code: caseInfo.caseCategoryCode || "", + case_category_name: caseInfo.caseCategoryName || "", + case_type_code: caseInfo.caseTypeCode || "", + case_type_name: caseInfo.caseTypeName || "", + }), + }); + const saved = await saveResponse.json(); + if (!saveResponse.ok || !saved.success) throw new Error(saved.error || "Could not save the case."); + window.location.href = saved.redirect_url; + } catch (error) { + errorBox.textContent = error.message; + errorBox.hidden = false; + state.hidden = true; + submitButton.disabled = false; + } + }); + + loadCourts(); +})(); \ No newline at end of file diff --git a/efile_app/efile/templates/efile/case_confirmation.html b/efile_app/efile/templates/efile/case_confirmation.html new file mode 100644 index 0000000..030c0be --- /dev/null +++ b/efile_app/efile/templates/efile/case_confirmation.html @@ -0,0 +1,83 @@ +{% extends "efile/workflow_base.html" %} +{% load i18n %} +{% block title %} + {% translate "Confirm your court case" %} +{% endblock title %} +{% block workflow_content %} +
+
{% translate "Confirm case" %}
+
+ +
+

{% translate "Is this your court case?" %}

+

{% translate "Check the details before adding this filing to the case." %}

+
+
+
+
+
{% translate "Case name" %}
+
+ {% if case.case_title %} + {{ case.case_title }} + {% else %} + {% translate "Case title not available" %} + {% endif %} +
+
+
+
{% translate "Case number" %}
+
+ {{ case.docket_number }} +
+
+
+
{% translate "Court" %}
+
+ {{ case.court_name|default:case.court_code }} +
+
+
+
{% translate "Case category" %}
+
+ {% if case.case_category_name %} + {{ case.case_category_name }} + {% elif case.case_category_code %} + {{ case.case_category_code }} + {% else %} + {% translate "Not available" %} + {% endif %} +
+
+
+
{% translate "Case type" %}
+
+ {% if case.case_type_name %} + {{ case.case_type_name }} + {% elif case.case_type_code %} + {{ case.case_type_code }} + {% else %} + {% translate "Not available" %} + {% endif %} +
+
+
+
+ {% csrf_token %} +
+ {% translate "Does this match your case?" %} +

{% translate "Choosing Yes will attach your documents to this case." %}

+
+
+ + +
+
+
+{% endblock workflow_content %} diff --git a/efile_app/efile/templates/efile/case_lookup.html b/efile_app/efile/templates/efile/case_lookup.html new file mode 100644 index 0000000..a3b54fd --- /dev/null +++ b/efile_app/efile/templates/efile/case_lookup.html @@ -0,0 +1,57 @@ +{% extends "efile/workflow_base.html" %} +{% load static %} +{% load i18n %} +{% block title %} + {% translate "Find your court case" %} +{% endblock title %} +{% block workflow_content %} +
+
{% translate "Confirm case" %}
+

{% translate "Find your court case" %}

+

+ {% translate "Choose the court and enter the case number exactly as it appears on your documents." %} +

+
+ {% csrf_token %} +
+ + +
+ + +
+ {% translate "Back" %} + +
+
+
+ {{ guessed_court|json_script:"guessed-court" }} + {{ selected_court_code|json_script:"selected-court-code" }} +{% endblock workflow_content %} +{% block extra_js %} + +{% endblock extra_js %} diff --git a/efile_app/efile/tests/test_case_confirmation.py b/efile_app/efile/tests/test_case_confirmation.py new file mode 100644 index 0000000..c50f5c0 --- /dev/null +++ b/efile_app/efile/tests/test_case_confirmation.py @@ -0,0 +1,106 @@ +import pytest +from django.urls import reverse + +from efile.models import FilingDraft +from efile.services.current_drafts import CURRENT_DRAFT_SESSION_KEY +from efile.workflow import ExistingCase, WorkflowStepKey + + +def prepare_client(client, django_user_model, *, existing_case=ExistingCase.EXISTING): + user = django_user_model.objects.create_user(username="case-lookup-user", tyler_jurisdiction="illinois") + draft = FilingDraft.objects.create( + user=user, + jurisdiction="illinois", + workflow_version=2, + existing_case=existing_case, + ) + client.force_login(user) + session = client.session + session[CURRENT_DRAFT_SESSION_KEY] = draft.pk + session["auth_tokens"] = {"TYLER-TOKEN-ILLINOIS": "token"} + session.save() + return draft + + +@pytest.mark.django_db +def test_new_case_skips_case_lookup(client, django_user_model): + prepare_client(client, django_user_model, existing_case=ExistingCase.NEW) + + response = client.get(reverse("case_lookup", kwargs={"jurisdiction": "illinois"})) + + assert response.status_code == 302 + assert response.url == reverse("document_checklist", kwargs={"jurisdiction": "illinois"}) + + +@pytest.mark.django_db +def test_case_lookup_result_is_persisted_on_the_draft(client, django_user_model): + draft = prepare_client(client, django_user_model) + + response = client.post( + reverse("case_lookup", kwargs={"jurisdiction": "illinois"}), + data={ + "court": "cook:cvd1", + "court_name": "Cook County - Municipal Civil - District 1 - Chicago", + "case_tracking_id": "tracking-123", + "case_docket_id": "2025MR000123", + "case_title": "Ada Lovelace v. Example LLC", + "case_category_code": "MR", + "case_category_name": "Miscellaneous Remedy", + "case_type_code": "NC", + "case_type_name": "Name Change", + }, + content_type="application/json", + ) + + assert response.status_code == 200 + assert response.json()["redirect_url"] == reverse( + "case_confirmation", + kwargs={"jurisdiction": "illinois"}, + ) + draft.refresh_from_db() + assert draft.current_step == WorkflowStepKey.CASE_CONFIRMATION + assert draft.previous_case_id == "tracking-123" + assert draft.docket_number == "2025MR000123" + assert draft.case_title == "Ada Lovelace v. Example LLC" + assert draft.case_type_name == "Name Change" + + +@pytest.mark.django_db +def test_case_confirmation_accepts_case_and_converges_on_checklist(client, django_user_model): + draft = prepare_client(client, django_user_model) + draft.previous_case_id = "tracking-123" + draft.docket_number = "2025MR000123" + draft.case_title = "Ada Lovelace v. Example LLC" + draft.save() + + response = client.post( + reverse("case_confirmation", kwargs={"jurisdiction": "illinois"}), + {"confirmed": "yes"}, + ) + + assert response.status_code == 302 + assert response.url == reverse("document_checklist", kwargs={"jurisdiction": "illinois"}) + draft.refresh_from_db() + assert draft.current_step == WorkflowStepKey.DOCUMENT_CHECKLIST + + +@pytest.mark.django_db +def test_case_confirmation_rejection_clears_result_and_returns_to_lookup(client, django_user_model): + draft = prepare_client(client, django_user_model) + draft.previous_case_id = "tracking-123" + draft.docket_number = "2025MR000123" + draft.case_title = "Wrong case" + draft.save() + + response = client.post( + reverse("case_confirmation", kwargs={"jurisdiction": "illinois"}), + {"confirmed": "no"}, + ) + + assert response.status_code == 302 + assert response.url == reverse("case_lookup", kwargs={"jurisdiction": "illinois"}) + draft.refresh_from_db() + assert draft.previous_case_id == "" + assert draft.docket_number == "" + assert draft.case_title == "" + assert draft.current_step == WorkflowStepKey.CASE_LOOKUP diff --git a/efile_app/efile/urls.py b/efile_app/efile/urls.py index 08180f2..6e40c1d 100644 --- a/efile_app/efile/urls.py +++ b/efile_app/efile/urls.py @@ -3,6 +3,8 @@ from django.views.i18n import JavaScriptCatalog from .views.api_views import get_case_data_api, get_filing_components +from .views.case_confirmation import case_confirmation +from .views.case_lookup import case_lookup from .views.choose_jurisdiction import choose_jurisdiction from .views.confirmation import filing_confirmation from .views.draft_views import create_draft_view, get_current_draft_view @@ -53,18 +55,8 @@ def jurisdiction_homepage(request, jurisdiction): path("jurisdiction//filing-path/", filing_path, name="filing_path"), path("jurisdiction//upload-documents/", upload_documents, name="upload_documents"), path("jurisdiction//extraction-review/", extraction_review, name="extraction_review"), - path( - "jurisdiction//case-lookup/", - legacy_workflow_redirect, - {"destination": "case_lookup"}, - name="case_lookup", - ), - path( - "jurisdiction//case-confirmation/", - legacy_workflow_redirect, - {"destination": "case_confirmation"}, - name="case_confirmation", - ), + path("jurisdiction//case-lookup/", case_lookup, name="case_lookup"), + path("jurisdiction//case-confirmation/", case_confirmation, name="case_confirmation"), path( "jurisdiction//document-checklist/", legacy_workflow_redirect, diff --git a/efile_app/efile/views/case_confirmation.py b/efile_app/efile/views/case_confirmation.py new file mode 100644 index 0000000..46c7aad --- /dev/null +++ b/efile_app/efile/views/case_confirmation.py @@ -0,0 +1,55 @@ +from django.contrib import messages +from django.shortcuts import redirect, render +from django.views.decorators.http import require_http_methods + +from efile.api.suffolk_api_views import get_tyler_token +from efile.services.current_drafts import ensure_current_draft +from efile.services.drafts import draft_snapshot, write_case_data +from efile.workflow import ExistingCase, WorkflowStepKey, get_step_url, get_workflow_context + + +@require_http_methods(["GET", "POST"]) +def case_confirmation(request, jurisdiction): + if not request.user.is_authenticated or not get_tyler_token(request, jurisdiction): + return redirect("efile_login", jurisdiction=jurisdiction) + + draft = ensure_current_draft( + request, + jurisdiction, + current_step=WorkflowStepKey.CASE_CONFIRMATION, + workflow_version=2, + ) + if draft.existing_case != ExistingCase.EXISTING: + return redirect("document_checklist", jurisdiction=jurisdiction) + if not draft.previous_case_id or not draft.docket_number: + messages.error(request, "Find your court case before confirming it.") + return redirect("case_lookup", jurisdiction=jurisdiction) + + if request.method == "POST": + if request.POST.get("confirmed") == "yes": + draft.current_step = WorkflowStepKey.DOCUMENT_CHECKLIST + draft.save(update_fields=["current_step", "updated_at"]) + return redirect(get_step_url(WorkflowStepKey.DOCUMENT_CHECKLIST, jurisdiction)) + + write_case_data( + draft, + { + "previous_case_id": "", + "docket_number": "", + "case_title": "", + "case_category_code": "", + "case_category_name": "", + "case_type_code": "", + "case_type_name": "", + }, + current_step=WorkflowStepKey.CASE_LOOKUP, + ) + return redirect(get_step_url(WorkflowStepKey.CASE_LOOKUP, jurisdiction)) + + context = { + "is_logged_in": True, + "filing_draft": draft_snapshot(draft), + "case": draft, + } + context.update(get_workflow_context(WorkflowStepKey.CASE_CONFIRMATION, jurisdiction, draft)) + return render(request, "efile/case_confirmation.html", context) diff --git a/efile_app/efile/views/case_lookup.py b/efile_app/efile/views/case_lookup.py new file mode 100644 index 0000000..57dd143 --- /dev/null +++ b/efile_app/efile/views/case_lookup.py @@ -0,0 +1,75 @@ +import json + +from django.contrib import messages +from django.http import JsonResponse +from django.shortcuts import redirect, render +from django.views.decorators.http import require_http_methods + +from efile.api.suffolk_api_views import get_tyler_token +from efile.services.current_drafts import ensure_current_draft +from efile.services.drafts import draft_snapshot, write_case_data +from efile.workflow import ExistingCase, WorkflowStepKey, get_step_url, get_workflow_context + + +@require_http_methods(["GET", "POST"]) +def case_lookup(request, jurisdiction): + if not request.user.is_authenticated or not get_tyler_token(request, jurisdiction): + return redirect("efile_login", jurisdiction=jurisdiction) + + draft = ensure_current_draft( + request, + jurisdiction, + current_step=WorkflowStepKey.CASE_LOOKUP, + workflow_version=2, + ) + if draft.existing_case != ExistingCase.EXISTING: + messages.info(request, "Case lookup is only needed for an existing court case.") + return redirect("document_checklist", jurisdiction=jurisdiction) + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"success": False, "error": "Invalid case information."}, status=400) + + court_code = data.get("court") + docket_number = data.get("case_docket_id") or data.get("docket_number") + tracking_id = data.get("case_tracking_id") + if not court_code or not docket_number or not tracking_id: + return JsonResponse( + {"success": False, "error": "The case lookup result is missing required information."}, + status=400, + ) + + write_case_data( + draft, + { + "existing_case": ExistingCase.EXISTING, + "court": court_code, + "court_name": data.get("court_name", ""), + "case_tracking_id": tracking_id, + "case_docket_id": docket_number, + "case_title": data.get("case_title", ""), + "case_category_code": data.get("case_category_code", ""), + "case_category_name": data.get("case_category_name", ""), + "case_type_code": data.get("case_type_code", ""), + "case_type_name": data.get("case_type_name", ""), + }, + current_step=WorkflowStepKey.CASE_CONFIRMATION, + ) + return JsonResponse( + { + "success": True, + "redirect_url": get_step_url(WorkflowStepKey.CASE_CONFIRMATION, jurisdiction), + } + ) + + context = { + "is_logged_in": True, + "filing_draft": draft_snapshot(draft), + "guessed_court": draft.court_name or (draft.extracted_guesses or {}).get("court", ""), + "selected_court_code": draft.court_code, + "docket_number": draft.docket_number or (draft.extracted_guesses or {}).get("docket number", ""), + } + context.update(get_workflow_context(WorkflowStepKey.CASE_LOOKUP, jurisdiction, draft)) + return render(request, "efile/case_lookup.html", context)