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
OpenSpec change: evolve-frontend-stack
316 lines
7.9 KiB
TypeScript
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 };
|