155 lines
5.8 KiB
TypeScript
155 lines
5.8 KiB
TypeScript
|
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||
|
|
import { Card, CardContent } from "@repo/shadcn-ui/components/card";
|
||
|
|
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
|
||
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||
|
|
import { useState } from "react";
|
||
|
|
import { useTranslation } from "react-i18next";
|
||
|
|
|
||
|
|
import type { PersonProcessDTO } from "../../common/processes";
|
||
|
|
|
||
|
|
import { processApi, processKeys } from "./process-api";
|
||
|
|
import { ProcessEditForm } from "./process-edit-form";
|
||
|
|
import { ProcessFeedback } from "./process-feedback";
|
||
|
|
import { processReferenceTitle, processTaskTitle } from "./process-labels";
|
||
|
|
export function ProcessDetail({
|
||
|
|
process,
|
||
|
|
person,
|
||
|
|
csrfToken,
|
||
|
|
}: {
|
||
|
|
process: PersonProcessDTO;
|
||
|
|
person: string;
|
||
|
|
csrfToken: string;
|
||
|
|
}) {
|
||
|
|
const { t, i18n } = useTranslation("companies");
|
||
|
|
const [edit, setEdit] = useState(false);
|
||
|
|
const client = useQueryClient();
|
||
|
|
const refresh = useMutation({
|
||
|
|
mutationFn: () =>
|
||
|
|
processApi.refresh(
|
||
|
|
process.companyId,
|
||
|
|
process.id,
|
||
|
|
process.history.at(-1)?.id ?? "",
|
||
|
|
csrfToken
|
||
|
|
),
|
||
|
|
onSuccess: async () => {
|
||
|
|
await client.invalidateQueries({ queryKey: processKeys.company(process.companyId) });
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const open = process.values.state === "open";
|
||
|
|
const resolved = process.values.tasks.filter((task) => task.status !== "pending").length;
|
||
|
|
return (
|
||
|
|
<div className="space-y-5">
|
||
|
|
<header className="flex flex-wrap items-start justify-between gap-3">
|
||
|
|
<div>
|
||
|
|
<h2 className="text-xl font-semibold">{process.values.name}</h2>
|
||
|
|
<p>
|
||
|
|
{person} · {t(`processes.${process.values.kind}`)} ·{" "}
|
||
|
|
{t(`processes.${process.values.state}`)}
|
||
|
|
</p>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
{t("processes.progress", { resolved, total: process.values.tasks.length })}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
{open && (
|
||
|
|
<div className="flex gap-2">
|
||
|
|
{process.values.kind !== "onboarding" && (
|
||
|
|
<Button
|
||
|
|
disabled={refresh.isPending}
|
||
|
|
onClick={() => refresh.mutate()}
|
||
|
|
variant="outline"
|
||
|
|
>
|
||
|
|
{t("processes.refresh")}
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
<Button disabled={refresh.isPending} onClick={() => setEdit(true)}>
|
||
|
|
{t("processes.update")}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</header>
|
||
|
|
<p className="text-sm text-muted-foreground">{t("processes.resolutionHelp")}</p>
|
||
|
|
<ProcessFeedback error={refresh.error} />
|
||
|
|
{process.values.notes && <p className="whitespace-pre-wrap">{process.values.notes}</p>}
|
||
|
|
<Card>
|
||
|
|
<CardContent className="divide-y pt-3">
|
||
|
|
{process.values.tasks.map((task) => {
|
||
|
|
const recorded = Boolean(
|
||
|
|
task.reference &&
|
||
|
|
process.review.some(
|
||
|
|
(ref) => ref.kind === task.reference?.kind && ref.id === task.reference?.id
|
||
|
|
)
|
||
|
|
);
|
||
|
|
return (
|
||
|
|
<section className="space-y-2 py-3" key={task.id}>
|
||
|
|
<h3 className="font-medium">{processTaskTitle(task, t)}</h3>
|
||
|
|
<p className="text-sm">{t(`processes.${task.status}`)}</p>
|
||
|
|
{task.reference && (
|
||
|
|
<p
|
||
|
|
className={
|
||
|
|
recorded ? "text-sm text-amber-700" : "text-sm text-muted-foreground"
|
||
|
|
}
|
||
|
|
>
|
||
|
|
{t(recorded ? "processes.stillRecorded" : "processes.notInReview")}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{task.notes && <p className="text-sm whitespace-pre-wrap">{task.notes}</p>}
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<details className="rounded-md border p-4">
|
||
|
|
<summary className="cursor-pointer font-medium">
|
||
|
|
{t("processes.currentReview", { count: process.review.length })}
|
||
|
|
</summary>
|
||
|
|
<p className="text-sm mt-2">{t("processes.currentReviewHelp")}</p>
|
||
|
|
<ol className="list-disc pl-5 text-sm mt-2">
|
||
|
|
{process.review.map((ref) => (
|
||
|
|
<li key={`${ref.kind}:${ref.id}`}>{processReferenceTitle(ref, t)}</li>
|
||
|
|
))}
|
||
|
|
</ol>
|
||
|
|
</details>
|
||
|
|
<details className="rounded-md border p-4">
|
||
|
|
<summary className="cursor-pointer font-medium">{t("processes.history")}</summary>
|
||
|
|
<ol className="space-y-3 mt-3">
|
||
|
|
{[...process.history].reverse().map((r) => (
|
||
|
|
<li className="rounded border p-3 text-sm" key={r.id}>
|
||
|
|
<p className="font-medium">
|
||
|
|
{new Intl.DateTimeFormat(i18n.language, {
|
||
|
|
dateStyle: "medium",
|
||
|
|
timeStyle: "short",
|
||
|
|
}).format(new Date(r.recordedAt))}{" "}
|
||
|
|
· {r.actorName}
|
||
|
|
</p>
|
||
|
|
<p>
|
||
|
|
{r.values.name} · {t(`processes.${r.values.state}`)}
|
||
|
|
</p>
|
||
|
|
{r.values.notes && <p className="whitespace-pre-wrap">{r.values.notes}</p>}
|
||
|
|
<ol className="list-decimal pl-5 mt-2">
|
||
|
|
{r.values.tasks.map((task) => (
|
||
|
|
<li key={task.id}>
|
||
|
|
{processTaskTitle(task, t)} · {t(`processes.${task.status}`)}
|
||
|
|
{task.notes && <p className="whitespace-pre-wrap">{task.notes}</p>}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ol>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ol>
|
||
|
|
</details>
|
||
|
|
<FormDialog
|
||
|
|
cancelLabel={t("cancel")}
|
||
|
|
formActions
|
||
|
|
onOpenChange={setEdit}
|
||
|
|
open={edit}
|
||
|
|
title={t("processes.update")}
|
||
|
|
wide
|
||
|
|
>
|
||
|
|
{edit && (
|
||
|
|
<ProcessEditForm csrfToken={csrfToken} onSaved={() => setEdit(false)} process={process} />
|
||
|
|
)}
|
||
|
|
</FormDialog>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|