metis-desk/modules/resources/src/web/resources-page.tsx

153 lines
5.3 KiB
TypeScript

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 { Label } from "@repo/shadcn-ui/components/label";
import { useQuery } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { useId, useState } from "react";
import { useTranslation } from "react-i18next";
import type { ResourceDTO } from "../common";
import { catalogApi, catalogKeys } from "./catalog-api";
import { inventoryApi, inventoryKeys } from "./inventory-api";
import { ResourceDetail } from "./resource-detail";
import { InventoryFeedback, ResourceForm } from "./resource-form";
export function ResourcesPage({
companyId,
csrfToken,
relatedContent,
}: {
companyId: string;
csrfToken: string;
relatedContent?: (resource: ResourceDTO) => ReactNode;
}) {
const { t } = useTranslation("resources");
const searchId = useId();
const [search, setSearch] = useState("");
const [editor, setEditor] = useState<string>();
const [viewId, setViewId] = useState<string>();
const query = useQuery({
queryKey: inventoryKeys.company(companyId),
queryFn: () => inventoryApi.get(companyId),
});
const catalog = useQuery({
queryKey: catalogKeys.scope(companyId),
queryFn: () => catalogApi.get(companyId),
});
const edited = query.data?.resources.find((item) => item.id === editor);
const viewed = query.data?.resources.find((item) => item.id === viewId);
const visible =
query.data?.resources.filter((item) =>
`${item.values.name} ${item.specification.templateName}`
.toLocaleLowerCase()
.includes(search.toLocaleLowerCase())
) ?? [];
return (
<div className="space-y-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<h2 className="text-2xl font-semibold">{t("inventory")}</h2>
<Button disabled={!(query.data && catalog.data)} onClick={() => setEditor("new")}>
{t("newResource")}
</Button>
</div>
<p className="text-sm text-muted-foreground">{t("resourceScope")}</p>
{(query.isPending || catalog.isPending) && <p role="status">{t("loading")}</p>}
<InventoryFeedback error={query.error ?? catalog.error} />
{(query.isError || catalog.isError) && (
<Button
onClick={() => {
Promise.all([query.refetch(), catalog.refetch()]).catch(() => undefined);
}}
variant="outline"
>
{t("retry")}
</Button>
)}
<Card>
<CardContent className="space-y-4 pt-6">
<Label htmlFor={searchId}>{t("search")}</Label>
<Input id={searchId} onChange={(event) => setSearch(event.target.value)} value={search} />
{query.data &&
(visible.length ? (
<div className="divide-y">
{visible.map((item) => (
<div
className="flex flex-wrap items-center justify-between gap-4 py-4"
key={item.id}
>
<div>
<h3 className="font-medium">{item.values.name}</h3>
<p className="text-sm text-muted-foreground">
{item.specification.templateName} ·{" "}
{t(item.values.active ? "active" : "inactive")}
</p>
</div>
<div className="flex gap-2">
<Button onClick={() => setViewId(item.id)} size="sm" variant="outline">
{t("viewResource")}
</Button>
<Button
disabled={!catalog.data}
onClick={() => setEditor(item.id)}
size="sm"
variant="outline"
>
{t("edit")}
</Button>
</div>
</div>
))}
</div>
) : (
<p className="text-muted-foreground">{t("empty")}</p>
))}
</CardContent>
</Card>
{editor && catalog.data && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setEditor(undefined);
}
}}
open
title={t(edited ? "editResource" : "newResource")}
wide
>
<ResourceForm
companyId={companyId}
csrfToken={csrfToken}
items={catalog.data.items}
onSaved={(id) => {
setEditor(undefined);
setViewId(id);
}}
resource={edited}
/>
</FormDialog>
)}
{viewed && (
<FormDialog
cancelLabel={t("close")}
onOpenChange={(open) => {
if (!open) {
setViewId(undefined);
}
}}
open
title={viewed.values.name}
wide
>
<ResourceDetail references={query.data?.references ?? []} resource={viewed} />
{relatedContent?.(viewed)}
</FormDialog>
)}
</div>
);
}