161 lines
5.8 KiB
TypeScript
161 lines
5.8 KiB
TypeScript
|
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||
|
|
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
|
||
|
|
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
|
||
|
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||
|
|
import { Label } from "@repo/shadcn-ui/components/label";
|
||
|
|
import { useQuery } from "@tanstack/react-query";
|
||
|
|
import { useId, useState } from "react";
|
||
|
|
import { useTranslation } from "react-i18next";
|
||
|
|
|
||
|
|
import { interventionTemplateApi, interventionTemplateKeys } from "./template-api";
|
||
|
|
import { TemplateHistory, TemplateSteps } from "./template-detail";
|
||
|
|
import { InterventionTemplateForm, copyTemplateValues } from "./template-form";
|
||
|
|
import { InterventionTemplateFeedback } from "./template-help";
|
||
|
|
export function InterventionTemplatesPage({
|
||
|
|
scope = "global",
|
||
|
|
csrfToken,
|
||
|
|
}: {
|
||
|
|
scope?: string;
|
||
|
|
csrfToken: string;
|
||
|
|
}) {
|
||
|
|
const { t } = useTranslation("companies");
|
||
|
|
const prefix = useId();
|
||
|
|
const query = useQuery({
|
||
|
|
queryKey: interventionTemplateKeys.list(scope),
|
||
|
|
queryFn: () => interventionTemplateApi.list(scope),
|
||
|
|
});
|
||
|
|
const [editor, setEditor] = useState<{ id?: string; copyId?: string }>();
|
||
|
|
const [search, setSearch] = useState("");
|
||
|
|
const [showInactive, setShowInactive] = useState(true);
|
||
|
|
const templates = query.data?.templates ?? [];
|
||
|
|
const selected = templates.find((item) => item.id === editor?.id);
|
||
|
|
const copied = templates.find((item) => item.id === editor?.copyId);
|
||
|
|
const visible = templates.filter(
|
||
|
|
(item) =>
|
||
|
|
(showInactive || item.values.active) &&
|
||
|
|
item.values.name.toLocaleLowerCase().includes(search.trim().toLocaleLowerCase())
|
||
|
|
);
|
||
|
|
return (
|
||
|
|
<div className="space-y-5">
|
||
|
|
<header className="flex items-center justify-between gap-4">
|
||
|
|
<h2 className="text-xl font-semibold">{t("interventionTemplates.title")}</h2>
|
||
|
|
<Button disabled={!query.data || query.isError} onClick={() => setEditor({})}>
|
||
|
|
{t("interventionTemplates.new")}
|
||
|
|
</Button>
|
||
|
|
</header>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
{t(
|
||
|
|
scope === "global"
|
||
|
|
? "interventionTemplates.scopeGlobal"
|
||
|
|
: "interventionTemplates.scopeCompany"
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
<div className="flex flex-wrap gap-4 items-center">
|
||
|
|
<div>
|
||
|
|
<Label className="sr-only" htmlFor={`${prefix}-search`}>
|
||
|
|
{t("interventionTemplates.filter")}
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id={`${prefix}-search`}
|
||
|
|
onChange={(event) => setSearch(event.target.value)}
|
||
|
|
placeholder={t("interventionTemplates.filter")}
|
||
|
|
value={search}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="flex gap-2 items-center">
|
||
|
|
<Checkbox
|
||
|
|
checked={showInactive}
|
||
|
|
id={`${prefix}-inactive`}
|
||
|
|
onCheckedChange={(value) => setShowInactive(value === true)}
|
||
|
|
/>
|
||
|
|
<Label htmlFor={`${prefix}-inactive`}>{t("interventionTemplates.showInactive")}</Label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{query.isPending && <p role="status">{t("loading")}</p>}
|
||
|
|
<InterventionTemplateFeedback error={query.error} />
|
||
|
|
{query.isError && (
|
||
|
|
<Button
|
||
|
|
onClick={() => {
|
||
|
|
query.refetch().catch(() => undefined);
|
||
|
|
}}
|
||
|
|
variant="outline"
|
||
|
|
>
|
||
|
|
{t("retry")}
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
{query.data && !query.isError && (
|
||
|
|
<>
|
||
|
|
{!visible.length && <p>{t("interventionTemplates.empty")}</p>}
|
||
|
|
{visible.map((template) => (
|
||
|
|
<section className="rounded-xl border p-5 space-y-4" key={template.id}>
|
||
|
|
<header className="flex flex-wrap justify-between gap-3">
|
||
|
|
<div>
|
||
|
|
<h3 className="font-medium">{template.values.name}</h3>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
{t(
|
||
|
|
template.companyId
|
||
|
|
? "interventionTemplates.company"
|
||
|
|
: "interventionTemplates.global"
|
||
|
|
)}{" "}
|
||
|
|
· {t(template.values.active ? "active" : "inactive")}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
{template.companyId === (scope === "global" ? null : scope) && (
|
||
|
|
<Button
|
||
|
|
onClick={() => setEditor({ id: template.id })}
|
||
|
|
size="sm"
|
||
|
|
variant="outline"
|
||
|
|
>
|
||
|
|
{t("edit")}
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
<Button
|
||
|
|
onClick={() => setEditor({ copyId: template.id })}
|
||
|
|
size="sm"
|
||
|
|
variant="outline"
|
||
|
|
>
|
||
|
|
{t("interventionTemplates.copy")}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
<TemplateSteps values={template.values} />
|
||
|
|
<TemplateHistory template={template} />
|
||
|
|
</section>
|
||
|
|
))}
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
<FormDialog
|
||
|
|
cancelLabel={t("cancel")}
|
||
|
|
formActions
|
||
|
|
onOpenChange={(open) => {
|
||
|
|
if (!open) {
|
||
|
|
setEditor(undefined);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
open={editor !== undefined}
|
||
|
|
title={t(selected ? "interventionTemplates.edit" : "interventionTemplates.new")}
|
||
|
|
wide
|
||
|
|
>
|
||
|
|
{editor && (
|
||
|
|
<InterventionTemplateForm
|
||
|
|
csrfToken={csrfToken}
|
||
|
|
initialValues={
|
||
|
|
copied
|
||
|
|
? copyTemplateValues(
|
||
|
|
copied,
|
||
|
|
`${t("interventionTemplates.copyOf")} ${copied.values.name}`
|
||
|
|
)
|
||
|
|
: undefined
|
||
|
|
}
|
||
|
|
key={editor.id ?? editor.copyId ?? "new"}
|
||
|
|
onSaved={() => setEditor(undefined)}
|
||
|
|
scope={scope}
|
||
|
|
template={selected}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</FormDialog>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|