2026-10-04 19:04:14 +00:00
|
|
|
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");
|
2026-10-05 07:53:29 +00:00
|
|
|
const members = (ids: readonly string[], empty = "noWorkstationAssets") =>
|
2026-10-04 19:04:14 +00:00
|
|
|
ids.map((id) => assets.find((item) => item.id === id)?.values.name ?? id).join(" · ") ||
|
2026-10-05 07:53:29 +00:00
|
|
|
t(empty);
|
2026-10-04 19:04:14 +00:00
|
|
|
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)} ·{" "}
|
2026-10-05 07:53:29 +00:00
|
|
|
{t("workstationCount", {
|
|
|
|
|
count: item.values.assetIds.length + item.values.sharedAssetIds.length,
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="mt-2 text-sm">
|
|
|
|
|
{t("exclusiveWorkstationAssets")}: {members(item.values.assetIds)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="mt-1 text-sm">
|
|
|
|
|
{t("sharedWorkstationAssets")}:{" "}
|
|
|
|
|
{members(item.values.sharedAssetIds, "noSharedWorkstationAssets")}
|
2026-10-04 19:04:14 +00:00
|
|
|
</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>
|
2026-10-05 07:53:29 +00:00
|
|
|
<p>
|
|
|
|
|
{t("exclusiveWorkstationAssets")}: {members(revision.values.assetIds)}
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
{t("sharedWorkstationAssets")}:{" "}
|
|
|
|
|
{members(revision.values.sharedAssetIds, "noSharedWorkstationAssets")}
|
|
|
|
|
</p>
|
2026-10-04 19:04:14 +00:00
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ol>
|
|
|
|
|
</FormDialog>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|