fix(support): polish operator response workspace

This commit is contained in:
SimpleTest 2026-07-24 22:31:20 +03:00
parent 8adfe77c00
commit cba544ffda
2 changed files with 169 additions and 71 deletions

View File

@ -206,44 +206,89 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do
id={"removal-moderation-#{notice.id}"} id={"removal-moderation-#{notice.id}"}
phx-submit="moderate-removal" phx-submit="moderate-removal"
phx-value-id={notice.id} 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"
> >
<div> <div class="flex flex-col gap-3 border-b border-error/15 bg-error/5 p-4 sm:flex-row sm:items-start sm:justify-between">
<h4 class="font-bold">{gettext("Operator action")}</h4> <div class="flex min-w-0 items-start gap-3">
<p class="mt-1 text-sm text-base-content/60"> <span class="grid size-10 shrink-0 place-items-center rounded-xl bg-error/10 text-error">
{gettext("Record the decision and notify the submitter when contact is available.")} <.icon name="hero-document-check" class="size-5" />
</p> </span>
<div class="min-w-0">
<h4 class="font-bold">{gettext("Operator action")}</h4>
<p class="mt-1 text-sm leading-relaxed text-base-content/65">
{gettext(
"Record the decision and notify the submitter when contact is available."
)}
</p>
</div>
</div>
<span class="badge badge-error badge-outline shrink-0">
{gettext("Audited update")}
</span>
</div> </div>
<div class="mt-4 grid gap-4 lg:grid-cols-[14rem_minmax(0,1fr)] lg:items-start"> <div class="grid gap-5 p-4 xl:grid-cols-[15rem_minmax(0,1fr)] xl:items-start">
<.input <div class="min-w-0">
field={form[:status]} <.input
type="select" field={form[:status]}
label={gettext("Status")} type="select"
class="select w-full bg-base-100 shadow-sm" label={gettext("Case status")}
options={[ 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"
{gettext("Open"), "open"}, options={[
{gettext("Urgent review"), "urgent_review"}, {gettext("Open"), "open"},
{gettext("Reviewing"), "reviewing"}, {gettext("Urgent review"), "urgent_review"},
{gettext("More information needed"), "needs_information"}, {gettext("Reviewing"), "reviewing"},
{gettext("Action taken"), "actioned"}, {gettext("More information needed"), "needs_information"},
{gettext("Rejected"), "rejected"}, {gettext("Action taken"), "actioned"},
{gettext("Closed"), "closed"} {gettext("Rejected"), "rejected"},
]} {gettext("Closed"), "closed"}
/> ]}
<.input />
field={form[:resolution_note]} <p class="mt-1 text-xs leading-relaxed text-base-content/55">
type="textarea" {gettext("Choose the state that will appear in the case history.")}
rows="2" </p>
label={gettext("Decision or information request")} </div>
placeholder={gettext("Explain the decision or request the missing information")} <div class="min-w-0">
class="textarea min-h-28 w-full resize-y bg-base-100 leading-relaxed shadow-sm" <.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"
/>
<div
id={"removal-note-help-#{notice.id}"}
class="mt-1 flex flex-col gap-1 text-xs leading-relaxed text-base-content/55 sm:flex-row sm:items-center sm:justify-between"
>
<span>{gettext("Write for the submitter; avoid internal-only notes.")}</span>
<span class="shrink-0">{gettext("Up to 10,000 characters")}</span>
</div>
</div>
</div> </div>
<div class="mt-2 flex flex-col gap-3 border-t border-error/15 pt-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-4 border-t border-error/15 bg-base-200/60 p-4 sm:flex-row sm:items-center sm:justify-between">
<p class="text-xs text-base-content/55"> <div class="flex items-start gap-2 text-xs leading-relaxed text-base-content/60">
{gettext("Review the status and note before sending this update.")} <.icon name="hero-bell-alert" class="mt-0.5 size-4 shrink-0" />
</p> <p>
<.button class="btn btn-error min-w-44 sm:self-end"> {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."
)}
</p>
</div>
<.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")} {gettext("Save and notify")}
</.button> </.button>
</div> </div>
@ -364,42 +409,85 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do
id={"support-moderation-#{request.id}"} id={"support-moderation-#{request.id}"}
phx-submit="moderate-support" phx-submit="moderate-support"
phx-value-id={request.id} 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"
> >
<div> <div class="flex flex-col gap-3 border-b border-primary/15 bg-primary/5 p-4 sm:flex-row sm:items-start sm:justify-between">
<h4 class="font-bold">{gettext("Operator action")}</h4> <div class="flex min-w-0 items-start gap-3">
<p class="mt-1 text-sm text-base-content/60"> <span class="grid size-10 shrink-0 place-items-center rounded-xl bg-primary/10 text-primary">
{gettext("Update the case status and add a private reply when needed.")} <.icon name="hero-chat-bubble-left-right" class="size-5" />
</p> </span>
<div class="min-w-0">
<h4 class="font-bold">{gettext("Operator action")}</h4>
<p class="mt-1 text-sm leading-relaxed text-base-content/65">
{gettext("Update the case status and add a private reply when needed.")}
</p>
</div>
</div>
<span class="badge badge-primary badge-outline shrink-0">
{gettext("Audited update")}
</span>
</div> </div>
<div class="mt-4 grid gap-4 lg:grid-cols-[14rem_minmax(0,1fr)] lg:items-start"> <div class="grid gap-5 p-4 xl:grid-cols-[15rem_minmax(0,1fr)] xl:items-start">
<.input <div class="min-w-0">
field={form[:status]} <.input
type="select" field={form[:status]}
label={gettext("Status")} type="select"
class="select w-full bg-base-100 shadow-sm" label={gettext("Case status")}
options={[ 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"
{gettext("Open"), "open"}, options={[
{gettext("Reviewing"), "reviewing"}, {gettext("Open"), "open"},
{gettext("Waiting for requester"), "waiting_for_requester"}, {gettext("Reviewing"), "reviewing"},
{gettext("Resolved"), "resolved"}, {gettext("Waiting for requester"), "waiting_for_requester"},
{gettext("Closed"), "closed"} {gettext("Resolved"), "resolved"},
]} {gettext("Closed"), "closed"}
/> ]}
<.input />
field={form[:response]} <p class="mt-1 text-xs leading-relaxed text-base-content/55">
type="textarea" {gettext("Choose the state that will appear in the case history.")}
rows="2" </p>
label={gettext("Private reply (optional)")} </div>
placeholder={gettext("Write a clear reply for the requester")} <div class="min-w-0">
class="textarea min-h-28 w-full resize-y bg-base-100 leading-relaxed shadow-sm" <.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"
/>
<div
id={"support-reply-help-#{request.id}"}
class="mt-1 flex flex-col gap-1 text-xs leading-relaxed text-base-content/55 sm:flex-row sm:items-center sm:justify-between"
>
<span>{gettext("This reply becomes part of the case conversation.")}</span>
<span class="shrink-0">{gettext("Up to 5,000 characters")}</span>
</div>
</div>
</div> </div>
<div class="mt-2 flex flex-col gap-3 border-t border-primary/15 pt-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-4 border-t border-primary/15 bg-base-200/60 p-4 sm:flex-row sm:items-center sm:justify-between">
<p class="text-xs text-base-content/55"> <div class="flex items-start gap-2 text-xs leading-relaxed text-base-content/60">
{gettext("Review the status and reply before sending this update.")} <.icon name="hero-bell-alert" class="mt-0.5 size-4 shrink-0" />
</p> <p>
<.button class="btn btn-primary min-w-44 sm:self-end"> {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."
)}
</p>
</div>
<.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")} {gettext("Save and notify")}
</.button> </.button>
</div> </div>

View File

@ -318,15 +318,25 @@ defmodule WhoNeedHelpWeb.SupportControllerTest do
) == 1 ) == 1
assert Enum.count( 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 ) == 1
assert Enum.count( 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 ) == 1
assert response =~ "Operator action" 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 end
end end