Presupuestador_web/client/src/app/quotes/edit.tsx

322 lines
10 KiB
TypeScript
Raw Normal View History

2024-07-11 17:43:08 +00:00
import {
BackHistoryButton,
CancelButton,
2024-08-20 16:30:51 +00:00
ColorBadge,
2024-07-11 17:43:08 +00:00
ErrorOverlay,
LoadingOverlay,
SubmitButton,
} from "@/components";
2024-08-29 11:59:12 +00:00
import { calculateQuoteItemTotals, calculateQuoteTotals } from "@/lib/calc";
import { useUnsavedChangesNotifier } from "@/lib/hooks";
2024-07-09 16:21:12 +00:00
import { useUrlId } from "@/lib/hooks/useUrlId";
2024-08-20 16:30:51 +00:00
import { Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
2024-08-29 11:30:05 +00:00
import {
CurrencyData,
IGetQuote_QuoteItem_Response_DTO,
IGetQuote_Response_DTO,
Language,
} from "@shared/contexts";
2024-07-01 17:12:15 +00:00
import { t } from "i18next";
2024-08-29 11:59:12 +00:00
import { useEffect, useMemo, useState } from "react";
2024-08-26 15:53:58 +00:00
import { useForm } from "react-hook-form";
2024-07-11 17:43:08 +00:00
import { useNavigate } from "react-router-dom";
2024-07-11 18:49:06 +00:00
import { toast } from "react-toastify";
2024-07-18 16:41:46 +00:00
import { QuotePricesResume } from "./components";
2024-07-09 16:21:12 +00:00
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
2024-07-01 17:12:15 +00:00
import { useQuotes } from "./hooks";
2024-08-29 11:30:05 +00:00
export type QuoteDataForm = IGetQuote_Response_DTO;
export type QuoteDataFormItem = IGetQuote_QuoteItem_Response_DTO;
2024-07-01 17:12:15 +00:00
2024-07-09 16:21:12 +00:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const QuoteEdit = () => {
2024-07-11 17:43:08 +00:00
const navigate = useNavigate();
2024-07-09 16:21:12 +00:00
const quoteId = useUrlId();
2024-07-17 11:03:02 +00:00
2024-07-18 10:22:43 +00:00
const [activeTab, setActiveTab] = useState("general");
2024-07-10 18:54:33 +00:00
const [quoteCurrency, setQuoteCurrency] = useState<CurrencyData>(
CurrencyData.createDefaultCode().object
);
2024-07-17 11:03:02 +00:00
const [quoteLanguage, setQuoteLanguage] = useState<Language>(Language.createDefaultCode().object);
2024-07-10 18:54:33 +00:00
2024-07-09 16:21:12 +00:00
const { useOne, useUpdate } = useQuotes();
2024-07-01 17:12:15 +00:00
2024-07-10 18:54:33 +00:00
const { data, status, error: queryError } = useOne(quoteId);
2024-07-17 18:10:07 +00:00
const defaultValues = useMemo(
() => ({
2024-07-01 17:12:15 +00:00
date: "",
reference: "",
2024-08-23 16:47:51 +00:00
customer_reference: "",
2024-07-01 17:12:15 +00:00
customer_information: "",
lang_code: "",
currency_code: "",
payment_method: "",
notes: "",
validity: "",
2024-07-11 17:43:08 +00:00
subtotal_price: {
amount: undefined,
2024-07-16 17:17:52 +00:00
scale: 2,
2024-07-17 18:10:07 +00:00
currency_code: data?.currency_code ?? quoteCurrency.code,
2024-07-11 17:43:08 +00:00
},
discount: {
amount: undefined,
2024-07-16 17:17:52 +00:00
scale: 0,
2024-07-11 17:43:08 +00:00
},
2024-07-19 15:59:50 +00:00
discount_price: {
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
},
before_tax_price: {
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
},
tax: {
amount: undefined,
scale: 0,
},
tax_price: {
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
},
2024-07-11 17:43:08 +00:00
total_price: {
amount: undefined,
2024-07-16 17:17:52 +00:00
scale: 2,
2024-07-17 18:10:07 +00:00
currency_code: data?.currency_code ?? quoteCurrency.code,
2024-07-11 17:43:08 +00:00
},
2024-07-16 18:18:17 +00:00
items: [
2024-07-11 18:31:01 +00:00
{
2024-07-16 18:18:17 +00:00
description: "",
2024-07-16 17:17:52 +00:00
quantity: {
2024-08-29 09:21:44 +00:00
amount: null,
scale: 2,
2024-07-16 17:17:52 +00:00
},
2024-07-17 18:10:07 +00:00
unit_price: {
amount: null,
scale: 4,
currency_code: data?.currency_code ?? quoteCurrency.code,
},
2024-07-11 18:31:01 +00:00
subtotal_price: {
2024-07-17 18:10:07 +00:00
amount: null,
2024-07-16 17:17:52 +00:00
scale: 4,
2024-07-17 18:10:07 +00:00
currency_code: data?.currency_code ?? quoteCurrency.code,
2024-07-11 18:31:01 +00:00
},
discount: {
2024-07-17 18:10:07 +00:00
amount: null,
2024-07-16 18:18:17 +00:00
scale: 2,
2024-07-11 18:31:01 +00:00
},
total_price: {
2024-07-17 18:10:07 +00:00
amount: null,
2024-07-16 17:17:52 +00:00
scale: 4,
2024-07-17 18:10:07 +00:00
currency_code: data?.currency_code ?? quoteCurrency.code,
2024-07-11 18:31:01 +00:00
},
},
2024-07-16 18:18:17 +00:00
],
2024-07-17 18:10:07 +00:00
}),
[data, quoteCurrency]
);
const { mutate, isPending } = useUpdate(String(quoteId));
2024-07-17 18:10:07 +00:00
const form = useForm<QuoteDataForm>({
mode: "onBlur",
values: data,
defaultValues,
2024-07-18 18:26:44 +00:00
//shouldUnregister: true,
2024-07-01 17:12:15 +00:00
});
2024-08-29 11:59:12 +00:00
const { getValues, reset, handleSubmit, formState, watch, setValue } = form;
const { isSubmitting, isDirty } = formState;
2024-07-11 18:49:06 +00:00
useUnsavedChangesNotifier({
isDirty,
});
2024-07-09 18:29:02 +00:00
2024-08-26 15:53:58 +00:00
const onSubmit = async (data: QuoteDataForm, shouldRedirect: boolean) => {
2024-07-10 18:54:33 +00:00
// Transformación del form -> typo de request
2024-08-20 16:30:51 +00:00
2024-07-10 18:54:33 +00:00
mutate(data, {
onError: (error) => {
2024-07-11 16:40:46 +00:00
console.debug(error);
2024-07-17 18:10:07 +00:00
toast.error(error.message);
2024-07-11 16:40:46 +00:00
//alert(error.message);
2024-07-10 18:54:33 +00:00
},
//onSettled: () => {},
onSuccess: () => {
2024-08-26 16:26:46 +00:00
console.log("onsuccess 2");
reset(getValues());
2024-08-12 11:26:10 +00:00
toast.success("Cotización guardada");
2024-08-26 15:53:58 +00:00
if (shouldRedirect) {
navigate("/quotes");
}
2024-07-16 17:17:52 +00:00
//clear();
2024-07-10 18:54:33 +00:00
},
});
2024-07-01 17:12:15 +00:00
};
2024-08-29 11:59:12 +00:00
useEffect(() => {
2024-07-19 15:02:19 +00:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { unsubscribe } = watch((_, { name, type }) => {
2024-08-29 11:30:05 +00:00
console.log("useEffect");
2024-07-19 15:02:19 +00:00
const quote = getValues();
2024-07-18 18:26:44 +00:00
if (name) {
switch (true) {
case name === "currency_code":
2024-07-19 15:02:19 +00:00
setQuoteCurrency(
CurrencyData.createFromCode(quote.currency_code ?? CurrencyData.DEFAULT_CURRENCY_CODE)
.object
);
2024-07-18 18:26:44 +00:00
break;
case name === "lang_code":
2024-07-19 15:02:19 +00:00
setQuoteLanguage(
Language.createFromCode(quote.lang_code ?? Language.DEFAULT_LANGUAGE_CODE).object
);
2024-07-18 18:26:44 +00:00
break;
2024-07-19 15:59:50 +00:00
case name === "discount" || name === "tax": {
const quoteTotals = calculateQuoteTotals(quote);
setValue("subtotal_price", quoteTotals.subtotalPrice.toObject());
setValue("discount_price", quoteTotals.discountPrice.toObject());
setValue("before_tax_price", quoteTotals.priceBeforeTaxes.toObject());
setValue("tax_price", quoteTotals.taxesPrice.toObject());
setValue("total_price", quoteTotals.totalPrice.toObject());
break;
}
2024-07-19 15:02:19 +00:00
case name === "items": {
quote.items &&
quote.items.map((item, index) => {
const quoteItemTotals = calculateQuoteItemTotals(item);
setValue(`items.${index}.subtotal_price`, quoteItemTotals.subtotalPrice.toObject());
setValue(`items.${index}.total_price`, quoteItemTotals.totalPrice.toObject());
});
2024-07-19 15:59:50 +00:00
const quoteTotals = calculateQuoteTotals(quote, true);
2024-07-19 15:02:19 +00:00
setValue("subtotal_price", quoteTotals.subtotalPrice.toObject());
2024-07-19 15:59:50 +00:00
setValue("discount_price", quoteTotals.discountPrice.toObject());
setValue("before_tax_price", quoteTotals.priceBeforeTaxes.toObject());
setValue("tax_price", quoteTotals.taxesPrice.toObject());
2024-07-19 15:02:19 +00:00
setValue("total_price", quoteTotals.totalPrice.toObject());
2024-07-18 18:26:44 +00:00
break;
2024-07-19 15:02:19 +00:00
}
2024-07-18 18:26:44 +00:00
case name.endsWith("quantity") ||
name.endsWith("unit_price") ||
name.endsWith("discount"): {
const [, indexString] = String(name).split(".");
const index = parseInt(indexString);
2024-07-19 15:02:19 +00:00
const quoteItemTotals = calculateQuoteItemTotals(quote.items[index]);
setValue(`items.${index}.subtotal_price`, quoteItemTotals.subtotalPrice.toObject());
setValue(`items.${index}.total_price`, quoteItemTotals.totalPrice.toObject());
// Cabecera
2024-07-19 15:59:50 +00:00
const quoteTotals = calculateQuoteTotals(quote, true);
2024-07-19 15:02:19 +00:00
setValue("subtotal_price", quoteTotals.subtotalPrice.toObject());
2024-07-19 15:59:50 +00:00
setValue("discount_price", quoteTotals.discountPrice.toObject());
setValue("before_tax_price", quoteTotals.priceBeforeTaxes.toObject());
setValue("tax_price", quoteTotals.taxesPrice.toObject());
2024-07-19 15:02:19 +00:00
setValue("total_price", quoteTotals.totalPrice.toObject());
2024-07-18 18:26:44 +00:00
break;
}
default:
break;
2024-07-09 16:21:12 +00:00
}
}
});
return () => unsubscribe();
2024-08-29 11:59:12 +00:00
}, [watch, getValues, setValue]);
2024-07-09 16:21:12 +00:00
if (isSubmitting || isPending) {
2024-07-18 10:02:59 +00:00
return <LoadingOverlay title='Guardando cotización' />;
2024-07-09 18:29:02 +00:00
}
if (status === "error") {
return <ErrorOverlay errorMessage={queryError.message} />;
}
2024-07-09 16:21:12 +00:00
if (status !== "success") {
return <LoadingOverlay />;
}
2024-07-01 17:12:15 +00:00
return (
<Form {...form}>
2024-08-26 15:53:58 +00:00
<form onSubmit={handleSubmit((data) => onSubmit(data, false))}>
2024-07-01 17:12:15 +00:00
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
<div className='flex items-center gap-4'>
2024-07-11 17:43:08 +00:00
<BackHistoryButton />
2024-07-01 17:12:15 +00:00
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
2024-08-23 16:47:51 +00:00
{t("quotes.edit.title")} {data.reference}
2024-07-01 17:12:15 +00:00
</h1>
2024-08-20 16:30:51 +00:00
<ColorBadge label={data.status} className='ml-auto sm:ml-0' />
2024-07-01 17:12:15 +00:00
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
2024-07-19 17:28:25 +00:00
<CancelButton
2024-08-26 18:29:41 +00:00
label={t("common.close")}
2024-08-26 15:53:58 +00:00
variant='secondary'
2024-07-19 17:28:25 +00:00
size='sm'
onClick={() => navigate("/quotes")}
/>
2024-07-12 18:13:17 +00:00
<SubmitButton
2024-07-19 17:28:25 +00:00
label={t("common.save")}
2024-07-12 18:13:17 +00:00
size='sm'
disabled={formState.isSubmitting || formState.isLoading || formState.isValidating}
2024-08-26 15:53:58 +00:00
/>
<Button
size='sm'
disabled={formState.isSubmitting || formState.isLoading || formState.isValidating}
onClick={handleSubmit((data) => onSubmit(data, true))}
2024-07-12 18:13:17 +00:00
>
2024-08-26 15:53:58 +00:00
{t("common.save_close")}
</Button>
2024-07-01 17:12:15 +00:00
</div>
</div>
2024-07-09 16:21:12 +00:00
2024-08-29 11:59:12 +00:00
<QuoteGeneralCardEditor />
<QuotePricesResume />
<QuoteDetailsCardEditor
currency={quoteCurrency}
language={quoteLanguage}
defaultValues={defaultValues}
/>
2024-07-18 10:22:43 +00:00
<Tabs
defaultValue='items'
className='space-y-4'
value={activeTab}
onValueChange={setActiveTab}
>
2024-07-01 17:12:15 +00:00
<TabsList>
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
2024-07-11 18:49:06 +00:00
{/* <TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>*/}
2024-07-01 17:12:15 +00:00
</TabsList>
2024-08-29 11:59:12 +00:00
<TabsContent value='general' forceMount hidden={"general" !== activeTab}></TabsContent>
<TabsContent value='items' forceMount hidden={"items" !== activeTab}></TabsContent>
2024-07-01 17:12:15 +00:00
</Tabs>
2024-07-18 10:22:43 +00:00
2024-07-01 17:12:15 +00:00
<div className='flex items-center justify-center gap-2 md:hidden'>
<Button variant='outline' size='sm'>
{t("quotes.create.buttons.discard")}
</Button>
<Button size='sm'>{t("quotes.create.buttons.save_quote")}</Button>
</div>
</div>
</form>
</Form>
);
};