322 lines
10 KiB
TypeScript
322 lines
10 KiB
TypeScript
import {
|
|
BackHistoryButton,
|
|
CancelButton,
|
|
ColorBadge,
|
|
ErrorOverlay,
|
|
LoadingOverlay,
|
|
SubmitButton,
|
|
} from "@/components";
|
|
import { calculateQuoteItemTotals, calculateQuoteTotals } from "@/lib/calc";
|
|
import { useUnsavedChangesNotifier } from "@/lib/hooks";
|
|
import { useUrlId } from "@/lib/hooks/useUrlId";
|
|
import { Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
|
|
import {
|
|
CurrencyData,
|
|
IGetQuote_QuoteItem_Response_DTO,
|
|
IGetQuote_Response_DTO,
|
|
Language,
|
|
} from "@shared/contexts";
|
|
import { t } from "i18next";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { toast } from "react-toastify";
|
|
import { QuotePricesResume } from "./components";
|
|
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
|
|
import { useQuotes } from "./hooks";
|
|
|
|
export type QuoteDataForm = IGetQuote_Response_DTO;
|
|
export type QuoteDataFormItem = IGetQuote_QuoteItem_Response_DTO;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
export const QuoteEdit = () => {
|
|
const navigate = useNavigate();
|
|
const quoteId = useUrlId();
|
|
|
|
const [activeTab, setActiveTab] = useState("general");
|
|
|
|
const [quoteCurrency, setQuoteCurrency] = useState<CurrencyData>(
|
|
CurrencyData.createDefaultCode().object
|
|
);
|
|
const [quoteLanguage, setQuoteLanguage] = useState<Language>(Language.createDefaultCode().object);
|
|
|
|
const { useOne, useUpdate } = useQuotes();
|
|
|
|
const { data, status, error: queryError } = useOne(quoteId);
|
|
|
|
const defaultValues = useMemo(
|
|
() => ({
|
|
date: "",
|
|
reference: "",
|
|
customer_reference: "",
|
|
customer_information: "",
|
|
lang_code: "",
|
|
currency_code: "",
|
|
payment_method: "",
|
|
notes: "",
|
|
validity: "",
|
|
subtotal_price: {
|
|
amount: undefined,
|
|
scale: 2,
|
|
currency_code: data?.currency_code ?? quoteCurrency.code,
|
|
},
|
|
discount: {
|
|
amount: undefined,
|
|
scale: 0,
|
|
},
|
|
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,
|
|
},
|
|
total_price: {
|
|
amount: undefined,
|
|
scale: 2,
|
|
currency_code: data?.currency_code ?? quoteCurrency.code,
|
|
},
|
|
items: [
|
|
{
|
|
description: "",
|
|
quantity: {
|
|
amount: null,
|
|
scale: 2,
|
|
},
|
|
unit_price: {
|
|
amount: null,
|
|
scale: 4,
|
|
currency_code: data?.currency_code ?? quoteCurrency.code,
|
|
},
|
|
subtotal_price: {
|
|
amount: null,
|
|
scale: 4,
|
|
currency_code: data?.currency_code ?? quoteCurrency.code,
|
|
},
|
|
discount: {
|
|
amount: null,
|
|
scale: 2,
|
|
},
|
|
total_price: {
|
|
amount: null,
|
|
scale: 4,
|
|
currency_code: data?.currency_code ?? quoteCurrency.code,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
[data, quoteCurrency]
|
|
);
|
|
|
|
const { mutate, isPending } = useUpdate(String(quoteId));
|
|
|
|
const form = useForm<QuoteDataForm>({
|
|
mode: "onBlur",
|
|
values: data,
|
|
defaultValues,
|
|
//shouldUnregister: true,
|
|
});
|
|
|
|
const { getValues, reset, handleSubmit, formState, watch, setValue } = form;
|
|
const { isSubmitting, isDirty } = formState;
|
|
|
|
useUnsavedChangesNotifier({
|
|
isDirty,
|
|
});
|
|
|
|
const onSubmit = async (data: QuoteDataForm, shouldRedirect: boolean) => {
|
|
// Transformación del form -> typo de request
|
|
|
|
mutate(data, {
|
|
onError: (error) => {
|
|
console.debug(error);
|
|
toast.error(error.message);
|
|
//alert(error.message);
|
|
},
|
|
//onSettled: () => {},
|
|
onSuccess: () => {
|
|
console.log("onsuccess 2");
|
|
reset(getValues());
|
|
toast.success("Cotización guardada");
|
|
if (shouldRedirect) {
|
|
navigate("/quotes");
|
|
}
|
|
//clear();
|
|
},
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { unsubscribe } = watch((_, { name, type }) => {
|
|
console.log("useEffect");
|
|
const quote = getValues();
|
|
|
|
if (name) {
|
|
switch (true) {
|
|
case name === "currency_code":
|
|
setQuoteCurrency(
|
|
CurrencyData.createFromCode(quote.currency_code ?? CurrencyData.DEFAULT_CURRENCY_CODE)
|
|
.object
|
|
);
|
|
|
|
break;
|
|
|
|
case name === "lang_code":
|
|
setQuoteLanguage(
|
|
Language.createFromCode(quote.lang_code ?? Language.DEFAULT_LANGUAGE_CODE).object
|
|
);
|
|
break;
|
|
|
|
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;
|
|
}
|
|
|
|
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());
|
|
});
|
|
|
|
const quoteTotals = calculateQuoteTotals(quote, true);
|
|
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;
|
|
}
|
|
|
|
case name.endsWith("quantity") ||
|
|
name.endsWith("unit_price") ||
|
|
name.endsWith("discount"): {
|
|
const [, indexString] = String(name).split(".");
|
|
const index = parseInt(indexString);
|
|
|
|
const quoteItemTotals = calculateQuoteItemTotals(quote.items[index]);
|
|
setValue(`items.${index}.subtotal_price`, quoteItemTotals.subtotalPrice.toObject());
|
|
setValue(`items.${index}.total_price`, quoteItemTotals.totalPrice.toObject());
|
|
|
|
// Cabecera
|
|
const quoteTotals = calculateQuoteTotals(quote, true);
|
|
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;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
return () => unsubscribe();
|
|
}, [watch, getValues, setValue]);
|
|
|
|
if (isSubmitting || isPending) {
|
|
return <LoadingOverlay title='Guardando cotización' />;
|
|
}
|
|
|
|
if (status === "error") {
|
|
return <ErrorOverlay errorMessage={queryError.message} />;
|
|
}
|
|
|
|
if (status !== "success") {
|
|
return <LoadingOverlay />;
|
|
}
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form onSubmit={handleSubmit((data) => onSubmit(data, false))}>
|
|
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
|
|
<div className='flex items-center gap-4'>
|
|
<BackHistoryButton />
|
|
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
|
{t("quotes.edit.title")} {data.reference}
|
|
</h1>
|
|
<ColorBadge label={data.status} className='ml-auto sm:ml-0' />
|
|
|
|
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
|
<CancelButton
|
|
label={t("common.close")}
|
|
variant='secondary'
|
|
size='sm'
|
|
onClick={() => navigate("/quotes")}
|
|
/>
|
|
|
|
<SubmitButton
|
|
label={t("common.save")}
|
|
size='sm'
|
|
disabled={formState.isSubmitting || formState.isLoading || formState.isValidating}
|
|
/>
|
|
|
|
<Button
|
|
size='sm'
|
|
disabled={formState.isSubmitting || formState.isLoading || formState.isValidating}
|
|
onClick={handleSubmit((data) => onSubmit(data, true))}
|
|
>
|
|
{t("common.save_close")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<QuoteGeneralCardEditor />
|
|
<QuotePricesResume />
|
|
<QuoteDetailsCardEditor
|
|
currency={quoteCurrency}
|
|
language={quoteLanguage}
|
|
defaultValues={defaultValues}
|
|
/>
|
|
|
|
<Tabs
|
|
defaultValue='items'
|
|
className='space-y-4'
|
|
value={activeTab}
|
|
onValueChange={setActiveTab}
|
|
>
|
|
<TabsList>
|
|
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
|
|
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
|
|
{/* <TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>*/}
|
|
</TabsList>
|
|
|
|
<TabsContent value='general' forceMount hidden={"general" !== activeTab}></TabsContent>
|
|
<TabsContent value='items' forceMount hidden={"items" !== activeTab}></TabsContent>
|
|
</Tabs>
|
|
|
|
<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>
|
|
);
|
|
};
|