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,20 +206,33 @@ 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"
>
<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">
<div class="flex min-w-0 items-start gap-3">
<span class="grid size-10 shrink-0 place-items-center rounded-xl bg-error/10 text-error">
<.icon name="hero-document-check" class="size-5" />
</span>
<div class="min-w-0">
<h4 class="font-bold">{gettext("Operator action")}</h4>
<p class="mt-1 text-sm text-base-content/60">
{gettext("Record the decision and notify the submitter when contact is available.")}
<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 class="mt-4 grid gap-4 lg:grid-cols-[14rem_minmax(0,1fr)] lg:items-start">
</div>
<span class="badge badge-error badge-outline shrink-0">
{gettext("Audited update")}
</span>
</div>
<div class="grid gap-5 p-4 xl:grid-cols-[15rem_minmax(0,1fr)] xl:items-start">
<div class="min-w-0">
<.input
field={form[:status]}
type="select"
label={gettext("Status")}
class="select w-full bg-base-100 shadow-sm"
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"},
@ -230,20 +243,52 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do
{gettext("Closed"), "closed"}
]}
/>
<p class="mt-1 text-xs leading-relaxed text-base-content/55">
{gettext("Choose the state that will appear in the case history.")}
</p>
</div>
<div class="min-w-0">
<.input
field={form[:resolution_note]}
type="textarea"
rows="2"
rows="4"
maxlength="10000"
autocomplete="off"
aria-describedby={"removal-note-help-#{notice.id}"}
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"
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 class="mt-2 flex flex-col gap-3 border-t border-error/15 pt-4 sm:flex-row sm:items-center sm:justify-between">
<p class="text-xs text-base-content/55">
{gettext("Review the status and note before sending this update.")}
</div>
</div>
<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">
<div class="flex items-start gap-2 text-xs leading-relaxed text-base-content/60">
<.icon name="hero-bell-alert" class="mt-0.5 size-4 shrink-0" />
<p>
{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>
<.button class="btn btn-error min-w-44 sm:self-end">
</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")}
</.button>
</div>
@ -364,20 +409,31 @@ 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"
>
<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">
<div class="flex min-w-0 items-start gap-3">
<span class="grid size-10 shrink-0 place-items-center rounded-xl bg-primary/10 text-primary">
<.icon name="hero-chat-bubble-left-right" class="size-5" />
</span>
<div class="min-w-0">
<h4 class="font-bold">{gettext("Operator action")}</h4>
<p class="mt-1 text-sm text-base-content/60">
<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 class="mt-4 grid gap-4 lg:grid-cols-[14rem_minmax(0,1fr)] lg:items-start">
</div>
<span class="badge badge-primary badge-outline shrink-0">
{gettext("Audited update")}
</span>
</div>
<div class="grid gap-5 p-4 xl:grid-cols-[15rem_minmax(0,1fr)] xl:items-start">
<div class="min-w-0">
<.input
field={form[:status]}
type="select"
label={gettext("Status")}
class="select w-full bg-base-100 shadow-sm"
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"},
@ -386,20 +442,52 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do
{gettext("Closed"), "closed"}
]}
/>
<p class="mt-1 text-xs leading-relaxed text-base-content/55">
{gettext("Choose the state that will appear in the case history.")}
</p>
</div>
<div class="min-w-0">
<.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"
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 class="mt-2 flex flex-col gap-3 border-t border-primary/15 pt-4 sm:flex-row sm:items-center sm:justify-between">
<p class="text-xs text-base-content/55">
{gettext("Review the status and reply before sending this update.")}
</div>
</div>
<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">
<div class="flex items-start gap-2 text-xs leading-relaxed text-base-content/60">
<.icon name="hero-bell-alert" class="mt-0.5 size-4 shrink-0" />
<p>
{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>
<.button class="btn btn-primary min-w-44 sm:self-end">
</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")}
</.button>
</div>

View File

@ -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