metis-desk/modules/resources/src/web/resources-page.tsx
David bada13a50e feat(dashboard): implement dashboard schemas and API integration
- Added Zod schemas for dashboard queries, entries, summaries, and companies.
- Created dashboard API functions for fetching summary, work, attention, activity, and companies data.
- Developed dashboard components including BlockState, DashboardDetailsDialog, and DashboardPage for rendering dashboard information.
- Implemented quick create functionality for companies, tickets, interventions, assets, and resources.
- Introduced a new ResourceDashboardReader for querying resource-related data.
- Added a StatisticCard component for displaying key performance indicators in the dashboard.
2026-10-08 13:27:27 +02:00

194 lines
6.0 KiB
TypeScript

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 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,
initialId,
initialCreate = false,
}: {
companyId: string;
csrfToken: string;
relatedContent?: (resource: ResourceDTO) => ReactNode;
initialId?: string | undefined;
initialCreate?: boolean | undefined;
}) {
const { t, i18n } = useTranslation("resources");
const _searchId = useId();
const [search, setSearch] = useState("");
const [editor, setEditor] = useState<string | undefined>(() => initialEditor(initialCreate));
const [viewId, setViewId] = useState<string | undefined>(initialId);
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">
<EntityListHeader
actions={
<>
{" "}
<Button disabled={!(query.data && catalog.data)} onClick={() => setEditor("new")}>
{t("newResource")}
</Button>{" "}
</>
}
description={t("resourceScope")}
locale={i18n.language}
title={t("inventory")}
/>
{(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>
)}
<EntityTable
columns={[
{
id: "name",
label: t("name"),
value: (item) => item.values.name,
hideable: false,
render: (item) => (
<button
className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setViewId(item.id)}
type="button"
>
{item.values.name}
</button>
),
},
{
id: "template",
label: t("template"),
value: (item) => item.specification.templateName,
render: (item) => <>{item.specification.templateName}</>,
},
{
id: "state",
label: t("status"),
value: (item) => t(item.values.active ? "active" : "inactive"),
render: (item) => (
<EntityStatus active={item.values.active}>
{t(item.values.active ? "active" : "inactive")}
</EntityStatus>
),
},
{
id: "actions",
label: t("actions"),
hideable: false,
render: (item) => (
<EntityRowActions label={t("actions")}>
{" "}
<div className="contents">
<EntityAction onClick={() => setViewId(item.id)}>
{t("viewResource")}
</EntityAction>
<EntityAction disabled={!catalog.data} onClick={() => setEditor(item.id)}>
{t("edit")}
</EntityAction>
</div>{" "}
</EntityRowActions>
),
},
]}
emptyMessage={t("empty")}
items={visible}
loading={query.isPending || catalog.isPending}
locale={i18n.language}
onSearchChange={setSearch}
search={search}
searchPlaceholder={t("search")}
title={t("inventory")}
unavailable={(!query.data && query.isError) || (!catalog.data && catalog.isError)}
/>
{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>
);
}
function initialEditor(create: boolean) {
return create ? "new" : undefined;
}