- Add mailbox persistence mapper for domain object mapping. - Create Sequelize repository for mailbox data access. - Define common mailbox schemas and types for validation. - Implement mailbox API for frontend communication. - Develop mailbox form component for creating and editing mailboxes. - Add internationalization support for mailbox labels and messages. - Create mailbox use form for managing mailbox usage periods. - Build mailboxes page to display and manage mailboxes and their usage.
208 lines
7.6 KiB
TypeScript
208 lines
7.6 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 { CompanyDTO } from "../../common";
|
|
|
|
import { MailFeedback } from "./mail-feedback";
|
|
import { mailboxApi, mailboxKeys } from "./mailbox-api";
|
|
import { MailboxForm } from "./mailbox-form";
|
|
import { MailboxUseForm } from "./mailbox-use-form";
|
|
export function MailboxesPage({ company, csrfToken }: { company: CompanyDTO; csrfToken: string }) {
|
|
const { t, i18n } = useTranslation("companies");
|
|
const [editor, setEditor] = useState<string>();
|
|
const [useEditor, setUseEditor] = useState<string>();
|
|
const [view, setView] = useState<string>();
|
|
const [search, setSearch] = useState("");
|
|
const query = useQuery({
|
|
queryKey: mailboxKeys.company(company.id),
|
|
queryFn: () => mailboxApi.request(company.id),
|
|
});
|
|
const edited = query.data?.mailboxes.find((item) => item.id === editor);
|
|
const use = query.data?.uses.find((item) => item.id === useEditor);
|
|
const viewed = query.data?.mailboxes.find((item) => item.id === view);
|
|
const name = (id: string) => company.people.find((person) => person.id === id)?.name ?? id;
|
|
const boxes = query.data?.mailboxes ?? [];
|
|
const filtered = boxes.filter((item) =>
|
|
[item.values.address, item.values.context ?? ""].some((value) =>
|
|
value.toLowerCase().includes(search.trim().toLowerCase())
|
|
)
|
|
);
|
|
return (
|
|
<div className="space-y-5">
|
|
<header className="flex flex-wrap justify-between items-center gap-4">
|
|
<h2 className="text-xl font-semibold">{t("mailbox.title")}</h2>
|
|
<div className="flex gap-2">
|
|
<Button disabled={!query.data} onClick={() => setUseEditor("new")} variant="outline">
|
|
{t("mailbox.linkPerson")}
|
|
</Button>
|
|
<Button disabled={!query.data} onClick={() => setEditor("new")}>
|
|
{t("mailbox.new")}
|
|
</Button>
|
|
</div>
|
|
</header>
|
|
<Input
|
|
aria-label={t("mailbox.search")}
|
|
onChange={(event) => setSearch(event.target.value)}
|
|
placeholder={t("mailbox.search")}
|
|
value={search}
|
|
/>
|
|
{query.isPending && <p role="status">{t("loading")}</p>}
|
|
<MailFeedback 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(boxes.length ? "noResults" : "mailbox.empty")}
|
|
</p>
|
|
)}
|
|
{filtered.map((item) => (
|
|
<div className="py-4 space-y-3" key={item.id}>
|
|
<div className="flex flex-wrap justify-between gap-3">
|
|
<div>
|
|
<h3 className="font-medium">{item.values.address}</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t(`mailbox.${item.values.kind}`)} ·{" "}
|
|
{t(item.values.active ? "active" : "inactive")}
|
|
{item.values.context ? ` · ${item.values.context}` : ""}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button onClick={() => setView(item.id)} size="sm" variant="outline">
|
|
{t("mail.history")}
|
|
</Button>
|
|
<Button onClick={() => setEditor(item.id)} size="sm" variant="outline">
|
|
{t("edit")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<ul className="space-y-2">
|
|
{query.data?.uses
|
|
.filter((use) => use.values.mailboxId === item.id)
|
|
.map((use) => (
|
|
<li className="flex justify-between gap-3 text-sm" key={use.id}>
|
|
<span>
|
|
{name(use.values.personId)} · {use.values.startDate} →{" "}
|
|
{use.values.endDate ?? t("mailbox.openPeriod")}
|
|
</span>
|
|
{!use.values.endDate && (
|
|
<Button onClick={() => setUseEditor(use.id)} size="sm" variant="outline">
|
|
{t("mailbox.closeUse")}
|
|
</Button>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
{editor && (
|
|
<FormDialog
|
|
cancelLabel={t("cancel")}
|
|
formActions
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setEditor(undefined);
|
|
}
|
|
}}
|
|
open
|
|
title={t(edited ? "mailbox.edit" : "mailbox.new")}
|
|
wide
|
|
>
|
|
<MailboxForm
|
|
company={company}
|
|
csrfToken={csrfToken}
|
|
mailbox={edited}
|
|
onSaved={() => setEditor(undefined)}
|
|
/>
|
|
</FormDialog>
|
|
)}
|
|
{useEditor && (
|
|
<FormDialog
|
|
cancelLabel={t("cancel")}
|
|
formActions
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setUseEditor(undefined);
|
|
}
|
|
}}
|
|
open
|
|
title={t(use ? "mailbox.closeUse" : "mailbox.linkPerson")}
|
|
wide
|
|
>
|
|
<MailboxUseForm
|
|
company={company}
|
|
csrfToken={csrfToken}
|
|
onSaved={() => setUseEditor(undefined)}
|
|
use={use}
|
|
/>
|
|
</FormDialog>
|
|
)}
|
|
{viewed && (
|
|
<FormDialog
|
|
cancelLabel={t("close")}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setView(undefined);
|
|
}
|
|
}}
|
|
open
|
|
title={t("mail.history")}
|
|
wide
|
|
>
|
|
<ol className="space-y-3">
|
|
{[...viewed.history].reverse().map((revision) => (
|
|
<li className="border rounded-md p-3 text-sm" key={revision.id}>
|
|
<p>
|
|
{new Intl.DateTimeFormat(i18n.language, {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
}).format(new Date(revision.recordedAt))}{" "}
|
|
· {revision.actorName}
|
|
</p>
|
|
<p>
|
|
{revision.values.address} · {t(`mailbox.${revision.values.kind}`)} ·{" "}
|
|
{t(revision.values.active ? "active" : "inactive")}
|
|
</p>
|
|
<p>{revision.values.context}</p>
|
|
<p>{revision.values.notes}</p>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
{query.data?.uses
|
|
.filter((use) => use.values.mailboxId === viewed.id)
|
|
.map((use) => (
|
|
<div className="mt-4 rounded-md border p-3 text-sm" key={use.id}>
|
|
<h3 className="font-medium">{name(use.values.personId)}</h3>
|
|
{use.history.map((revision) => (
|
|
<p key={revision.id}>
|
|
{revision.values.startDate} →{" "}
|
|
{revision.values.endDate ?? t("mailbox.openPeriod")} · {revision.actorName} ·{" "}
|
|
{revision.recordedAt}
|
|
</p>
|
|
))}
|
|
</div>
|
|
))}
|
|
</FormDialog>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|