fix(support): polish operator response workspace
This commit is contained in:
parent
8adfe77c00
commit
cba544ffda
|
|
@ -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"
|
||||
>
|
||||
<div>
|
||||
<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>
|
||||
<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 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 class="mt-4 grid gap-4 lg:grid-cols-[14rem_minmax(0,1fr)] lg:items-start">
|
||||
<.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"
|
||||
/>
|
||||
<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("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"}
|
||||
]}
|
||||
/>
|
||||
<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="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 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.")}
|
||||
</p>
|
||||
<.button class="btn btn-error min-w-44 sm:self-end">
|
||||
<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>
|
||||
</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,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"
|
||||
>
|
||||
<div>
|
||||
<h4 class="font-bold">{gettext("Operator action")}</h4>
|
||||
<p class="mt-1 text-sm text-base-content/60">
|
||||
{gettext("Update the case status and add a private reply when needed.")}
|
||||
</p>
|
||||
<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 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 class="mt-4 grid gap-4 lg:grid-cols-[14rem_minmax(0,1fr)] lg:items-start">
|
||||
<.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"
|
||||
/>
|
||||
<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("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"}
|
||||
]}
|
||||
/>
|
||||
<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="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 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.")}
|
||||
</p>
|
||||
<.button class="btn btn-primary min-w-44 sm:self-end">
|
||||
<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>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user