219 lines
8.2 KiB
TypeScript
219 lines
8.2 KiB
TypeScript
import { inventoryApi, inventoryKeys } from "@metis/assets/client";
|
|
import {
|
|
inventoryApi as resourceApi,
|
|
inventoryKeys as resourceKeys,
|
|
} from "@metis/resources/client";
|
|
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 { Input } from "@repo/shadcn-ui/components/input";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import type { CertificateConfigurationDTO, CompanyDTO } from "../../common";
|
|
import { profileApi, profileKeys } from "../profiles/work-profile-api";
|
|
|
|
import { certificateApi, certificateKeys } from "./certificate-api";
|
|
import { CertificateConfigurationForm } from "./certificate-configuration-form";
|
|
import { CertificateFeedback } from "./certificate-feedback";
|
|
export function CertificateConfigurationsPage({
|
|
company,
|
|
csrfToken,
|
|
initialProfileId,
|
|
}: {
|
|
company: CompanyDTO;
|
|
csrfToken: string;
|
|
initialProfileId?: string;
|
|
}) {
|
|
const { t, i18n } = useTranslation("companies");
|
|
const [editor, setEditor] = useState<string>();
|
|
const [view, setView] = useState<string>();
|
|
const [search, setSearch] = useState("");
|
|
const query = useQuery({
|
|
queryKey: certificateKeys.company(company.id),
|
|
queryFn: () => certificateApi.get(company.id),
|
|
});
|
|
const profiles = useQuery({
|
|
queryKey: profileKeys.company(company.id),
|
|
queryFn: () => profileApi.get(company.id),
|
|
});
|
|
const inventory = useQuery({
|
|
queryKey: inventoryKeys.company(company.id),
|
|
queryFn: () => inventoryApi.get(company.id),
|
|
});
|
|
const resources = useQuery({
|
|
queryKey: resourceKeys.company(company.id),
|
|
queryFn: () => resourceApi.get(company.id),
|
|
});
|
|
const resource = (id: string) =>
|
|
resources.data?.resources.find((item) => item.id === id)?.values.name ?? id;
|
|
const configurations = query.data?.configurations ?? [];
|
|
const asset = (id: string) =>
|
|
inventory.data?.assets.find((item) => item.id === id)?.values.name ?? id;
|
|
const profile = (id: string) => profiles.data?.profiles.find((item) => item.id === id);
|
|
const label = (id: string) => {
|
|
const item = profile(id);
|
|
return item
|
|
? `${item.values.name} · ${company.people.find((person) => person.id === item.values.personId)?.name ?? ""} · ${asset(item.values.assetId)}`
|
|
: id;
|
|
};
|
|
const filtered = configurations.filter(
|
|
(item) =>
|
|
(!initialProfileId || item.values.profileId === initialProfileId) &&
|
|
`${resource(item.values.resourceId)} ${label(item.values.profileId)}`
|
|
.toLocaleLowerCase()
|
|
.includes(search.trim().toLocaleLowerCase())
|
|
);
|
|
const edited = configurations.find((item) => item.id === editor);
|
|
const viewed = configurations.find((item) => item.id === view);
|
|
const ready = [query.data, profiles.data, inventory.data, resources.data].every(Boolean);
|
|
const canCreate = !initialProfileId || profile(initialProfileId)?.values.active === true;
|
|
return (
|
|
<div className="space-y-5">
|
|
<header className="flex flex-wrap items-center justify-between gap-4">
|
|
<h2 className="text-xl font-semibold">{t("certificates.title")}</h2>
|
|
<Button disabled={!(ready && canCreate)} onClick={() => setEditor("new")}>
|
|
{t("certificates.new")}
|
|
</Button>
|
|
</header>
|
|
<Input
|
|
aria-label={t("certificates.search")}
|
|
onChange={(event) => setSearch(event.target.value)}
|
|
placeholder={t("certificates.search")}
|
|
value={search}
|
|
/>
|
|
{(query.isPending || profiles.isPending || inventory.isPending || resources.isPending) && (
|
|
<p role="status">{t("loading")}</p>
|
|
)}
|
|
<CertificateFeedback
|
|
error={query.error ?? profiles.error ?? inventory.error ?? resources.error}
|
|
/>
|
|
{(query.isError || profiles.isError || inventory.isError || resources.isError) && (
|
|
<Button
|
|
onClick={() => {
|
|
Promise.all([
|
|
query.refetch(),
|
|
profiles.refetch(),
|
|
inventory.refetch(),
|
|
resources.refetch(),
|
|
]).catch(() => undefined);
|
|
}}
|
|
variant="outline"
|
|
>
|
|
{t("retry")}
|
|
</Button>
|
|
)}
|
|
{ready && (
|
|
<Card>
|
|
<CardContent className="divide-y pt-3">
|
|
{!filtered.length && (
|
|
<p className="py-4 text-muted-foreground">{t("certificates.empty")}</p>
|
|
)}
|
|
{filtered.map((item) => (
|
|
<div className="flex flex-wrap justify-between gap-4 py-4" key={item.id}>
|
|
<div>
|
|
<h3 className="font-medium">{resource(item.values.resourceId)}</h3>
|
|
<p className="text-sm text-muted-foreground">{label(item.values.profileId)}</p>
|
|
<p className="text-sm">{t(`mail.${item.values.state}`)}</p>
|
|
{item.values.location && (
|
|
<p className="text-sm">
|
|
{t("certificates.locationValue", { location: item.values.location })}
|
|
</p>
|
|
)}
|
|
{item.expiresOn && (
|
|
<p className="text-sm">
|
|
{t("certificates.expiryValue", {
|
|
date: new Intl.DateTimeFormat(i18n.language, {
|
|
dateStyle: "medium",
|
|
timeZone: "UTC",
|
|
}).format(new Date(item.expiresOn)),
|
|
})}
|
|
{item.expired ? ` · ${t("certificates.expired")}` : ""}
|
|
</p>
|
|
)}
|
|
{item.needsReview && (
|
|
<p className="text-sm text-amber-700">{t("certificates.review")}</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-start gap-2">
|
|
<Button onClick={() => setView(item.id)} size="sm" variant="outline">
|
|
{t("mail.history")}
|
|
</Button>
|
|
{!["removed", "never_configured"].includes(item.values.state) && (
|
|
<Button onClick={() => setEditor(item.id)} size="sm" variant="outline">
|
|
{t("edit")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
{editor && (
|
|
<FormDialog
|
|
cancelLabel={t("cancel")}
|
|
formActions
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setEditor(undefined);
|
|
}
|
|
}}
|
|
open
|
|
title={t(edited ? "certificates.edit" : "certificates.new")}
|
|
wide
|
|
>
|
|
<CertificateConfigurationForm
|
|
company={company}
|
|
configuration={edited}
|
|
csrfToken={csrfToken}
|
|
initialProfileId={initialProfileId ?? ""}
|
|
onSaved={() => setEditor(undefined)}
|
|
/>
|
|
</FormDialog>
|
|
)}
|
|
{viewed && (
|
|
<FormDialog
|
|
cancelLabel={t("close")}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setView(undefined);
|
|
}
|
|
}}
|
|
open
|
|
title={t("mail.history")}
|
|
wide
|
|
>
|
|
<CertificateHistory configuration={viewed} />
|
|
</FormDialog>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
function CertificateHistory({ configuration }: { configuration: CertificateConfigurationDTO }) {
|
|
const { t, i18n } = useTranslation("companies");
|
|
return (
|
|
<ol className="space-y-3">
|
|
{[...configuration.history].reverse().map((revision) => (
|
|
<li className="rounded-md border p-3 text-sm" key={revision.id}>
|
|
<p className="font-medium">
|
|
{new Intl.DateTimeFormat(i18n.language, {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
}).format(new Date(revision.recordedAt))}{" "}
|
|
· {revision.actorName}
|
|
</p>
|
|
<p>{t(`mail.${revision.values.state}`)}</p>
|
|
{revision.values.location && (
|
|
<p>{t("certificates.locationValue", { location: revision.values.location })}</p>
|
|
)}
|
|
{revision.values.notes && (
|
|
<p className="mt-2 whitespace-pre-wrap">{revision.values.notes}</p>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
);
|
|
}
|