300 lines
11 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
};
|