fix(housekeeping): harden people account workflows
This commit is contained in:
1 parent
e1b31ff773
commit
25b76437ff
41 files changed
+2657
-838
No files matched your search
@@ -7,64 +7,123 @@ import {
|
||||
} from "../../../foundation/contracts";
|
||||
import type { HousekeepingPageInput } from "../../../route-handlers";
|
||||
import { type PeopleStaffQueryData, peopleStaffQuery } from "../queries/staff";
|
||||
import { PeoplePageFrame } from "./page-state";
|
||||
import { PeoplePageFrame, parsePeopleListInput } from "./page-state";
|
||||
import { PeopleCommandForm } from "./people-command-form";
|
||||
|
||||
interface PeopleStaffPageProps {
|
||||
readonly context: HousekeepingCapabilityContext;
|
||||
readonly result?: HousekeepingResult<PeopleStaffQueryData>;
|
||||
readonly partialDependencies?: readonly string[];
|
||||
}
|
||||
|
||||
export function PeopleStaffPage({
|
||||
context,
|
||||
result,
|
||||
partialDependencies,
|
||||
}: PeopleStaffPageProps) {
|
||||
export function PeopleStaffPage({ context, result }: PeopleStaffPageProps) {
|
||||
return (
|
||||
<PeoplePageFrame
|
||||
title="Staff"
|
||||
description="Review staff applications and team definitions."
|
||||
result={result}
|
||||
partialDependencies={partialDependencies}
|
||||
isEmpty={(data) => data.page.items.length === 0}
|
||||
>
|
||||
{(data) =>
|
||||
data.kind === "applications" ? (
|
||||
<ul className="space-y-2">
|
||||
{data.page.items.map((application) => (
|
||||
<li
|
||||
key={application.id}
|
||||
className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<h2>{application.username ?? `User #${application.userId}`}</h2>
|
||||
<p>{application.content}</p>
|
||||
{context.has(PERMS.USERS_EDIT) ? (
|
||||
<span data-housekeeping-command="people.application.decide">
|
||||
Dismiss application
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : data.kind === "teams" ? (
|
||||
<ul className="space-y-2">
|
||||
{data.page.items.map((team) => (
|
||||
<li
|
||||
key={team.id}
|
||||
className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<h2>{team.name}</h2>
|
||||
<p>{team.jobDescription ?? "No job description"}</p>
|
||||
{context.has(PERMS.USERS_EDIT) ? (
|
||||
<span data-housekeeping-command="people.team.change">
|
||||
Team controls
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null
|
||||
}
|
||||
{(data) => (
|
||||
<div className="space-y-3">
|
||||
<form method="get" className="flex gap-2">
|
||||
<input
|
||||
name="search"
|
||||
maxLength={100}
|
||||
aria-label="Search staff workflow"
|
||||
/>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
{data.kind === "applications" ? (
|
||||
<ul className="space-y-2">
|
||||
{data.page.items.map((application) => (
|
||||
<li
|
||||
key={application.id}
|
||||
className="space-y-3 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<h2>
|
||||
{application.username ?? `User #${application.userId}`}
|
||||
</h2>
|
||||
<p>{application.content}</p>
|
||||
{context.has(PERMS.USERS_EDIT) ? (
|
||||
<PeopleCommandForm
|
||||
commandId="people.application.decide"
|
||||
buttonLabel="Dismiss application"
|
||||
input={{
|
||||
applicationId: application.id,
|
||||
decision: "dismiss",
|
||||
}}
|
||||
requiresReason
|
||||
/>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : data.kind === "teams" ? (
|
||||
<div className="space-y-3">
|
||||
{context.has(PERMS.USERS_EDIT) ? (
|
||||
<PeopleCommandForm
|
||||
commandId="people.team.change"
|
||||
buttonLabel="Create team"
|
||||
input={{ action: "create" }}
|
||||
fields={[
|
||||
{
|
||||
name: "rankName",
|
||||
label: "Team name",
|
||||
type: "text",
|
||||
maxLength: 255,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "badge",
|
||||
label: "Badge",
|
||||
type: "text",
|
||||
maxLength: 255,
|
||||
},
|
||||
{
|
||||
name: "jobDescription",
|
||||
label: "Job description",
|
||||
type: "text",
|
||||
maxLength: 255,
|
||||
},
|
||||
{
|
||||
name: "staffColor",
|
||||
label: "Staff color",
|
||||
type: "text",
|
||||
maxLength: 255,
|
||||
defaultValue: "#327fa8",
|
||||
},
|
||||
{
|
||||
name: "hiddenRank",
|
||||
label: "Hidden rank",
|
||||
type: "checkbox",
|
||||
},
|
||||
]}
|
||||
requiresReason
|
||||
/>
|
||||
) : null}
|
||||
<ul className="space-y-2">
|
||||
{data.page.items.map((team) => (
|
||||
<li
|
||||
key={team.id}
|
||||
className="space-y-3 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<h2>{team.name}</h2>
|
||||
<p>{team.jobDescription ?? "No job description"}</p>
|
||||
{context.has(PERMS.USERS_EDIT) ? (
|
||||
<PeopleCommandForm
|
||||
commandId="people.team.change"
|
||||
buttonLabel="Delete team"
|
||||
input={{ action: "delete", teamId: team.id }}
|
||||
requiresReason
|
||||
/>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</PeoplePageFrame>
|
||||
);
|
||||
}
|
||||
@@ -73,7 +132,10 @@ export async function renderPeopleStaffPage(input: HousekeepingPageInput) {
|
||||
const routeId = input.match.routeId;
|
||||
const result =
|
||||
routeId === "people.staff.applications" || routeId === "people.staff.teams"
|
||||
? await peopleStaffQuery.run(input.context, { routeId, list: {} })
|
||||
? await peopleStaffQuery.run(input.context, {
|
||||
routeId,
|
||||
list: parsePeopleListInput(input.searchParams ?? {}),
|
||||
})
|
||||
: fail(
|
||||
"NOT_FOUND",
|
||||
"errors.housekeeping.notFound",
|
||||
|
||||
Reference in new issue
Block a user