metis-desk/modules/companies/src/web/applications/application-configurations-page.tsx

260 lines
9.0 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 {
EntityAction,
EntityListHeader,
EntityRowActions,
EntityStatus,
EntityTable,
} from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import type { ApplicationConfigurationDTO, CompanyDTO } from "../../common";
import { profileApi, profileKeys } from "../profiles/work-profile-api";
import { applicationApi, applicationKeys } from "./application-api";
import { ApplicationConfigurationForm } from "./application-configuration-form";
import { ApplicationFeedback } from "./application-feedback";
export function ApplicationConfigurationsPage({
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: applicationKeys.company(company.id),
queryFn: () => applicationApi.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">
<EntityListHeader
actions={
<>
{" "}
<Button disabled={!(ready && canCreate)} onClick={() => setEditor("new")}>
{t("applications.new")}
</Button>{" "}
</>
}
locale={i18n.language}
title={t("applications.title")}
/>
{(query.isPending || profiles.isPending || inventory.isPending || resources.isPending) && (
<p role="status">{t("loading")}</p>
)}
<ApplicationFeedback
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 && (
<EntityTable
columns={[
{
id: "name",
label: t("applications.title"),
value: (item) => resource(item.values.resourceId),
hideable: false,
render: (item) => (
<button
className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(item.id)}
type="button"
>
{resource(item.values.resourceId)}
</button>
),
},
{
id: "profile",
label: t("profiles.title"),
value: (item) => label(item.values.profileId),
render: (item) => <>{label(item.values.profileId)}</>,
},
{
id: "state",
label: t("status"),
value: (item) => t(`mail.${item.values.state}`),
render: (item) => (
<EntityStatus active={item.values.state === "configured"}>
{t(`mail.${item.values.state}`)}
</EntityStatus>
),
},
{
id: "delivery",
label: t("applications.delivery"),
value: (item) => t(`applications.${item.values.delivery}`),
render: (item) => <>{t(`applications.${item.values.delivery}`)}</>,
},
{
id: "version",
label: t("applications.version"),
value: (item) => item.values.version,
render: (item) => <>{item.values.version || "—"}</>,
},
{
id: "review",
label: t("profiles.review"),
value: (item) => item.needsReview,
render: (item) => <>{item.needsReview ? t("applications.review") : "—"}</>,
},
{
id: "actions",
label: t("actions"),
hideable: false,
render: (item) => (
<EntityRowActions label={t("actions")}>
{" "}
<div className="contents">
<EntityAction onClick={() => setView(item.id)}>
{t("mail.history")}
</EntityAction>
{!["removed", "never_configured"].includes(item.values.state) && (
<EntityAction onClick={() => setEditor(item.id)}>{t("edit")}</EntityAction>
)}
</div>{" "}
</EntityRowActions>
),
},
]}
emptyMessage={t("applications.empty")}
items={filtered}
locale={i18n.language}
onSearchChange={setSearch}
search={search}
searchPlaceholder={t("applications.search")}
title={t("applications.title")}
/>
)}
{editor && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setEditor(undefined);
}
}}
open
title={t(edited ? "applications.edit" : "applications.new")}
wide
>
<ApplicationConfigurationForm
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
>
<ApplicationHistory configuration={viewed} />
</FormDialog>
)}
</div>
);
}
function ApplicationHistory({ configuration }: { configuration: ApplicationConfigurationDTO }) {
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(`applications.${revision.values.delivery}`)} · {t(`mail.${revision.values.state}`)}
</p>
{revision.values.version && (
<p>{t("applications.versionValue", { version: revision.values.version })}</p>
)}
{revision.values.notes && (
<p className="mt-2 whitespace-pre-wrap">{revision.values.notes}</p>
)}
</li>
))}
</ol>
);
}