metis-desk/modules/companies/src/web/mail/mailbox-use-form.tsx
David 6375fa1b32 feat(mailbox): implement mailbox and mailbox use management features
- 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.
2026-10-05 10:45:02 +02:00

231 lines
7.7 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@repo/shadcn-ui/components/button";
import { ContextHelp } from "@repo/shadcn-ui/components/context-help";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useId, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { CompanyDTO } from "../../common";
import {
type MailboxUseDTO,
type MailboxUseInput,
MailboxUseInputSchema,
} from "../../common/mailboxes";
import { PersonForm } from "../components/person-form";
import { mailKeys } from "./mail-api";
import { MailFeedback } from "./mail-feedback";
import { mailboxApi, mailboxKeys } from "./mailbox-api";
import { MailboxForm } from "./mailbox-form";
export function MailboxUseForm({
company,
csrfToken,
use,
initialMailboxId = "",
initialPersonId = "",
onSaved,
allowQuickCreate = true,
}: {
company: CompanyDTO;
csrfToken: string;
use?: MailboxUseDTO | undefined;
initialMailboxId?: string;
initialPersonId?: string;
onSaved: () => void;
allowQuickCreate?: boolean;
}) {
const { t } = useTranslation("companies");
const prefix = useId();
const client = useQueryClient();
const [quick, setQuick] = useState<"person" | "mailbox">();
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const query = useQuery({
queryKey: mailboxKeys.company(company.id),
queryFn: () => mailboxApi.request(company.id),
});
const today = new Date().toISOString().slice(0, 10);
const form = useForm<MailboxUseInput>({
resolver: zodResolver(MailboxUseInputSchema),
defaultValues: useDefaults(use, initialMailboxId, initialPersonId, today),
});
const values = form.watch();
const mutation = useMutation({
mutationFn: (input: MailboxUseInput) =>
mailboxApi.request(company.id, input, csrfToken, use?.id),
onSuccess: async () => {
await Promise.all([
client.invalidateQueries({ queryKey: mailboxKeys.company(company.id) }),
client.invalidateQueries({ queryKey: mailKeys.company(company.id) }),
]);
if (mounted.current) {
onSaved();
}
},
});
return (
<>
<form className="space-y-5" onSubmit={form.handleSubmit((input) => mutation.mutate(input))}>
<fieldset className="space-y-4" disabled={mutation.isPending}>
<div className="space-y-2">
<Label htmlFor={`${prefix}-mailbox`}>{t("mailbox.titleSingle")}</Label>
<SelectWithAction
action={
canCreate(!!use, initialMailboxId, allowQuickCreate)
? { label: t("mailbox.new"), onClick: () => setQuick("mailbox") }
: undefined
}
disabled={!!use || !!initialMailboxId}
id={`${prefix}-mailbox`}
label={t("mailbox.titleSingle")}
onValueChange={(id) => form.setValue("mailboxId", id)}
options={(query.data?.mailboxes ?? [])
.filter((item) => item.values.active || item.id === values.mailboxId)
.map((item) => ({ value: item.id, label: item.values.address }))}
placeholder={t("mail.choose")}
required
value={values.mailboxId}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`${prefix}-person`}>{t("mail.person")}</Label>
<SelectWithAction
action={
canCreate(!!use, initialPersonId, allowQuickCreate)
? { label: t("newPerson"), onClick: () => setQuick("person") }
: undefined
}
disabled={!!use || !!initialPersonId}
id={`${prefix}-person`}
label={t("mail.person")}
onValueChange={(id) => form.setValue("personId", id)}
options={company.people
.filter((item) => item.active || item.id === values.personId)
.map((item) => ({ value: item.id, label: item.name }))}
placeholder={t("mail.choose")}
required
value={values.personId}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-start`}>{t("mailbox.start")}</Label>
<Input
disabled={!!use}
id={`${prefix}-start`}
required
type="date"
{...form.register("startDate")}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`${prefix}-end`}>{t("mailbox.end")}</Label>
<Input
id={`${prefix}-end`}
onChange={(event) => form.setValue("endDate", event.target.value || null)}
required={!!use}
type="date"
value={values.endDate ?? ""}
/>
</div>
</div>
</fieldset>
{Object.keys(form.formState.errors).length > 0 && (
<p role="alert">{t("mail.errors.INVALID_REQUEST")}</p>
)}
<MailFeedback error={mutation.error ?? query.error} />
<ContextHelp
description={t("mailbox.useDescription")}
examples={t("mailbox.useExamples")}
examplesLabel={t("mail.examplesLabel")}
guidance={t("mailbox.useGuidance")}
title={t("mailbox.useHelp")}
/>
<FormActions>
<Button disabled={mutation.isPending || !query.data} type="submit">
{t(mutation.isPending ? "saving" : "save")}
</Button>
</FormActions>
</form>
{quick === "person" && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setQuick(undefined);
}
}}
open
title={t("newPerson")}
>
<PersonForm
companyId={company.id}
csrfToken={csrfToken}
onSaved={(id) => {
if (mounted.current) {
form.setValue("personId", id);
setQuick(undefined);
}
}}
requireActive
/>
</FormDialog>
)}
{quick === "mailbox" && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setQuick(undefined);
}
}}
open
title={t("mailbox.new")}
>
<MailboxForm
allowQuickCreate={false}
company={company}
csrfToken={csrfToken}
onSaved={(id) => {
if (mounted.current) {
form.setValue("mailboxId", id);
setQuick(undefined);
}
}}
/>
</FormDialog>
)}
</>
);
}
function useDefaults(
existing: MailboxUseDTO | undefined,
mailboxId: string,
personId: string,
today: string
): MailboxUseInput {
if (existing) {
const values = existing.values;
return { ...values, endDate: today > values.startDate ? today : values.startDate };
}
return { mailboxId, personId, startDate: today, endDate: null };
}
function canCreate(editing: boolean, initial: string, allowed: boolean) {
return !(editing || initial) && allowed;
}