2026-10-05 10:02:04 +00:00
|
|
|
import {FolderMappingsPage} from "../folders/folder-mappings-page";
|
2026-10-04 16:36:09 +00:00
|
|
|
import { AssetCatalogPage } from "@metis/assets/client";
|
2026-10-03 10:36:03 +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";
|
2026-10-04 18:16:39 +00:00
|
|
|
import {
|
|
|
|
|
BoxIcon,
|
|
|
|
|
BriefcaseIcon,
|
|
|
|
|
KeyRoundIcon,
|
|
|
|
|
LayoutTemplateIcon,
|
2026-10-04 21:06:07 +00:00
|
|
|
MailIcon,
|
2026-10-04 18:16:39 +00:00
|
|
|
MapPinIcon,
|
|
|
|
|
UsersIcon,
|
|
|
|
|
} from "@repo/shadcn-ui/lib/icons";
|
2026-10-03 10:36:03 +00:00
|
|
|
import { useQuery } from "@tanstack/react-query";
|
2026-10-04 18:16:39 +00:00
|
|
|
import type { ReactNode } from "react";
|
2026-10-03 10:36:03 +00:00
|
|
|
import { useState } from "react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
|
|
|
|
|
import type { CompanyDTO } from "../../common";
|
2026-10-03 13:19:20 +00:00
|
|
|
import { AccessWorkspacePage } from "../access/access-workspace";
|
2026-10-03 10:36:03 +00:00
|
|
|
import { companyApi, companyKeys } from "../api/company-api";
|
2026-10-04 21:06:07 +00:00
|
|
|
import { MailConfigurationsPage } from "../mail/mail-configurations-page";
|
2026-10-05 08:45:02 +00:00
|
|
|
import { MailboxesPage } from "../mail/mailboxes-page";
|
2026-10-05 07:53:29 +00:00
|
|
|
import { WorkProfilesPage } from "../profiles/work-profiles-page";
|
2026-10-03 10:36:03 +00:00
|
|
|
|
|
|
|
|
import { CompanyFeedback } from "./company-feedback";
|
|
|
|
|
import { CompanyForm } from "./company-form";
|
2026-10-04 16:36:09 +00:00
|
|
|
import { CompanyInventory } from "./company-inventory";
|
2026-10-03 11:13:15 +00:00
|
|
|
import { DepartmentSection } from "./department-section";
|
|
|
|
|
import { PeopleSection } from "./people-section";
|
2026-10-03 10:36:03 +00:00
|
|
|
import { SiteForm } from "./site-form";
|
|
|
|
|
|
|
|
|
|
function SiteSection({ company, csrfToken }: { company: CompanyDTO; csrfToken: string }) {
|
|
|
|
|
const { t } = useTranslation("companies");
|
|
|
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
|
|
|
const [selectedId, setSelectedId] = useState<string | undefined>();
|
|
|
|
|
const selected = company.sites.find((site) => site.id === selectedId);
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="pt-6">
|
|
|
|
|
<div className="mb-4 flex items-center justify-between gap-4">
|
|
|
|
|
<h2 className="text-lg font-semibold">{t("sites")}</h2>
|
|
|
|
|
<Button onClick={() => setCreateOpen(true)}>{t("newSite")}</Button>
|
|
|
|
|
</div>
|
|
|
|
|
{company.sites.length === 0 ? (
|
|
|
|
|
<p className="text-muted-foreground">{t("noSites")}</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="divide-y">
|
|
|
|
|
{company.sites.map((site) => (
|
|
|
|
|
<div className="flex items-start justify-between gap-4 py-4" key={site.id}>
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<h3 className="font-medium">{site.name}</h3>
|
|
|
|
|
<p className="mt-2 whitespace-pre-wrap break-words text-sm text-muted-foreground">
|
|
|
|
|
{site.address ?? t("noAddress")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button onClick={() => setSelectedId(site.id)} size="sm" variant="outline">
|
|
|
|
|
{t("edit")}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
<FormDialog
|
|
|
|
|
cancelLabel={t("cancel")}
|
2026-10-03 16:23:38 +00:00
|
|
|
formActions
|
2026-10-03 10:36:03 +00:00
|
|
|
onOpenChange={setCreateOpen}
|
|
|
|
|
open={createOpen}
|
|
|
|
|
title={t("newSite")}
|
|
|
|
|
>
|
|
|
|
|
<SiteForm
|
|
|
|
|
companyId={company.id}
|
|
|
|
|
csrfToken={csrfToken}
|
|
|
|
|
onSaved={() => setCreateOpen(false)}
|
|
|
|
|
showTitle={false}
|
|
|
|
|
/>
|
|
|
|
|
</FormDialog>
|
|
|
|
|
<FormDialog
|
|
|
|
|
cancelLabel={t("cancel")}
|
2026-10-03 16:23:38 +00:00
|
|
|
formActions
|
2026-10-03 10:36:03 +00:00
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
setSelectedId(undefined);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
open={selected !== undefined}
|
|
|
|
|
title={t("editSite")}
|
|
|
|
|
>
|
|
|
|
|
{selected && (
|
|
|
|
|
<SiteForm
|
|
|
|
|
companyId={company.id}
|
|
|
|
|
csrfToken={csrfToken}
|
|
|
|
|
key={selected.id}
|
|
|
|
|
onSaved={() => setSelectedId(undefined)}
|
|
|
|
|
showTitle={false}
|
|
|
|
|
site={selected}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</FormDialog>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-10-04 19:04:14 +00:00
|
|
|
type CompanyTab =
|
|
|
|
|
| "people"
|
|
|
|
|
| "departments"
|
|
|
|
|
| "sites"
|
|
|
|
|
| "access"
|
|
|
|
|
| "assets"
|
|
|
|
|
| "inventory"
|
2026-10-04 21:06:07 +00:00
|
|
|
| "workstations"
|
2026-10-05 10:02:04 +00:00
|
|
|
| "folders"
|
2026-10-05 08:45:02 +00:00
|
|
|
| "mailboxes"
|
2026-10-05 07:53:29 +00:00
|
|
|
| "mail"
|
|
|
|
|
| "profiles";
|
2026-10-04 16:36:09 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
export function CompanyDetail({
|
|
|
|
|
id,
|
|
|
|
|
csrfToken,
|
|
|
|
|
onBack,
|
2026-10-04 18:16:39 +00:00
|
|
|
embedded = false,
|
|
|
|
|
accountNavigation,
|
2026-10-03 10:36:03 +00:00
|
|
|
}: {
|
|
|
|
|
id: string;
|
|
|
|
|
csrfToken: string;
|
|
|
|
|
onBack: () => void;
|
2026-10-04 18:16:39 +00:00
|
|
|
embedded?: boolean;
|
|
|
|
|
accountNavigation?: ReactNode;
|
2026-10-03 10:36:03 +00:00
|
|
|
}) {
|
|
|
|
|
const { t } = useTranslation("companies");
|
2026-10-04 16:36:09 +00:00
|
|
|
const { t: assetTranslation } = useTranslation("assets");
|
2026-10-03 13:19:20 +00:00
|
|
|
const { t: accessTranslation } = useTranslation("access");
|
2026-10-03 10:36:03 +00:00
|
|
|
const query = useQuery({ queryKey: companyKeys.detail(id), queryFn: () => companyApi.get(id) });
|
|
|
|
|
const [editOpen, setEditOpen] = useState(false);
|
2026-10-04 21:06:07 +00:00
|
|
|
const [tab, setTab] = useState<CompanyTab>(embedded ? "inventory" : "people");
|
2026-10-03 10:36:03 +00:00
|
|
|
const company = query.data?.company;
|
2026-10-04 18:16:39 +00:00
|
|
|
const layout = embedded
|
|
|
|
|
? {
|
|
|
|
|
root: "company-space",
|
|
|
|
|
heading: "company-space-heading",
|
|
|
|
|
navigation: "company-space-navigation",
|
|
|
|
|
main: "company-space-main",
|
|
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
root: "space-y-6",
|
|
|
|
|
heading: "flex flex-wrap items-start justify-between gap-4",
|
|
|
|
|
navigation: "flex flex-wrap gap-2",
|
|
|
|
|
main: undefined,
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
return (
|
2026-10-04 18:16:39 +00:00
|
|
|
<div className={layout.root}>
|
|
|
|
|
{!embedded && (
|
|
|
|
|
<Button onClick={onBack} variant="outline">
|
|
|
|
|
{t("back")}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2026-10-03 10:36:03 +00:00
|
|
|
{query.isPending && <p role="status">{t("loading")}</p>}
|
|
|
|
|
<CompanyFeedback error={query.error} />
|
|
|
|
|
{query.isError && (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
query.refetch().catch(() => undefined);
|
|
|
|
|
}}
|
|
|
|
|
variant="outline"
|
|
|
|
|
>
|
|
|
|
|
{t("retry")}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2026-10-04 18:16:39 +00:00
|
|
|
{embedded && <footer className="company-space-account">{accountNavigation}</footer>}
|
2026-10-03 10:36:03 +00:00
|
|
|
{company && (
|
|
|
|
|
<>
|
2026-10-04 18:16:39 +00:00
|
|
|
<div className={layout.heading}>
|
2026-10-03 10:36:03 +00:00
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-semibold">{company.name}</h1>
|
|
|
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
|
|
|
{t(company.active ? "activeCompany" : "inactiveCompany")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button onClick={() => setEditOpen(true)} variant="outline">
|
|
|
|
|
{t("editCompany")}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<FormDialog
|
|
|
|
|
cancelLabel={t("cancel")}
|
2026-10-03 16:23:38 +00:00
|
|
|
formActions
|
2026-10-03 10:36:03 +00:00
|
|
|
onOpenChange={setEditOpen}
|
|
|
|
|
open={editOpen}
|
|
|
|
|
title={t("editCompany")}
|
|
|
|
|
>
|
|
|
|
|
<CompanyForm
|
|
|
|
|
company={company}
|
|
|
|
|
csrfToken={csrfToken}
|
|
|
|
|
key={company.id}
|
|
|
|
|
onSaved={() => setEditOpen(false)}
|
|
|
|
|
showTitle={false}
|
|
|
|
|
/>
|
|
|
|
|
</FormDialog>
|
2026-10-04 18:16:39 +00:00
|
|
|
<nav aria-label={t("companySections")} className={layout.navigation}>
|
|
|
|
|
{(
|
|
|
|
|
[
|
|
|
|
|
["inventory", assetTranslation("inventory"), BoxIcon],
|
2026-10-04 19:04:14 +00:00
|
|
|
["workstations", assetTranslation("workstations"), BriefcaseIcon],
|
2026-10-05 07:53:29 +00:00
|
|
|
["profiles", t("profiles.title"), UsersIcon],
|
2026-10-04 18:16:39 +00:00
|
|
|
["people", `${t("people")} (${company.people.length})`, UsersIcon],
|
|
|
|
|
[
|
|
|
|
|
"departments",
|
|
|
|
|
`${t("departments")} (${company.departments.length})`,
|
|
|
|
|
BriefcaseIcon,
|
|
|
|
|
],
|
|
|
|
|
["sites", `${t("sites")} (${company.sites.length})`, MapPinIcon],
|
|
|
|
|
["access", accessTranslation("title"), KeyRoundIcon],
|
2026-10-05 10:02:04 +00:00
|
|
|
["folders",t("folders.title"),MailIcon],
|
2026-10-05 08:45:02 +00:00
|
|
|
["mailboxes", t("mailbox.title"), MailIcon],
|
2026-10-04 21:06:07 +00:00
|
|
|
["mail", t("mail.title"), MailIcon],
|
2026-10-04 18:16:39 +00:00
|
|
|
["assets", assetTranslation("companyTitle"), LayoutTemplateIcon],
|
|
|
|
|
] as const
|
|
|
|
|
).map(([section, label, Icon]) => (
|
|
|
|
|
<Button
|
|
|
|
|
aria-pressed={tab === section}
|
|
|
|
|
key={section}
|
|
|
|
|
onClick={() => setTab(section)}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
>
|
|
|
|
|
<Icon aria-hidden="true" className="size-4 shrink-0" />
|
|
|
|
|
{label}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
2026-10-03 10:36:03 +00:00
|
|
|
</nav>
|
2026-10-04 18:16:39 +00:00
|
|
|
|
|
|
|
|
<main className={layout.main} key={tab}>
|
|
|
|
|
{embedded && <div className="company-space-breadcrumb">{company.name}</div>}
|
|
|
|
|
<CompanySection company={company} csrfToken={csrfToken} tab={tab} />
|
|
|
|
|
</main>
|
2026-10-03 10:36:03 +00:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-10-04 16:36:09 +00:00
|
|
|
|
|
|
|
|
function CompanySection({
|
|
|
|
|
company,
|
|
|
|
|
csrfToken,
|
|
|
|
|
tab,
|
|
|
|
|
}: {
|
|
|
|
|
company: CompanyDTO;
|
|
|
|
|
csrfToken: string;
|
|
|
|
|
tab: CompanyTab;
|
|
|
|
|
}) {
|
|
|
|
|
switch (tab) {
|
|
|
|
|
case "people":
|
|
|
|
|
return <PeopleSection company={company} csrfToken={csrfToken} />;
|
|
|
|
|
case "departments":
|
|
|
|
|
return <DepartmentSection company={company} csrfToken={csrfToken} />;
|
|
|
|
|
case "sites":
|
|
|
|
|
return <SiteSection company={company} csrfToken={csrfToken} />;
|
|
|
|
|
case "access":
|
|
|
|
|
return (
|
|
|
|
|
<AccessWorkspacePage csrfToken={csrfToken} people={company.people} scope={company.id} />
|
|
|
|
|
);
|
2026-10-05 07:53:29 +00:00
|
|
|
case "profiles":
|
|
|
|
|
return <WorkProfilesPage company={company} csrfToken={csrfToken} />;
|
2026-10-05 10:02:04 +00:00
|
|
|
case "folders":
|
|
|
|
|
return <FolderMappingsPage company={company} csrfToken={csrfToken} />;
|
|
|
|
|
case "mailboxes":
|
2026-10-05 08:45:02 +00:00
|
|
|
return <MailboxesPage company={company} csrfToken={csrfToken} />;
|
2026-10-04 21:06:07 +00:00
|
|
|
case "mail":
|
|
|
|
|
return <MailConfigurationsPage company={company} csrfToken={csrfToken} />;
|
2026-10-04 19:04:14 +00:00
|
|
|
case "workstations":
|
|
|
|
|
return <CompanyInventory company={company} csrfToken={csrfToken} workstations />;
|
2026-10-04 16:36:09 +00:00
|
|
|
case "inventory":
|
|
|
|
|
return <CompanyInventory company={company} csrfToken={csrfToken} />;
|
|
|
|
|
case "assets":
|
|
|
|
|
return <AssetCatalogPage csrfToken={csrfToken} scope={company.id} />;
|
|
|
|
|
}
|
|
|
|
|
}
|