168 lines
5.9 KiB
TypeScript
168 lines
5.9 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 { useQuery } from "@tanstack/react-query";
|
||
|
|
import { useState } from "react";
|
||
|
|
import { useTranslation } from "react-i18next";
|
||
|
|
|
||
|
|
import type { WorkstationDTO } from "../common";
|
||
|
|
|
||
|
|
import { InventoryFeedback, type InventoryLookups } from "./asset-form";
|
||
|
|
import { inventoryApi, inventoryKeys } from "./inventory-api";
|
||
|
|
import { WorkstationForm } from "./workstation-form";
|
||
|
|
export function WorkstationsPage({
|
||
|
|
companyId,
|
||
|
|
csrfToken,
|
||
|
|
lookups,
|
||
|
|
}: {
|
||
|
|
companyId: string;
|
||
|
|
csrfToken: string;
|
||
|
|
lookups: InventoryLookups;
|
||
|
|
}) {
|
||
|
|
const { t, i18n } = useTranslation("assets");
|
||
|
|
const [search, setSearch] = useState("");
|
||
|
|
const [editor, setEditor] = useState<string>();
|
||
|
|
const [view, setView] = useState<string>();
|
||
|
|
const query = useQuery({
|
||
|
|
queryKey: inventoryKeys.company(companyId),
|
||
|
|
queryFn: () => inventoryApi.get(companyId),
|
||
|
|
});
|
||
|
|
const stations = query.data?.workstations ?? [];
|
||
|
|
const assets = query.data?.assets ?? [];
|
||
|
|
const edited = stations.find((item) => item.id === editor);
|
||
|
|
const viewed = stations.find((item) => item.id === view);
|
||
|
|
const personName = (id: string | null) =>
|
||
|
|
lookups.people.find((item) => item.id === id)?.name ?? t("noAssignment");
|
||
|
|
const siteName = (id: string | null) =>
|
||
|
|
lookups.sites.find((item) => item.id === id)?.name ?? t("noAssignment");
|
||
|
|
const members = (ids: readonly string[]) =>
|
||
|
|
ids.map((id) => assets.find((item) => item.id === id)?.values.name ?? id).join(" · ") ||
|
||
|
|
t("noWorkstationAssets");
|
||
|
|
const filtered = stations.filter((item) =>
|
||
|
|
[item.values.name, personName(item.values.personId), siteName(item.values.siteId)].some(
|
||
|
|
(value) => value.toLocaleLowerCase().includes(search.trim().toLocaleLowerCase())
|
||
|
|
)
|
||
|
|
);
|
||
|
|
const summary = (item: WorkstationDTO) => (
|
||
|
|
<>
|
||
|
|
<h3 className="font-medium">
|
||
|
|
{item.values.name} · {t(item.values.active ? "active" : "inactive")}
|
||
|
|
</h3>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
{siteName(item.values.siteId)} · {personName(item.values.personId)} ·{" "}
|
||
|
|
{t("workstationCount", { count: item.values.assetIds.length })}
|
||
|
|
</p>
|
||
|
|
<p className="mt-2 text-sm">{members(item.values.assetIds)}</p>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
return (
|
||
|
|
<div className="space-y-5">
|
||
|
|
<header className="flex flex-wrap items-center justify-between gap-4">
|
||
|
|
<h2 className="text-xl font-semibold">{t("workstations")}</h2>
|
||
|
|
<Button disabled={!query.data} onClick={() => setEditor("new")}>
|
||
|
|
{t("newWorkstation")}
|
||
|
|
</Button>
|
||
|
|
</header>
|
||
|
|
<Input
|
||
|
|
aria-label={t("search")}
|
||
|
|
onChange={(event) => setSearch(event.target.value)}
|
||
|
|
placeholder={t("search")}
|
||
|
|
value={search}
|
||
|
|
/>
|
||
|
|
{query.isPending && <p role="status">{t("loading")}</p>}
|
||
|
|
<InventoryFeedback error={query.error} />
|
||
|
|
{query.isError && (
|
||
|
|
<Button
|
||
|
|
onClick={() => {
|
||
|
|
query.refetch().catch(() => undefined);
|
||
|
|
}}
|
||
|
|
variant="outline"
|
||
|
|
>
|
||
|
|
{t("retry")}
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
{query.data && (
|
||
|
|
<Card>
|
||
|
|
<CardContent className="divide-y pt-3">
|
||
|
|
{!filtered.length && (
|
||
|
|
<p className="py-4 text-muted-foreground">
|
||
|
|
{t(stations.length ? "noResults" : "noWorkstations")}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{filtered.map((item) => (
|
||
|
|
<div className="flex flex-wrap items-center justify-between gap-4 py-4" key={item.id}>
|
||
|
|
<div className="min-w-0 break-words">{summary(item)}</div>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Button onClick={() => setView(item.id)} size="sm" variant="outline">
|
||
|
|
{t("workstationHistory")}
|
||
|
|
</Button>
|
||
|
|
<Button onClick={() => setEditor(item.id)} size="sm" variant="outline">
|
||
|
|
{t("edit")}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)}
|
||
|
|
{editor && (
|
||
|
|
<FormDialog
|
||
|
|
cancelLabel={t("cancel")}
|
||
|
|
formActions
|
||
|
|
onOpenChange={(open) => {
|
||
|
|
if (!open) {
|
||
|
|
setEditor(undefined);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
open
|
||
|
|
title={t(edited ? "editWorkstation" : "newWorkstation")}
|
||
|
|
wide
|
||
|
|
>
|
||
|
|
<WorkstationForm
|
||
|
|
companyId={companyId}
|
||
|
|
csrfToken={csrfToken}
|
||
|
|
lookups={lookups}
|
||
|
|
onSaved={() => setEditor(undefined)}
|
||
|
|
workstation={edited}
|
||
|
|
/>
|
||
|
|
</FormDialog>
|
||
|
|
)}
|
||
|
|
{viewed && (
|
||
|
|
<FormDialog
|
||
|
|
cancelLabel={t("close")}
|
||
|
|
onOpenChange={(open) => {
|
||
|
|
if (!open) {
|
||
|
|
setView(undefined);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
open
|
||
|
|
title={t("workstationHistory")}
|
||
|
|
wide
|
||
|
|
>
|
||
|
|
<ol className="space-y-4">
|
||
|
|
{[...viewed.history].reverse().map((revision) => (
|
||
|
|
<li className="space-y-2 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))}{" "}
|
||
|
|
· {t("recordedBy", { name: revision.actorName })}
|
||
|
|
</p>
|
||
|
|
<p>
|
||
|
|
{revision.values.name} · {t(revision.values.active ? "active" : "inactive")}
|
||
|
|
</p>
|
||
|
|
<p>
|
||
|
|
{siteName(revision.values.siteId)} · {personName(revision.values.personId)}
|
||
|
|
</p>
|
||
|
|
<p>{members(revision.values.assetIds)}</p>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ol>
|
||
|
|
</FormDialog>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|