Files
frontend/src/pages/card/ui/OperationForm.view.tsx
T
ksv741 f876411a37
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Successful in 5m27s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 5m24s
Conventional commits / commits (pull_request) Failing after 26s
refactor: establish local frontend workspace boundaries
OpenSpec change: evolve-frontend-stack
2026-09-22 22:11:02 +03:00

316 lines
7.9 KiB
TypeScript

import {
Button,
Group,
Modal,
Select,
Stack,
Text,
Textarea,
TextInput,
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import type { FC, FormEventHandler, ReactNode } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { BankOperation } from "@/entities/bank-card";
import { formatMoney } from "@/shared/lib";
import classes from "./OperationForm.module.css";
import type { OperationValues } from "./OperationForm.types";
type Choice = { value: string; label: string };
type OperationFieldsProps = {
editing: boolean;
refundCardId: string;
kind: OperationValues["kind"];
cards: Choice[];
targets: Choice[];
categories: Choice[];
defaultCategory: string;
kinds: Choice[];
dateInputValue: (value: string) => string;
validDate: (value: string) => boolean;
maxDate: () => string;
};
const OperationFields: FC<OperationFieldsProps> = (props) => {
const { t } = useTranslation();
const {
control,
register,
setValue,
formState: { errors, isSubmitting },
} = useFormContext<OperationValues>();
return (
<Stack>
<Controller
control={control}
name="kind"
render={({ field }) => (
<Select
label={t("bank.kind")}
required
data={props.kinds}
{...field}
disabled={isSubmitting || props.editing}
onChange={(value) => {
field.onChange(value ?? "expense");
if (value === "refund") setValue("cardId", props.refundCardId);
}}
/>
)}
/>
<Controller
control={control}
name="cardId"
render={({ field }) => (
<Select
label={t(
props.kind === "transfer" ? "bank.source" : "bank.operationCard",
)}
required
data={props.cards}
{...field}
disabled={isSubmitting || props.kind === "refund"}
error={errors.cardId?.message}
/>
)}
/>
{props.kind === "transfer" && (
<Controller
control={control}
name="targetCardId"
render={({ field }) => (
<Select
label={t("bank.destination")}
required
data={props.targets}
{...field}
disabled={isSubmitting}
error={errors.targetCardId?.message}
/>
)}
/>
)}
<TextInput
label={t("bank.amount")}
required
inputMode="decimal"
{...register("amount")}
error={errors.amount?.message}
/>
<Controller
control={control}
name="date"
render={({ field }) => (
<DateInput
{...field}
label={t("bank.date")}
required
valueFormat="DD.MM.YYYY"
value={props.validDate(field.value) ? field.value : null}
dateParser={(value) =>
props.validDate(props.dateInputValue(value))
? props.dateInputValue(value)
: null
}
fixOnBlur={false}
onInput={(event) =>
field.onChange(props.dateInputValue(event.currentTarget.value))
}
onChange={(value) => field.onChange(value ?? "")}
onBlur={(event) => {
field.onChange(props.dateInputValue(event.currentTarget.value));
field.onBlur();
}}
maxDate={props.maxDate()}
popoverProps={{
withinPortal: false,
middlewares: { shift: { crossAxis: true } },
}}
error={errors.date?.message}
/>
)}
/>
{props.kind === "expense" && (
<Controller
control={control}
name="categoryId"
render={({ field }) => (
<Select
label={t("bank.category")}
required
data={props.categories}
{...field}
value={field.value || props.defaultCategory}
disabled={isSubmitting}
error={errors.categoryId?.message}
/>
)}
/>
)}
<Textarea
label={t("bank.description")}
{...register("description")}
error={errors.description?.message}
/>
</Stack>
);
};
type RefundFieldsProps = {
editing: boolean;
expenses: Choice[];
onSelect: (id: string) => void;
limit: BankOperation["availableRefund"] | undefined;
};
const RefundFields: FC<RefundFieldsProps> = (props) => {
const { t } = useTranslation();
const {
control,
formState: { errors, isSubmitting },
} = useFormContext<OperationValues>();
return (
<Stack>
<Controller
control={control}
name="originalExpenseId"
render={({ field }) => (
<Select
label={t("bank.expense")}
required
data={props.expenses}
{...field}
onChange={(value) => {
field.onChange(value ?? "");
props.onSelect(value ?? "");
}}
disabled={isSubmitting || props.editing}
error={errors.originalExpenseId?.message}
/>
)}
/>
{props.limit && (
<Text>
{t("bank.availableRefund", { amount: formatMoney(props.limit) })}
</Text>
)}
{props.editing && <Text>{t("bank.purchaseImmutable")}</Text>}
</Stack>
);
};
type OperationFormViewProps = {
opened: boolean;
pending: boolean;
categoriesOpen: boolean;
editing: boolean;
onClose: () => void;
onExitTransitionEnd: () => void;
onCancel: () => void;
onSubmit: FormEventHandler<HTMLFormElement>;
fields: OperationFieldsProps;
refund: RefundFieldsProps;
loadingExpenses: boolean;
emptyExpenses: boolean;
sourcePage: number;
previousDisabled: boolean;
nextDisabled: boolean;
onPrevious: () => void;
onNext: () => void;
onAddCategory: () => void;
onReload?: () => void;
latest?: { version: number; details: string; onAdopt: () => void };
saveDisabled: boolean;
refundFeedback?: ReactNode;
categoriesFeedback?: ReactNode;
feedback?: ReactNode;
};
const OperationFormView: FC<OperationFormViewProps> = (props) => {
const { t } = useTranslation();
return (
<Modal
opened={props.opened}
onExitTransitionEnd={props.onExitTransitionEnd}
onClose={props.onClose}
title={t(props.editing ? "bank.editOperation" : "bank.addOperation")}
closeOnEscape={!props.pending && !props.categoriesOpen}
closeOnClickOutside={!props.pending && !props.categoriesOpen}
closeButtonProps={{
"aria-label": t("bank.close"),
disabled: props.pending,
}}
>
<form onSubmit={props.onSubmit}>
<fieldset disabled={props.pending} className={classes.fieldset}>
<Stack>
<OperationFields {...props.fields} />
{props.fields.kind === "refund" && (
<>
<RefundFields {...props.refund} />
{props.loadingExpenses && (
<Text role="status">{t("bank.loadingHistory")}</Text>
)}
{props.emptyExpenses && <Text>{t("bank.noPurchases")}</Text>}
{!props.editing && (
<Group>
<Button
variant="default"
disabled={props.previousDisabled}
onClick={props.onPrevious}
>
{t("bank.previous")}
</Button>
<Text>{t("bank.page", { page: props.sourcePage })}</Text>
<Button
variant="default"
disabled={props.nextDisabled}
onClick={props.onNext}
>
{t("bank.next")}
</Button>
</Group>
)}
{props.refundFeedback}
</>
)}
{props.fields.kind === "expense" && (
<Button variant="subtle" onClick={props.onAddCategory}>
{t("bank.addCategory")}
</Button>
)}
{props.categoriesFeedback}
{props.feedback}
{props.onReload && (
<Button variant="default" onClick={props.onReload}>
{t("bank.reloadRecord")}
</Button>
)}
{props.latest && (
<>
<Text>
{t("bank.versionHint", { details: props.latest.details })}
</Text>
<Button variant="default" onClick={props.latest.onAdopt}>
{t("bank.adoptVersion", { version: props.latest.version })}
</Button>
</>
)}
<Group>
<Button
type="submit"
loading={props.pending}
disabled={props.saveDisabled}
>
{t("bank.save")}
</Button>
<Button variant="default" onClick={props.onCancel}>
{t("bank.cancel")}
</Button>
</Group>
</Stack>
</fieldset>
</form>
</Modal>
);
};
export { OperationFormView };