This commit is contained in:
David Arranz 2024-08-29 16:29:10 +02:00
parent 3cfe438f1b
commit 65ea9f212c
8 changed files with 55 additions and 142 deletions

View File

@ -1,21 +1,23 @@
import { Button, ButtonProps } from "@/ui"; import { Button, ButtonProps } from "@/ui";
import { t } from "i18next"; import { t } from "i18next";
import { PlusCircleIcon } from "lucide-react"; import { PlusCircleIcon } from "lucide-react";
import { forwardRef } from "react";
export interface AppendEmptyRowButtonProps extends ButtonProps { export interface AppendEmptyRowButtonProps extends ButtonProps {
label?: string; label?: string;
className?: string; className?: string;
} }
export const AppendEmptyRowButton = ({ export const AppendEmptyRowButton = forwardRef(
label = t("common.append_empty_row"), (
className, { label = t("common.append_empty_row"), className, ...props }: AppendEmptyRowButtonProps,
...props ref
}: AppendEmptyRowButtonProps): JSX.Element => ( ): JSX.Element => (
<Button type='button' variant='outline' {...props}> <Button type='button' variant='outline' {...props}>
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} /> <PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>} {label && <>{label}</>}
</Button> </Button>
)
); );
AppendEmptyRowButton.displayName = "AddNewRowButton"; AppendEmptyRowButton.displayName = "AddNewRowButton";

View File

@ -1,35 +1,19 @@
import { PDFViewer } from "@/components"; import { PDFViewer } from "@/components";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import { Card, CardContent, CardHeader, Skeleton } from "@/ui";
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Skeleton,
} from "@/ui";
import { useToast } from "@/ui/use-toast"; import { useToast } from "@/ui/use-toast";
import { IListQuotes_Response_DTO } from "@shared/contexts"; import { IListQuotes_Response_DTO } from "@shared/contexts";
import { t } from "i18next"; import { t } from "i18next";
import { DownloadIcon, MoreVerticalIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Trans } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useQuotes } from "../hooks"; import { useQuotes } from "../hooks";
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
const QuotePDFPreview = ({ const QuotePDFPreview = ({
quote, quote,
className, className,
}: { }: {
quote?: IListQuotes_Response_DTO; quote?: IListQuotes_Response_DTO;
className: string; className?: string;
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
@ -99,84 +83,7 @@ const QuotePDFPreview = ({
} }
return ( return (
<> <PDFViewer file={URLReport} className='object-contain' onThumbnailClick={handleDownload} />
<Card className={cn("overflow-hidden", className)}>
<CardHeader className='bg-muted/50'>
<CardTitle className='flex items-center gap-2 text-lg group'>
{t("quotes.list.preview.quote")}
<div className='flex items-center gap-1 ml-auto'>
{/*<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={(e) => {
e.preventDefault();
printJS({
printable: reportData?.original,
type: "pdf",
showModal: false,
modalMessage: "Cargando...",
});
}}
>
<PrinterIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
{t("common.print")}
</span>
</Button>*/}
<Button size='sm' variant='outline' className='h-8 gap-1' onClick={handleDownload}>
<DownloadIcon className='h-3.5 w-3.5' />
<span className='xl:sr-only 2xl:not-sr-only 2xl:whitespace-nowrap'>
{t("quotes.list.preview.download_quote")}
</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size='icon' variant='outline' className='w-8 h-8'>
<MoreVerticalIcon className='h-3.5 w-3.5' />
<span className='sr-only'>
<Trans i18nKey={"common.more"} />
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
navigate(`/quotes/edit/${quote.id}`, { relative: "path" });
}}
>
<Trans i18nKey={"common.edit"} />
</DropdownMenuItem>
<DropdownMenuItem>
<Trans i18nKey={"common.duplicate"} />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trans i18nKey={"common.archive"} />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardTitle>
{false && (
<CardDescription className='grid grid-cols-1 gap-2 space-y-2'>
{quote?.reference}
{quote?.date.toString()}
</CardDescription>
)}
</CardHeader>
<CardContent className='py-4'>
<PDFViewer
file={URLReport}
className='object-contain'
onThumbnailClick={handleDownload}
/>
</CardContent>
</Card>
<DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} />
</>
); );
}; };

View File

@ -28,6 +28,7 @@ import { useNavigate } from "react-router-dom";
import { useQuotes } from "../hooks"; import { useQuotes } from "../hooks";
import { DownloadQuoteDialog } from "./DownloadQuoteDialog"; import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
import { QuoteStatusEditor } from "./editors"; import { QuoteStatusEditor } from "./editors";
import { QuotePDFPreview } from "./QuotePDFPreview";
type QuoteResumeProps = { type QuoteResumeProps = {
quoteId?: string; quoteId?: string;
@ -43,7 +44,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
const { mutate: setStatusMutation } = useSetStatus(quoteId); const { mutate: setStatusMutation } = useSetStatus(quoteId);
const { download, ...downloadProps } = useDownloader(); const { download, ...downloadProps } = useDownloader();
const { formatCurrency } = useCustomLocalization({ const { formatCurrency, formatNumber } = useCustomLocalization({
locale: data?.lang_code || "ES", locale: data?.lang_code || "ES",
}); });
@ -58,15 +59,21 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
return data return data
? { ? {
subtotal_price: formatCurrency(data.subtotal_price), subtotal_price: formatCurrency(data.subtotal_price),
discount: formatNumber(data.discount),
discount_price: formatCurrency(data.discount_price), discount_price: formatCurrency(data.discount_price),
tax: formatNumber(data.tax),
tax_price: formatCurrency(data.tax_price), tax_price: formatCurrency(data.tax_price),
total_price: formatCurrency(data.total_price), total_price: formatCurrency(data.total_price),
} }
: { : {
subtotal_price: "0,00", subtotal_price: "0,00 €",
discount_price: "0,00", discount: "0",
tax_price: "0,00", discount_price: "0,00 €",
total_price: "0,00", tax: "0",
tax_price: "0,00 €",
total_price: "0,00 €",
}; };
}, [data]); }, [data]);
@ -228,43 +235,32 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
</li> </li>
<li className='flex items-center justify-between'> <li className='flex items-center justify-between'>
<span className='text-muted-foreground'> <span className='text-muted-foreground'>
{t("quotes.form_fields.discount.label")} {t("quotes.form_fields.discount_value.label", { value: totals.discount })}
</span> </span>
<span>$5.00</span> <span>{totals.discount_price}</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
{t("quotes.form_fields.discount_price.label")}
</span>
<span>$25.00</span>
</li> </li>
<li className='flex items-center justify-between'> <li className='flex items-center justify-between'>
<span className='text-muted-foreground'> <span className='text-muted-foreground'>
{t("quotes.form_fields.tax.label")} {t("quotes.form_fields.tax_value.label", { value: totals.tax })}
</span> </span>
<span>$5.00</span> <span>{totals.tax_price}</span>
</li> </li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
{t("quotes.form_fields.tax_price.label")}
</span>
<span>$25.00</span>
</li>
<li className='flex items-center justify-between font-semibold'> <li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'> <span className='text-muted-foreground'>
{t("quotes.form_fields.total_price.label")} {t("quotes.form_fields.total_price.label")}
</span> </span>
<span>$329.00</span> <span>{totals.total_price}</span>
</li> </li>
</ul> </ul>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value='preview'></TabsContent> <TabsContent value='preview'>
<QuotePDFPreview quote={data} />
</TabsContent>
</CardContent> </CardContent>
<CardFooter className='flex flex-row items-center px-6 py-3 border-t bg-accent'> <CardFooter className='flex flex-row items-center px-6 py-3 border-t bg-accent'>
<div className='text-xs text-muted-foreground'>Updated...</div> <div className='text-xs text-muted-foreground'></div>
</CardFooter> </CardFooter>
</Card> </Card>
</Tabs> </Tabs>

View File

@ -15,14 +15,13 @@ export const QuoteGeneralCardEditor = () => {
> >
<div className='grid grid-cols-6 gap-6'> <div className='grid grid-cols-6 gap-6'>
<FormTextField <FormTextField
required
className='col-span-2' className='col-span-2'
label={t("quotes.form_fields.customer_reference.label")} label={t("quotes.form_fields.customer_reference.label")}
description={t("quotes.form_fields.customer_reference.desc")} description={t("quotes.form_fields.customer_reference.desc")}
disabled={formState.disabled} disabled={formState.disabled}
placeholder={t("quotes.form_fields.customer_reference.placeholder")} placeholder={t("quotes.form_fields.customer_reference.placeholder")}
{...register("customer_reference", { {...register("customer_reference")}
required: false,
})}
/> />
<FormDatePickerField <FormDatePickerField

View File

@ -10,6 +10,7 @@ import { calculateQuoteItemTotals, calculateQuoteTotals } from "@/lib/calc";
import { useUnsavedChangesNotifier } from "@/lib/hooks"; import { useUnsavedChangesNotifier } from "@/lib/hooks";
import { useUrlId } from "@/lib/hooks/useUrlId"; import { useUrlId } from "@/lib/hooks/useUrlId";
import { Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui"; import { Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
import { useToast } from "@/ui/use-toast";
import { import {
CurrencyData, CurrencyData,
IGetQuote_QuoteItem_Response_DTO, IGetQuote_QuoteItem_Response_DTO,
@ -20,7 +21,6 @@ import { t } from "i18next";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { QuotePricesResume } from "./components"; import { QuotePricesResume } from "./components";
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors"; import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
import { useQuotes } from "./hooks"; import { useQuotes } from "./hooks";
@ -32,6 +32,7 @@ export type QuoteDataFormItem = IGetQuote_QuoteItem_Response_DTO;
export const QuoteEdit = () => { export const QuoteEdit = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const quoteId = useUrlId(); const quoteId = useUrlId();
const { toast } = useToast();
const [activeTab, setActiveTab] = useState("general"); const [activeTab, setActiveTab] = useState("general");
@ -142,14 +143,13 @@ export const QuoteEdit = () => {
mutate(data, { mutate(data, {
onError: (error) => { onError: (error) => {
console.debug(error); console.debug(error);
toast.error(error.message); toast({ description: error.message });
//alert(error.message); //alert(error.message);
}, },
//onSettled: () => {}, //onSettled: () => {},
onSuccess: () => { onSuccess: () => {
console.log("onsuccess 2");
reset(getValues()); reset(getValues());
toast.success("Cotización guardada"); toast({ description: "Cotización guardada" });
if (shouldRedirect) { if (shouldRedirect) {
navigate("/quotes"); navigate("/quotes");
} }
@ -161,7 +161,6 @@ export const QuoteEdit = () => {
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { unsubscribe } = watch((_, { name, type }) => { const { unsubscribe } = watch((_, { name, type }) => {
console.log("useEffect");
const quote = getValues(); const quote = getValues();
if (name) { if (name) {

View File

@ -1,5 +1,5 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button, Pagination, PaginationContent, PaginationItem } from "@/ui"; import { Button } from "@/ui";
import { useResizeObserver } from "@wojtekmaj/react-hooks"; import { useResizeObserver } from "@wojtekmaj/react-hooks";
import { t } from "i18next"; import { t } from "i18next";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
@ -132,7 +132,7 @@ export const PDFViewer = ({ file, onThumbnailClick, className }: PDFViewerProps)
/> />
</Document> </Document>
<Pagination className='w-auto ml-auto mr-0'> {/* <Pagination className='w-auto ml-auto mr-0'>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
<Button size='icon' variant='outline' className='w-6 h-6'> <Button size='icon' variant='outline' className='w-6 h-6'>
@ -147,7 +147,7 @@ export const PDFViewer = ({ file, onThumbnailClick, className }: PDFViewerProps)
</Button> </Button>
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination> </Pagination>*/}
<div className='flex flex-row justify-center w-full mt-4 space-x-4'> <div className='flex flex-row justify-center w-full mt-4 space-x-4'>
<Button <Button

View File

@ -35,7 +35,7 @@ export const useCustomLocalization = (props: UseLocalizationProps) => {
useGrouping: true, useGrouping: true,
maximumFractionDigits: scale, maximumFractionDigits: scale,
}).format(amount === null ? 0 : amount); }).format(amount === null ? 0 : adjustPrecision({ amount, scale }));
}, },
[locale] [locale]
); );

View File

@ -309,6 +309,11 @@
"placeholder": "", "placeholder": "",
"desc": "Percentage discount" "desc": "Percentage discount"
}, },
"discount_value": {
"label": "Discount ({{value}}%)",
"placeholder": "",
"desc": "Percentage discount"
},
"discount_price": { "discount_price": {
"label": "Discount price", "label": "Discount price",
"placeholder": "", "placeholder": "",
@ -324,6 +329,11 @@
"placeholder": "", "placeholder": "",
"desc": "Percentage Tax" "desc": "Percentage Tax"
}, },
"tax_value": {
"label": "Tax ({{value}}%)",
"placeholder": "",
"desc": "Percentage Tax"
},
"tax_price": { "tax_price": {
"label": "Tax price", "label": "Tax price",
"placeholder": "", "placeholder": "",