260 lines
9.0 KiB
TypeScript
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>
|
|
);
|
|
}
|