262 lines
9.3 KiB
TypeScript
262 lines
9.3 KiB
TypeScript
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { catalogApi, catalogKeys, inventoryApi, inventoryKeys } from "@metis/resources/client";
|
|
import type { CatalogItemDTO, ResourceDTO } from "@metis/resources/common";
|
|
import { Button } from "@repo/shadcn-ui/components/button";
|
|
import { ContextHelp } from "@repo/shadcn-ui/components/context-help";
|
|
import { FormActions } from "@repo/shadcn-ui/components/form-dialog";
|
|
import { Input } from "@repo/shadcn-ui/components/input";
|
|
import { Label } from "@repo/shadcn-ui/components/label";
|
|
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
|
|
import { Textarea } from "@repo/shadcn-ui/components/textarea";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { useEffect, useId, useRef, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import type { CompanyDTO, WorkProfileDTO } from "../../common";
|
|
import {
|
|
CERTIFICATE_STATE_TRANSITIONS,
|
|
type CertificateConfigurationDTO,
|
|
type CertificateConfigurationInput,
|
|
CertificateConfigurationInputSchema,
|
|
} from "../../common/certificate-configurations";
|
|
import { profileApi, profileKeys } from "../profiles/work-profile-api";
|
|
|
|
import { certificateApi, certificateKeys } from "./certificate-api";
|
|
import { CertificateFeedback } from "./certificate-feedback";
|
|
import { CertificateQuickEditor } from "./certificate-quick-editor";
|
|
|
|
export function CertificateConfigurationForm({
|
|
company,
|
|
csrfToken,
|
|
configuration,
|
|
initialProfileId = "",
|
|
onSaved,
|
|
}: {
|
|
company: CompanyDTO;
|
|
csrfToken: string;
|
|
configuration?: CertificateConfigurationDTO | undefined;
|
|
initialProfileId?: string;
|
|
onSaved: () => void;
|
|
}) {
|
|
const { t } = useTranslation("companies");
|
|
const prefix = useId();
|
|
const client = useQueryClient();
|
|
const mounted = useRef(true);
|
|
useEffect(() => {
|
|
mounted.current = true;
|
|
return () => {
|
|
mounted.current = false;
|
|
};
|
|
}, []);
|
|
const [quick, setQuick] = useState<"profile" | "certificate">();
|
|
const profiles = useQuery({
|
|
queryKey: profileKeys.company(company.id),
|
|
queryFn: () => profileApi.get(company.id),
|
|
});
|
|
const resources = useQuery({
|
|
queryKey: inventoryKeys.company(company.id),
|
|
queryFn: () => inventoryApi.get(company.id),
|
|
});
|
|
const catalog = useQuery({
|
|
queryKey: catalogKeys.scope(company.id),
|
|
queryFn: () => catalogApi.get(company.id),
|
|
});
|
|
const form = useForm<CertificateConfigurationInput>({
|
|
resolver: zodResolver(CertificateConfigurationInputSchema),
|
|
defaultValues: certificateDefaults(configuration, initialProfileId),
|
|
});
|
|
const values = form.watch();
|
|
const ready = [profiles.data, resources.data, catalog.data].every(Boolean);
|
|
const fixed = Boolean(configuration || initialProfileId);
|
|
const states = certificateStates(configuration);
|
|
const mutation = useMutation({
|
|
mutationFn: (input: CertificateConfigurationInput) =>
|
|
certificateApi.save(company.id, input, csrfToken, configuration?.id),
|
|
onSuccess: async () => {
|
|
await client.invalidateQueries({ queryKey: certificateKeys.company(company.id) });
|
|
if (mounted.current) {
|
|
onSaved();
|
|
}
|
|
},
|
|
});
|
|
return (
|
|
<>
|
|
<form className="space-y-5" onSubmit={form.handleSubmit((input) => mutation.mutate(input))}>
|
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
|
|
<SelectWithAction
|
|
action={
|
|
fixed
|
|
? undefined
|
|
: { label: t("profiles.new"), onClick: () => setQuick("profile") }
|
|
}
|
|
disabled={fixed || !ready}
|
|
id={`${prefix}-profile`}
|
|
label={t("mail.profile")}
|
|
onValueChange={(id) => form.setValue("profileId", id)}
|
|
options={certificateProfiles(values.profileId, profiles.data?.profiles)}
|
|
placeholder={t("mail.choose")}
|
|
required
|
|
value={values.profileId}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`${prefix}-resource`}>{t("certificates.resource")}</Label>
|
|
<SelectWithAction
|
|
action={
|
|
configuration
|
|
? undefined
|
|
: {
|
|
label: t("certificates.newResource"),
|
|
onClick: () => setQuick("certificate"),
|
|
}
|
|
}
|
|
disabled={!!configuration || !ready}
|
|
id={`${prefix}-resource`}
|
|
label={t("certificates.resource")}
|
|
onValueChange={(id) => form.setValue("resourceId", id)}
|
|
options={certificateOptions(
|
|
values.resourceId,
|
|
resources.data?.resources,
|
|
catalog.data?.items
|
|
)}
|
|
placeholder={t("mail.choose")}
|
|
required
|
|
value={values.resourceId}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
|
<SelectWithAction
|
|
id={`${prefix}-state`}
|
|
label={t("mail.state")}
|
|
onValueChange={(value) => {
|
|
const parsed = CertificateConfigurationInputSchema.shape.state.safeParse(value);
|
|
if (parsed.success) {
|
|
form.setValue("state", parsed.data);
|
|
}
|
|
}}
|
|
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
|
placeholder={t("mail.choose")}
|
|
required
|
|
value={values.state}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`${prefix}-location`}>{t("certificates.location")}</Label>
|
|
<Input
|
|
id={`${prefix}-location`}
|
|
maxLength={255}
|
|
onChange={(event) => form.setValue("location", event.target.value || null)}
|
|
value={values.location ?? ""}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
|
<Textarea
|
|
id={`${prefix}-notes`}
|
|
maxLength={4000}
|
|
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
|
rows={3}
|
|
value={values.notes ?? ""}
|
|
/>
|
|
</div>
|
|
</fieldset>
|
|
{configuration && (
|
|
<p className="text-sm text-muted-foreground">{t("certificates.fixed")}</p>
|
|
)}
|
|
{Object.keys(form.formState.errors).length > 0 && (
|
|
<p className="text-sm text-destructive" role="alert">
|
|
{t("certificates.errors.INVALID_REQUEST")}
|
|
</p>
|
|
)}
|
|
<CertificateFeedback
|
|
error={mutation.error ?? profiles.error ?? resources.error ?? catalog.error}
|
|
/>
|
|
<ContextHelp
|
|
description={t("certificates.description")}
|
|
examples={t("certificates.examples")}
|
|
examplesLabel={t("mail.examplesLabel")}
|
|
guidance={t("certificates.guidance")}
|
|
scopeNote={t("certificates.scope")}
|
|
title={t("certificates.help")}
|
|
/>
|
|
<FormActions>
|
|
<Button disabled={!ready || mutation.isPending} type="submit">
|
|
{t(mutation.isPending ? "saving" : "save")}
|
|
</Button>
|
|
</FormActions>
|
|
</form>
|
|
{quick && (
|
|
<CertificateQuickEditor
|
|
company={company}
|
|
csrfToken={csrfToken}
|
|
kind={quick}
|
|
onClose={() => setQuick(undefined)}
|
|
onSaved={(field, id) => {
|
|
if (mounted.current) {
|
|
form.setValue(field, id);
|
|
setQuick(undefined);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function certificateOptions(
|
|
selected: string,
|
|
resources: readonly ResourceDTO[] = [],
|
|
items: readonly CatalogItemDTO[] = []
|
|
) {
|
|
const templates = new Set(
|
|
items
|
|
.filter(
|
|
(item) =>
|
|
item.values.kind === "template" &&
|
|
(item.builtinKey === "template:certificate" || item.values.usage === "certificate")
|
|
)
|
|
.map((item) => item.id)
|
|
);
|
|
return resources
|
|
.filter(
|
|
(item) =>
|
|
templates.has(item.values.templateId) && (item.values.active || item.id === selected)
|
|
)
|
|
.map((item) => ({ value: item.id, label: item.values.name }));
|
|
}
|
|
|
|
function certificateDefaults(
|
|
configuration: CertificateConfigurationDTO | undefined,
|
|
profileId: string
|
|
): CertificateConfigurationInput {
|
|
return (
|
|
configuration?.values ?? {
|
|
profileId,
|
|
resourceId: "",
|
|
location: null,
|
|
state: "pending",
|
|
notes: null,
|
|
}
|
|
);
|
|
}
|
|
|
|
function certificateStates(
|
|
configuration: CertificateConfigurationDTO | undefined
|
|
): readonly CertificateConfigurationInput["state"][] {
|
|
return configuration
|
|
? CERTIFICATE_STATE_TRANSITIONS[configuration.values.state]
|
|
: ["pending", "configured"];
|
|
}
|
|
|
|
function certificateProfiles(selected: string, profiles: readonly WorkProfileDTO[] = []) {
|
|
return profiles
|
|
.filter((item) => item.values.active || item.id === selected)
|
|
.map((item) => ({ value: item.id, label: item.values.name }));
|
|
}
|