- 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.
194 lines
6.0 KiB
TypeScript
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;
|
|
}
|