This commit is contained in:
David Arranz 2024-11-14 17:46:31 +01:00
parent 2edc6f4ecd
commit 7eae1295ca
4 changed files with 252 additions and 158 deletions

View File

@ -1,6 +1,6 @@
import { DownloadIcon, FilePenLineIcon } from "lucide-react"; import { ArchiveIcon, CopyIcon, DownloadIcon, EditIcon } from "lucide-react";
import { QuoteStatusBadge } from "@/components"; import { ButtonGroup } from "@/components";
import { useCustomLocalization } from "@/lib/hooks"; import { useCustomLocalization } from "@/lib/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
@ -38,10 +38,12 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const { useOne, useSetStatus, useSentTo, useDownloader, getQuotePDFFilename } = useQuotes(); const { useOne, useSetStatus, useSentTo, useDownloader, useDuplicate, getQuotePDFFilename } =
useQuotes();
const { data, status } = useOne(quoteId); const { data, status } = useOne(quoteId);
const { mutate: setStatusMutation } = useSetStatus(); const { mutate: setStatusMutation } = useSetStatus();
const { mutate: sentToMutation } = useSentTo(quoteId); const { mutate: sentToMutation } = useSentTo(quoteId);
const { mutate: duplicate } = useDuplicate();
const { download, ...downloadProps } = useDownloader(); const { download, ...downloadProps } = useDownloader();
const { formatCurrency, formatNumber } = useCustomLocalization({ const { formatCurrency, formatNumber } = useCustomLocalization({
@ -78,7 +80,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
}, [data]); }, [data]);
const allowToSent = useMemo(() => data?.status === "accepted" && !data?.date_sent, [data]); const allowToSent = useMemo(() => data?.status === "accepted" && !data?.date_sent, [data]);
const isSent = useMemo(() => data?.status === "accepted" && data?.date_sent, [data]); const isSent = useMemo(() => data?.status === "accepted" && !!data?.date_sent, [data]);
const handleOnChangeStatus = (newStatus: string) => { const handleOnChangeStatus = (newStatus: string) => {
setStatusMutation( setStatusMutation(
@ -88,6 +90,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
onSuccess: () => { onSuccess: () => {
toast({ toast({
description: t("quotes.quote_status_editor.toast_status_changed"), description: t("quotes.quote_status_editor.toast_status_changed"),
variant: "success",
}); });
}, },
} }
@ -101,6 +104,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
onSuccess: () => { onSuccess: () => {
toast({ toast({
description: t("quotes.quote_sent_to_editor.toast_status_changed"), description: t("quotes.quote_sent_to_editor.toast_status_changed"),
variant: "success",
}); });
}, },
} }
@ -110,6 +114,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
const handleFinishDownload = useCallback(() => { const handleFinishDownload = useCallback(() => {
toast({ toast({
description: t("quotes.downloading_dialog.toast_success"), description: t("quotes.downloading_dialog.toast_success"),
variant: "success",
}); });
}, [toast]); }, [toast]);
@ -135,6 +140,41 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
); );
} }
const handleDuplicateQuote = (id: string) => {
duplicate(
{
id,
},
{
onSuccess(data) {
toast({
description: t("quotes.duplicate_action.toast_success"),
variant: "success",
});
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
},
}
);
};
const handleArchiveQuote = (id: string) => {
setStatusMutation(
{ id, newStatus: "archived" },
{
onSuccess: () => {
toast({
description: t("quotes.quote_status_editor.toast_status_changed", {
newStatus: t("quotes.status.archived"),
}),
variant: "success",
});
},
}
);
};
return ( return (
<> <>
<DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} /> <DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} />
@ -143,74 +183,94 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
<CardHeader className='gap-3 border-b bg-accent'> <CardHeader className='gap-3 border-b bg-accent'>
<CardTitle className='flex items-center justify-between text-lg'> <CardTitle className='flex items-center justify-between text-lg'>
<span>{t("quotes.list.resume.title")}</span> <span>{t("quotes.list.resume.title")}</span>
<QuoteStatusBadge className='text-sm' status={data.status} /> <QuoteStatusEditor
status={data.status}
onChangeStatus={handleOnChangeStatus}
type='badge'
/>
</CardTitle> </CardTitle>
<div className='flex mr-auto text-foreground'> <div className='flex w-full mr-auto text-foreground'>
<div className='flex items-center gap-1'> <div className='flex justify-between w-full'>
{allowToSent && !isSent && ( <ButtonGroup className='gap-0'>
<> <Tooltip>
<QuoteSentToEditor quote={data} onSentTo={handleOnSentTo} /> <TooltipTrigger asChild>
<QuoteStatusEditor status={data.status} onChangeStatus={handleOnChangeStatus} /> <Button
</> variant='outline'
)} size='icon'
disabled={isSent}
onClick={(e) => {
e.preventDefault();
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
}}
>
<EditIcon className='w-4 h-4' />
<span className='sr-only'>Editar</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Editar</p>
</TooltipContent>
</Tooltip>
{!isSent && ( <Tooltip>
<> <TooltipTrigger asChild>
<Button <Button
size='sm' variant='outline'
variant='default' size='icon'
className='h-8 gap-1' onClick={(e) => {
onClick={(e) => { e.preventDefault();
e.preventDefault(); handleDuplicateQuote(data.id);
navigate(`/quotes/edit/${data.id}`, { relative: "path" }); }}
}} >
> <CopyIcon className='w-4 h-4' />
<FilePenLineIcon className='h-3.5 w-3.5' /> <span className='sr-only'>Duplicar</span>
<span className='sr-only md:not-sr-only md:whitespace-nowrap'> </Button>
{t("quotes.list.columns.actions.edit")} </TooltipTrigger>
</span> <TooltipContent>
</Button> <p>Duplicar</p>
</TooltipContent>
</Tooltip>
<QuoteStatusEditor status={data.status} onChangeStatus={handleOnChangeStatus} /> <Tooltip>
</> <TooltipTrigger asChild>
)} <Button variant='outline' size='icon' onClick={handleDownload}>
<DownloadIcon className='w-4 h-4' />
<span className='sr-only'>Descargar</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Descargar</p>
</TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
size='sm' variant='outline'
variant='outline' size='icon'
className='h-8 gap-1' disabled={isSent}
onClick={handleDownload} onClick={(e) => {
> e.preventDefault();
<DownloadIcon className='h-3.5 w-3.5 ' /> handleArchiveQuote(data.id);
<span className={isSent ? "" : "sr-only"}> }}
{t("quotes.list.resume.download_quote")} >
</span> <ArchiveIcon className='w-4 h-4' />
</Button> <span className='sr-only'>Archivar</span>
</TooltipTrigger> </Button>
<TooltipContent>{t("quotes.list.resume.download_quote")}</TooltipContent> </TooltipTrigger>
</Tooltip> <TooltipContent>
<p>Archivar</p>
</TooltipContent>
</Tooltip>
</ButtonGroup>
{/*<DropdownMenu> <ButtonGroup>
<DropdownMenuTrigger> <QuoteSentToEditor
<Tooltip> disabled={isSent || !allowToSent}
<TooltipTrigger asChild> id={data.id}
<Button size='icon' variant='outline' className='w-8 h-8'> onSentTo={handleOnSentTo}
<MoreVertical className='h-3.5 w-3.5' /> />
<span className='sr-only'>{t("common.more")}</span> </ButtonGroup>
</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>
</div> </div>
</CardHeader> </CardHeader>

View File

@ -19,7 +19,7 @@ import { useNavigate } from "react-router-dom";
import { useQuotes } from "../hooks"; import { useQuotes } from "../hooks";
import { DownloadQuoteDialog } from "./DownloadQuoteDialog"; import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
import { QuoteResume } from "./QuoteResume"; import { QuoteResume } from "./QuoteResume";
import { QuoteStatusEditor } from "./editors"; import { QuoteSentToEditor, QuoteStatusEditor } from "./editors";
export const QuotesDataTable = ({ export const QuotesDataTable = ({
status = "all", status = "all",
@ -89,6 +89,23 @@ export const QuotesDataTable = ({
); );
}; };
const handleArchiveQuote = (id: string) => {
setStatusMutation(
{ id, newStatus: "archived" },
{
onSuccess: () => {
toast({
description: t("quotes.quote_status_editor.toast_status_changed", {
newStatus: t("quotes.status.archived"),
}),
variant: "success",
});
},
}
);
};
const handleOnChangeStatus = (id: string, newStatus: string) => { const handleOnChangeStatus = (id: string, newStatus: string) => {
setStatusMutation( setStatusMutation(
{ id, newStatus }, { id, newStatus },
@ -148,9 +165,7 @@ export const QuotesDataTable = ({
<QuoteStatusEditor <QuoteStatusEditor
type='badge' type='badge'
status={original.status} status={original.status}
onChangeStatus={(newStatus: string, oldStatus: string) => onChangeStatus={(newStatus: string) => handleOnChangeStatus(original.id, newStatus)}
handleOnChangeStatus(original.id, newStatus)
}
/> />
), ),
}, },
@ -162,12 +177,17 @@ export const QuotesDataTable = ({
), ),
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => { cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
const quoteDate = UTCDateValue.create(original.date_sent); const quoteDate = UTCDateValue.create(original.date_sent);
const isSuccess = quoteDate.isSuccess && !quoteDate.object.isEmpty();
return ( return (
<div className='text-left text-ellipsis'> <div className='text-left text-ellipsis'>
{quoteDate.isSuccess ? ( {isSuccess ? (
<ColorBadge label={quoteDate.object.toLocaleDateString("es-ES")} /> <ColorBadge
label={quoteDate.object.toLocaleDateString("es-ES")}
className='text-green-800 bg-green-100 hover:text-green-800 hover:bg-green-100'
/>
) : ( ) : (
<>-</> <></>
)} )}
</div> </div>
); );
@ -217,7 +237,7 @@ export const QuotesDataTable = ({
})} })}
</div> </div>
), ),
size: 600, size: 500,
}, },
/*{ /*{
@ -240,86 +260,96 @@ export const QuotesDataTable = ({
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => { cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
const allowToSent = original?.status === "accepted" && !original?.date_sent; const allowToSent = original?.status === "accepted" && !original?.date_sent;
const isSent = original?.status === "accepted" && !!original?.date_sent; const isSent = original?.status === "accepted" && !!original?.date_sent;
const isArchived = original?.status === "archived";
return ( return (
<ButtonGroup className='gap-0'> <div className='flex gap-1'>
<Tooltip> <ButtonGroup className='gap-0'>
<TooltipTrigger asChild> <Tooltip>
<Button <TooltipTrigger asChild>
variant='ghost' <Button
size='icon' variant='ghost'
disabled={isSent} size='icon'
onClick={(e) => { disabled={isSent}
e.preventDefault(); onClick={(e) => {
handleEditQuote(original); e.preventDefault();
}} handleEditQuote(original);
> }}
<EditIcon className='w-4 h-4' /> >
<span className='sr-only'>Editar</span> <EditIcon className='w-4 h-4' />
</Button> <span className='sr-only'>Editar</span>
</TooltipTrigger> </Button>
<TooltipContent> </TooltipTrigger>
<p>Editar</p> <TooltipContent>
</TooltipContent> <p>Editar</p>
</Tooltip> </TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant='ghost' variant='ghost'
size='icon' size='icon'
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleDuplicateQuote(original.id); handleDuplicateQuote(original.id);
}} }}
> >
<CopyIcon className='w-4 h-4' /> <CopyIcon className='w-4 h-4' />
<span className='sr-only'>Duplicar</span> <span className='sr-only'>Duplicar</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Duplicar</p> <p>Duplicar</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant='ghost' variant='ghost'
size='icon' size='icon'
onClick={() => { onClick={() => {
download(original.id, getQuotePDFFilename(original)); download(original.id, getQuotePDFFilename(original));
}} }}
> >
<DownloadIcon className='w-4 h-4' /> <DownloadIcon className='w-4 h-4' />
<span className='sr-only'>Descargar</span> <span className='sr-only'>Descargar</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Descargar</p> <p>Descargar</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant='ghost' variant='ghost'
size='icon' size='icon'
disabled={isSent} disabled={isArchived || isSent}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
//handleArchiveQuote(original) handleArchiveQuote(original.id);
}} }}
> >
<ArchiveIcon className='w-4 h-4' /> <ArchiveIcon className='w-4 h-4' />
<span className='sr-only'>Archivar</span> <span className='sr-only'>Archivar</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Archivar</p> <p>Archivar</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>
<ButtonGroup>
<QuoteSentToEditor
id={original.id}
onSentTo={() => null}
disabled={isSent || !allowToSent}
/>
</ButtonGroup>
</div>
); );
}, },
}, },

View File

@ -10,26 +10,30 @@ import {
AlertDialogTrigger, AlertDialogTrigger,
Button, Button,
} from "@/ui"; } from "@/ui";
import { IGetQuote_Response_DTO } from "@shared/contexts";
import { t } from "i18next"; import { t } from "i18next";
import { SendIcon } from "lucide-react"; import { SendIcon } from "lucide-react";
export const QuoteSentToEditor = ({ export const QuoteSentToEditor = ({
quote, id,
onSentTo, onSentTo,
disabled,
}: { }: {
quote: IGetQuote_Response_DTO; id: string;
onSentTo: (quoteId: string) => void; onSentTo: (quoteId: string) => void;
disabled?: boolean;
}) => { }) => {
const handleSentTo = () => { const handleSentTo = () => {
onSentTo(quote.id); onSentTo(id);
}; };
return ( return (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild disabled={disabled}>
<Button size='sm' variant='default' className='h-8 gap-1'> <Button
<SendIcon className='h-3.5 w-3.5' /> variant='ghost'
className='h-10 gap-1 text-primary disabled:text-secondary-foreground'
>
<SendIcon className='w-4 h-4' />
{t("quotes.quote_sent_to_editor.trigger_button")} {t("quotes.quote_sent_to_editor.trigger_button")}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>

View File

@ -50,7 +50,7 @@ export const ColorBadge = ({ label, className }: ColorBadgeProps) => {
const [color, backgroundColor] = stringToColorPair(label); const [color, backgroundColor] = stringToColorPair(label);
return ( return (
<Badge className={className} style={{ backgroundColor, color }}> <Badge className={className} style={className ? {} : { backgroundColor, color }}>
{label} {label}
</Badge> </Badge>
); );