Presupuestador_web/client/src/app/quotes/components/QuoteResume.tsx

300 lines
11 KiB
TypeScript

import { DownloadIcon, FilePenLineIcon } from "lucide-react";
import { ColorBadge } from "@/components";
import { useCustomLocalization } from "@/lib/hooks";
import { cn } from "@/lib/utils";
import {
Button,
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
Separator,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/ui";
import { useToast } from "@/ui/use-toast";
import { formatDateToYYYYMMDD } from "@shared/utilities/helpers";
import { t } from "i18next";
import { useCallback, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useQuotes } from "../hooks";
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
import { QuotePDFPreview } from "./QuotePDFPreview";
import { QuoteSentToEditor, QuoteStatusEditor } from "./editors";
type QuoteResumeProps = {
quoteId?: string;
className?: string;
};
export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
const navigate = useNavigate();
const { toast } = useToast();
const { useOne, useSetStatus, useSentTo, useDownloader, getQuotePDFFilename } = useQuotes();
const { data, status } = useOne(quoteId);
const { mutate: setStatusMutation } = useSetStatus(quoteId);
const { mutate: sentToMutation } = useSentTo(quoteId);
const { download, ...downloadProps } = useDownloader();
const { formatCurrency, formatNumber } = useCustomLocalization({
locale: data?.lang_code || "ES",
});
/*const currency_symbol = useMemo(() => {
const currencyOrError = data
? CurrencyData.createFromCode(data?.currency_code)
: CurrencyData.createDefaultCode();
return currencyOrError.isSuccess ? currencyOrError.object.symbol : "";
}, [data]);*/
const totals = useMemo(() => {
return data
? {
subtotal_price: formatCurrency(data.subtotal_price),
discount: formatNumber(data.discount),
discount_price: formatCurrency(data.discount_price),
tax: formatNumber(data.tax),
tax_price: formatCurrency(data.tax_price),
total_price: formatCurrency(data.total_price),
}
: {
subtotal_price: "0,00 €",
discount: "0",
discount_price: "0,00 €",
tax: "0",
tax_price: "0,00 €",
total_price: "0,00 €",
};
}, [data]);
const allowToSent = useMemo(() => data?.status === "accepted" && !data?.date_sent, [data]);
const isSent = useMemo(() => data?.status === "accepted" && data?.date_sent, [data]);
const handleOnChangeStatus = (_: string, newStatus: string) => {
setStatusMutation(
{ newStatus },
{
onSuccess: () => {
toast({
description: t("quotes.quote_status_editor.toast_status_changed"),
});
},
}
);
};
const handleOnSentTo = (_: string) => {
sentToMutation(
{ sent_date: formatDateToYYYYMMDD(new Date()) },
{
onSuccess: () => {
toast({
description: t("quotes.quote_sent_to_editor.toast_status_changed"),
});
},
}
);
};
const handleFinishDownload = useCallback(() => {
toast({
description: t("quotes.downloading_dialog.toast_success"),
});
}, [toast]);
const handleDownload = useCallback(() => {
if (data) download(data.id, getQuotePDFFilename(data));
}, [data]);
if (status === "error") {
return null;
}
if (status !== "success") {
return null;
}
if (!data) {
return (
<Card className={cn("overflow-hidden", className)}>
<CardContent className='px-4 py-6 text-center'>
<p className='mx-auto'>Select a quote</p>
</CardContent>
</Card>
);
}
return (
<>
<DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} />
<Tabs defaultValue='resume'>
<Card className='w-[390px] overflow-hidden'>
<CardHeader className='gap-3 border-b bg-accent'>
<CardTitle className='flex items-center justify-between text-lg'>
<span>{t("quotes.list.resume.title")}</span>
<ColorBadge className='text-sm' label={t(`quotes.status.${data.status}`)} />
</CardTitle>
<div className='flex mr-auto text-foreground'>
<div className='flex items-center gap-1'>
{allowToSent && !isSent && (
<>
<QuoteSentToEditor quote={data} onSentTo={handleOnSentTo} />
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
</>
)}
{!isSent && (
<>
<Button
size='sm'
variant='default'
className='h-8 gap-1'
onClick={(e) => {
e.preventDefault();
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
}}
>
<FilePenLineIcon className='h-3.5 w-3.5' />
<span className='sr-only md:not-sr-only md:whitespace-nowrap'>
{t("quotes.list.columns.actions.edit")}
</span>
</Button>
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
</>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={handleDownload}
>
<DownloadIcon className='h-3.5 w-3.5 ' />
<span className={isSent ? "" : "sr-only"}>
{t("quotes.list.resume.download_quote")}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("quotes.list.resume.download_quote")}</TooltipContent>
</Tooltip>
{/*<DropdownMenu>
<DropdownMenuTrigger>
<Tooltip>
<TooltipTrigger asChild>
<Button size='icon' variant='outline' className='w-8 h-8'>
<MoreVertical className='h-3.5 w-3.5' />
<span className='sr-only'>{t("common.more")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.more")}</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem className='not-sr-only 2xl:sr-only'>
<DownloadIcon className='h-3.5 w-3.5 mr-2' />
<span>{t("quotes.list.preview.download_quote")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>*/}
</div>
</div>
</CardHeader>
<CardContent className='p-6 text-sm'>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='resume'>{t("quotes.list.resume.tabs.resume")}</TabsTrigger>
<TabsTrigger value='preview'>{t("quotes.list.resume.tabs.preview")}</TabsTrigger>
</TabsList>
<TabsContent value='resume' className='pt-4'>
<div className='grid gap-3'>
<div className='grid gap-3'>
<div className='font-semibold'>{t("quotes.list.resume.quote_information")}</div>
<dl className='grid gap-3'>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>
{t("quotes.form_fields.reference.label")}
</dt>
<dd className='font-medium'>{data.reference}</dd>
</div>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>
{t("quotes.form_fields.date.label")}
</dt>
<dd className='font-medium'>{new Date(data.date).toLocaleDateString()}</dd>
</div>
<div className='flex items-start justify-between'>
<dt className='text-muted-foreground whitespace-nowrap'>
{t("quotes.form_fields.customer_reference.label")}
</dt>
<dd className='font-medium text-right whitespace-break-spaces'>
{data.customer_reference}
</dd>
</div>
</dl>
</div>
<Separator className='my-4' />
<div className='grid gap-3'>
<div className='font-semibold'>
{t("quotes.list.resume.customer_information")}
</div>
<div>{data.customer_information}</div>
</div>
<Separator className='my-4' />
<div className='font-semibold'>{t("quotes.list.resume.price_information")}</div>
<ul className='grid gap-3'>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
{t("quotes.form_fields.subtotal_price.label")}
</span>
<span>{totals.subtotal_price}</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
{t("quotes.form_fields.discount_value.label", { value: totals.discount })}
</span>
<span>{totals.discount_price}</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
{t("quotes.form_fields.tax_value.label", { value: totals.tax })}
</span>
<span>{totals.tax_price}</span>
</li>
<li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>
{t("quotes.form_fields.total_price.label")}
</span>
<span>{totals.total_price}</span>
</li>
</ul>
</div>
</TabsContent>
<TabsContent value='preview'>
<QuotePDFPreview quote={data} />
</TabsContent>
</CardContent>
<CardFooter className='flex flex-row items-center px-6 py-3 border-t bg-accent'>
<div className='text-xs text-muted-foreground'></div>
</CardFooter>
</Card>
</Tabs>
</>
);
};