140 lines
3.7 KiB
TypeScript
140 lines
3.7 KiB
TypeScript
import { PERMS } from "@/lib/permission-slugs";
|
|
import {
|
|
createCorrelationId,
|
|
fail,
|
|
type HousekeepingCapabilityContext,
|
|
type HousekeepingResult,
|
|
} from "../../../foundation/contracts";
|
|
import type { HousekeepingPageInput } from "../../../route-handlers";
|
|
import {
|
|
type PeopleSupportQueryData,
|
|
peopleSupportQuery,
|
|
} from "../queries/support";
|
|
import { PeoplePageFrame } from "./page-state";
|
|
import { PeopleCommandForm } from "./people-command-form";
|
|
|
|
interface Props {
|
|
readonly context: HousekeepingCapabilityContext;
|
|
readonly result?: HousekeepingResult<PeopleSupportQueryData>;
|
|
}
|
|
|
|
export function PeopleTicketDetailPage({ context, result }: Props) {
|
|
const canEdit = context.hasAny(PERMS.TICKETS_EDIT, PERMS.MOD_TICKETS_EDIT);
|
|
return (
|
|
<PeoplePageFrame
|
|
title="Ticket"
|
|
description="Review the conversation and coordinate the support response."
|
|
result={result}
|
|
isEmpty={(data) => data.kind !== "ticket"}
|
|
>
|
|
{(data) =>
|
|
data.kind === "ticket" ? (
|
|
<article className="space-y-4">
|
|
<header>
|
|
<h2>{data.ticket.subject}</h2>
|
|
<p>
|
|
{data.ticket.status} · {data.ticket.priority}
|
|
</p>
|
|
</header>
|
|
<ul className="space-y-2">
|
|
{data.ticket.messages.map((message) => (
|
|
<li
|
|
key={message.id}
|
|
className="rounded border border-[var(--admin-border)] p-3"
|
|
>
|
|
<strong>
|
|
{message.username ?? `User #${message.userId}`}
|
|
</strong>
|
|
<p>{message.message}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{canEdit ? (
|
|
<div className="grid gap-3 lg:grid-cols-2">
|
|
<PeopleCommandForm
|
|
commandId="people.ticket.reply"
|
|
buttonLabel="Reply"
|
|
input={{ ticketId: data.ticket.id }}
|
|
fields={[
|
|
{
|
|
name: "message",
|
|
label: "Message",
|
|
type: "text",
|
|
required: true,
|
|
maxLength: 5000,
|
|
},
|
|
]}
|
|
/>
|
|
<PeopleCommandForm
|
|
commandId="people.ticket.assign"
|
|
buttonLabel="Assign"
|
|
input={{ ticketId: data.ticket.id }}
|
|
fields={[
|
|
{
|
|
name: "assigneeId",
|
|
label: "Assignee",
|
|
type: "select",
|
|
options: data.ticket.staff.map((staff) => ({
|
|
value: staff.id,
|
|
label: staff.username,
|
|
})),
|
|
},
|
|
]}
|
|
/>
|
|
<PeopleCommandForm
|
|
commandId="people.ticket.status"
|
|
buttonLabel="Change status"
|
|
input={{ ticketId: data.ticket.id }}
|
|
fields={[
|
|
{
|
|
name: "status",
|
|
label: "Status",
|
|
type: "select",
|
|
options: ["open", "in_progress", "waiting", "closed"].map(
|
|
(value) => ({ value, label: value }),
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
<PeopleCommandForm
|
|
commandId="people.ticket.priority"
|
|
buttonLabel="Change priority"
|
|
input={{ ticketId: data.ticket.id }}
|
|
fields={[
|
|
{
|
|
name: "priority",
|
|
label: "Priority",
|
|
type: "select",
|
|
options: ["low", "normal", "high", "urgent"].map(
|
|
(value) => ({ value, label: value }),
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</article>
|
|
) : null
|
|
}
|
|
</PeoplePageFrame>
|
|
);
|
|
}
|
|
|
|
export async function renderPeopleTicketDetailPage(
|
|
input: HousekeepingPageInput,
|
|
) {
|
|
const id = Number(input.match.params.id);
|
|
const result =
|
|
Number.isSafeInteger(id) && id > 0
|
|
? await peopleSupportQuery.run(input.context, {
|
|
routeId: "people.support.ticket-detail",
|
|
id,
|
|
})
|
|
: fail(
|
|
"VALIDATION",
|
|
"errors.housekeeping.validation",
|
|
createCorrelationId(),
|
|
);
|
|
return <PeopleTicketDetailPage context={input.context} result={result} />;
|
|
}
|