diff --git a/lib/who_need_help_web/live/support_operations_live.ex b/lib/who_need_help_web/live/support_operations_live.ex index 197e7b2..2ad3e6f 100644 --- a/lib/who_need_help_web/live/support_operations_live.ex +++ b/lib/who_need_help_web/live/support_operations_live.ex @@ -206,44 +206,89 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do id={"removal-moderation-#{notice.id}"} phx-submit="moderate-removal" phx-value-id={notice.id} - class="mt-4 rounded-2xl border border-error/20 bg-error/5 p-4" + class="mt-5 overflow-hidden rounded-2xl border border-error/25 bg-base-100 shadow-sm" > -
-

{gettext("Operator action")}

-

- {gettext("Record the decision and notify the submitter when contact is available.")} -

+
+
+ + <.icon name="hero-document-check" class="size-5" /> + +
+

{gettext("Operator action")}

+

+ {gettext( + "Record the decision and notify the submitter when contact is available." + )} +

+
+
+ + {gettext("Audited update")} +
-
- <.input - field={form[:status]} - type="select" - label={gettext("Status")} - class="select w-full bg-base-100 shadow-sm" - options={[ - {gettext("Open"), "open"}, - {gettext("Urgent review"), "urgent_review"}, - {gettext("Reviewing"), "reviewing"}, - {gettext("More information needed"), "needs_information"}, - {gettext("Action taken"), "actioned"}, - {gettext("Rejected"), "rejected"}, - {gettext("Closed"), "closed"} - ]} - /> - <.input - field={form[:resolution_note]} - type="textarea" - rows="2" - label={gettext("Decision or information request")} - placeholder={gettext("Explain the decision or request the missing information")} - class="textarea min-h-28 w-full resize-y bg-base-100 leading-relaxed shadow-sm" - /> +
+
+ <.input + field={form[:status]} + type="select" + label={gettext("Case status")} + class="select h-12 w-full rounded-xl border-base-300 bg-base-100 shadow-none focus:border-error focus:outline-none focus:ring-3 focus:ring-error/15" + options={[ + {gettext("Open"), "open"}, + {gettext("Urgent review"), "urgent_review"}, + {gettext("Reviewing"), "reviewing"}, + {gettext("More information needed"), "needs_information"}, + {gettext("Action taken"), "actioned"}, + {gettext("Rejected"), "rejected"}, + {gettext("Closed"), "closed"} + ]} + /> +

+ {gettext("Choose the state that will appear in the case history.")} +

+
+
+ <.input + field={form[:resolution_note]} + type="textarea" + rows="4" + maxlength="10000" + autocomplete="off" + aria-describedby={"removal-note-help-#{notice.id}"} + label={gettext("Decision or information request")} + placeholder={ + gettext( + "Explain what was decided, what action was taken, or what information is still needed…" + ) + } + class="textarea min-h-36 w-full resize-y rounded-xl border-base-300 bg-base-100 px-4 py-3 text-sm leading-6 shadow-none transition-[border-color,box-shadow] placeholder:text-base-content/40 focus:border-error focus:outline-none focus:ring-3 focus:ring-error/15" + /> +
+ {gettext("Write for the submitter; avoid internal-only notes.")} + {gettext("Up to 10,000 characters")} +
+
-
-

- {gettext("Review the status and note before sending this update.")} -

- <.button class="btn btn-error min-w-44 sm:self-end"> +
+
+ <.icon name="hero-bell-alert" class="mt-0.5 size-4 shrink-0" /> +

+ {if notice.contact_verified_at, + do: gettext("The verified contact will receive this update by email."), + else: + gettext( + "This update is recorded, but email is disabled until contact is verified." + )} +

+
+ <.button + phx-disable-with={gettext("Saving…")} + class="btn btn-error min-w-48 gap-2 sm:self-end" + > + <.icon name="hero-paper-airplane" class="size-4" /> {gettext("Save and notify")}
@@ -364,42 +409,85 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do id={"support-moderation-#{request.id}"} phx-submit="moderate-support" phx-value-id={request.id} - class="mt-4 rounded-2xl border border-primary/20 bg-primary/5 p-4" + class="mt-5 overflow-hidden rounded-2xl border border-primary/25 bg-base-100 shadow-sm" > -
-

{gettext("Operator action")}

-

- {gettext("Update the case status and add a private reply when needed.")} -

+
+
+ + <.icon name="hero-chat-bubble-left-right" class="size-5" /> + +
+

{gettext("Operator action")}

+

+ {gettext("Update the case status and add a private reply when needed.")} +

+
+
+ + {gettext("Audited update")} +
-
- <.input - field={form[:status]} - type="select" - label={gettext("Status")} - class="select w-full bg-base-100 shadow-sm" - options={[ - {gettext("Open"), "open"}, - {gettext("Reviewing"), "reviewing"}, - {gettext("Waiting for requester"), "waiting_for_requester"}, - {gettext("Resolved"), "resolved"}, - {gettext("Closed"), "closed"} - ]} - /> - <.input - field={form[:response]} - type="textarea" - rows="2" - label={gettext("Private reply (optional)")} - placeholder={gettext("Write a clear reply for the requester")} - class="textarea min-h-28 w-full resize-y bg-base-100 leading-relaxed shadow-sm" - /> +
+
+ <.input + field={form[:status]} + type="select" + label={gettext("Case status")} + class="select h-12 w-full rounded-xl border-base-300 bg-base-100 shadow-none focus:border-primary focus:outline-none focus:ring-3 focus:ring-primary/15" + options={[ + {gettext("Open"), "open"}, + {gettext("Reviewing"), "reviewing"}, + {gettext("Waiting for requester"), "waiting_for_requester"}, + {gettext("Resolved"), "resolved"}, + {gettext("Closed"), "closed"} + ]} + /> +

+ {gettext("Choose the state that will appear in the case history.")} +

+
+
+ <.input + field={form[:response]} + type="textarea" + rows="4" + maxlength="5000" + autocomplete="off" + aria-describedby={"support-reply-help-#{request.id}"} + label={gettext("Reply to requester (optional)")} + placeholder={ + gettext( + "Write a concise reply with the decision, next step, or information you need…" + ) + } + class="textarea min-h-36 w-full resize-y rounded-xl border-base-300 bg-base-100 px-4 py-3 text-sm leading-6 shadow-none transition-[border-color,box-shadow] placeholder:text-base-content/40 focus:border-primary focus:outline-none focus:ring-3 focus:ring-primary/15" + /> +
+ {gettext("This reply becomes part of the case conversation.")} + {gettext("Up to 5,000 characters")} +
+
-
-

- {gettext("Review the status and reply before sending this update.")} -

- <.button class="btn btn-primary min-w-44 sm:self-end"> +
+
+ <.icon name="hero-bell-alert" class="mt-0.5 size-4 shrink-0" /> +

+ {if request.contact_verified_at, + do: gettext("The verified contact will receive this update by email."), + else: + gettext( + "This update is recorded, but email is disabled until contact is verified." + )} +

+
+ <.button + phx-disable-with={gettext("Saving…")} + class="btn btn-primary min-w-48 gap-2 sm:self-end" + > + <.icon name="hero-paper-airplane" class="size-4" /> {gettext("Save and notify")}
diff --git a/test/who_need_help_web/controllers/support_controller_test.exs b/test/who_need_help_web/controllers/support_controller_test.exs index 15f9034..00516b5 100644 --- a/test/who_need_help_web/controllers/support_controller_test.exs +++ b/test/who_need_help_web/controllers/support_controller_test.exs @@ -318,15 +318,25 @@ defmodule WhoNeedHelpWeb.SupportControllerTest do ) == 1 assert Enum.count( - LazyHTML.query(document, "form[id^='removal-moderation-'] textarea[rows='2']") + LazyHTML.query( + document, + "form[id^='removal-moderation-'] textarea[rows='4'][maxlength='10000']" + ) ) == 1 assert Enum.count( - LazyHTML.query(document, "form[id^='support-moderation-'] textarea[rows='2']") + LazyHTML.query( + document, + "form[id^='support-moderation-'] textarea[rows='4'][maxlength='5000']" + ) ) == 1 assert response =~ "Operator action" - assert response =~ "Private reply (optional)" + assert response =~ "Reply to requester (optional)" + assert response =~ "Audited update" + + assert response =~ + "This update is recorded, but email is disabled until contact is verified." end end end