metis-desk/modules/companies/src/web/profiles/work-profile-form.tsx
2026-10-05 09:53:29 +02:00

242 lines
8.6 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import { inventoryApi, inventoryKeys } from "@metis/assets/client";
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 } from "../../common";
import {
type WorkProfileDTO,
type WorkProfileInput,
WorkProfileInputSchema,
} from "../../common/work-profiles";
import { accessApi, accessKeys } from "../access/access-api";
import { accessLabel } from "../access/access-labels";
import {
ConfigurationQuickEditor,
type ConfigurationQuickKind,
} from "../components/configuration-quick-editor";
import { ProfileFeedback } from "./profile-feedback";
import { profileApi, profileKeys } from "./work-profile-api";
export function WorkProfileForm({
company,
csrfToken,
profile,
onSaved,
}: {
company: CompanyDTO;
csrfToken: string;
profile?: WorkProfileDTO | undefined;
onSaved: () => void;
}) {
const { t } = useTranslation("companies");
const client = useQueryClient();
const prefix = useId();
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const [quick, setQuick] = useState<ConfigurationQuickKind>();
const access = useQuery({
queryKey: accessKeys.scope(company.id),
queryFn: () => accessApi.request(company.id),
});
const inventory = useQuery({
queryKey: inventoryKeys.company(company.id),
queryFn: () => inventoryApi.get(company.id),
});
const form = useForm<WorkProfileInput>({
resolver: zodResolver(WorkProfileInputSchema),
defaultValues: profile?.values ?? {
name: "",
personId: "",
assetId: "",
accountId: null,
active: true,
notes: null,
},
});
const values = form.watch();
const items = access.data?.items ?? [];
const today = new Date().toISOString().slice(0, 10);
const authorized =
!values.accountId ||
items.some((item) => {
const link = item.values;
return (
link.kind === "account_person" &&
link.personId === values.personId &&
link.accountId === values.accountId &&
link.startDate <= today &&
(link.endDate === null || today < link.endDate) &&
(link.configuration === "pending" || link.configuration === "configured")
);
});
const mutation = useMutation({
mutationFn: (input: WorkProfileInput) =>
profileApi.save(company.id, input, csrfToken, profile?.id),
onSuccess: async () => {
await client.invalidateQueries({ queryKey: profileKeys.company(company.id) });
if (mounted.current) {
onSaved();
}
},
});
const disabled = mutation.isPending;
const selectors = [
{
field: "personId",
label: "person",
create: "newPerson",
kind: "person",
options: company.people
.filter((item) => item.active || item.id === values.personId)
.map((item) => ({ value: item.id, label: item.name })),
},
{
field: "assetId",
label: "asset",
create: "newAsset",
kind: "asset",
options: (inventory.data?.assets ?? [])
.filter((item) => item.values.status !== "retired" || item.id === values.assetId)
.map((item) => ({ value: item.id, label: item.values.name })),
},
{
field: "accountId",
label: "account",
create: "newAccount",
kind: "account",
options: items
.filter(
(item) =>
item.values.kind === "account" &&
((item.values.active && item.values.usage !== "technical") ||
item.id === values.accountId)
)
.map((item) => ({ value: item.id, label: accessLabel(item, items, company.people) })),
},
] as const;
return (
<>
<form className="space-y-5" onSubmit={form.handleSubmit((input) => mutation.mutate(input))}>
<fieldset className="space-y-4" disabled={disabled}>
<div className="space-y-2">
<Label htmlFor={prefix}>{t("profiles.name")}</Label>
<Input id={prefix} maxLength={100} required {...form.register("name")} />
</div>
<div className="grid gap-4 sm:grid-cols-2">
{selectors.map((selector) => {
const fixed = !!profile && selector.field !== "accountId";
return (
<div className="space-y-2" key={selector.field}>
<Label htmlFor={`${prefix}-${selector.field}`}>
{t(`profiles.${selector.label}`)}
</Label>
<SelectWithAction
action={
fixed
? undefined
: {
label: t(`mail.${selector.create}`),
onClick: () => setQuick(selector.kind),
}
}
disabled={disabled || fixed || !inventory.data || !access.data}
id={`${prefix}-${selector.field}`}
label={t(`profiles.${selector.label}`)}
onValueChange={(id) =>
form.setValue(
selector.field,
selector.field === "accountId" ? id || null : id
)
}
options={selector.options}
placeholder={t(
selector.field === "accountId" ? "profiles.noAccount" : "profiles.choose"
)}
required={selector.field !== "accountId"}
value={values[selector.field] ?? ""}
/>
</div>
);
})}
</div>
<p className="text-sm text-muted-foreground">{t("profiles.accountHint")}</p>
{values.personId && values.accountId && !authorized && (
<div className="space-y-2 rounded-md border p-3 text-sm">
<p>{t("mail.noAuthorization")}</p>
<Button onClick={() => setQuick("authorization")} type="button" variant="outline">
{t("mail.authorize")}
</Button>
</div>
)}
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" {...form.register("active")} />
{t("profiles.active")}
</label>
<div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("profiles.notes")}</Label>
<Textarea
id={`${prefix}-notes`}
maxLength={4000}
onChange={(event) => form.setValue("notes", event.target.value || null)}
rows={3}
value={values.notes ?? ""}
/>
</div>
</fieldset>
{profile && <p className="text-sm text-muted-foreground">{t("profiles.fixed")}</p>}
{Object.keys(form.formState.errors).length > 0 && (
<p className="text-destructive" role="alert">
{t("profiles.errors.INVALID_REQUEST")}
</p>
)}
<ProfileFeedback error={mutation.error ?? access.error ?? inventory.error} />
<ContextHelp
description={t("profiles.description")}
examples={t("profiles.examples")}
examplesLabel={t("profiles.examplesLabel")}
guidance={t("profiles.guidance")}
scopeNote={t("profiles.scope")}
title={t("profiles.help")}
/>
<FormActions>
<Button disabled={disabled || !inventory.data || !access.data} type="submit">
{t(disabled ? "saving" : "save")}
</Button>
</FormActions>
</form>
{quick && (
<ConfigurationQuickEditor
company={company}
csrfToken={csrfToken}
data={access.data}
kind={quick}
onClose={() => setQuick(undefined)}
onCreated={(field, id) => {
if (mounted.current && id) {
form.setValue(field, id);
setQuick(undefined);
}
}}
values={{ personId: values.personId, accountId: values.accountId ?? "" }}
/>
)}
</>
);
}