Quote Status

This commit is contained in:
David Arranz 2024-11-13 17:28:58 +01:00
parent adcc6e3028
commit 1be73865ea
7 changed files with 212 additions and 98 deletions

View File

@ -1,6 +1,6 @@
import { DownloadIcon, FilePenLineIcon } from "lucide-react";
import { ColorBadge } from "@/components";
import { QuoteStatusBadge } from "@/components";
import { useCustomLocalization } from "@/lib/hooks";
import { cn } from "@/lib/utils";
import {
@ -40,7 +40,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
const { useOne, useSetStatus, useSentTo, useDownloader, getQuotePDFFilename } = useQuotes();
const { data, status } = useOne(quoteId);
const { mutate: setStatusMutation } = useSetStatus(quoteId);
const { mutate: setStatusMutation } = useSetStatus();
const { mutate: sentToMutation } = useSentTo(quoteId);
const { download, ...downloadProps } = useDownloader();
@ -80,9 +80,9 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
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) => {
const handleOnChangeStatus = (newStatus: string) => {
setStatusMutation(
{ newStatus },
{ id: data!!.id, newStatus },
{
onSuccess: () => {
@ -143,14 +143,14 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
<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}`)} />
<QuoteStatusBadge className='text-sm' 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} />
<QuoteStatusEditor status={data.status} onChangeStatus={handleOnChangeStatus} />
</>
)}
@ -171,7 +171,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
</span>
</Button>
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
<QuoteStatusEditor status={data.status} onChangeStatus={handleOnChangeStatus} />
</>
)}

View File

@ -8,29 +8,18 @@ import {
} from "@/components";
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
import { useDataTable, useDataTableContext } from "@/lib/hooks";
import {
Button,
Card,
CardContent,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/ui";
import { Button, Card, CardContent, Tooltip, TooltipContent, TooltipTrigger } from "@/ui";
import { useToast } from "@/ui/use-toast";
import { IListQuotes_Response_DTO, UTCDateValue } from "@shared/contexts";
import { ColumnDef, Row } from "@tanstack/react-table";
import { t } from "i18next";
import { FilePenLineIcon, MoreVerticalIcon, SendIcon } from "lucide-react";
import { ArchiveIcon, CopyIcon, DownloadIcon, EditIcon } from "lucide-react";
import { useCallback, useEffect, useId, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuotes } from "../hooks";
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
import { QuoteResume } from "./QuoteResume";
import { QuoteStatusEditor } from "./editors";
export const QuotesDataTable = ({
status = "all",
@ -49,7 +38,9 @@ export const QuotesDataTable = ({
const [activeRow, setActiveRow] = useState<Row<IListQuotes_Response_DTO> | undefined>(undefined);
const { useList, useDownloader, getQuotePDFFilename } = useQuotes();
const { useList, useDownloader, useSetStatus, getQuotePDFFilename } = useQuotes();
const { mutate: setStatusMutation } = useSetStatus();
const { data, isPending, isError, error } = useList({
pagination: {
@ -79,6 +70,20 @@ export const QuotesDataTable = ({
[navigate, toast]
);
const handleOnChangeStatus = (id: string, newStatus: string) => {
setStatusMutation(
{ id, newStatus },
{
onSuccess: () => {
toast({
description: t("quotes.quote_status_editor.toast_status_changed"),
});
},
}
);
};
const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, unknown>[]>(() => {
const columns = [
{
@ -118,7 +123,13 @@ export const QuotesDataTable = ({
header: () => <>{t("quotes.list.columns.status")}</>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => (
<ColorBadge label={t(`quotes.status.${original.status}`)} />
<QuoteStatusEditor
type='badge'
status={original.status}
onChangeStatus={(newStatus: string, oldStatus: string) =>
handleOnChangeStatus(original.id, newStatus)
}
/>
),
},
{
@ -206,72 +217,86 @@ export const QuotesDataTable = ({
header: () => null,
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
const allowToSent = original?.status === "accepted" && !original?.date_sent;
const isSent = original?.status === "accepted" && original?.date_sent;
const isSent = original?.status === "accepted" && !!original?.date_sent;
return (
<ButtonGroup>
<ButtonGroup className='gap-0'>
<Tooltip>
<TooltipTrigger asChild>
<>
{allowToSent && !isSent && (
<Button
size='sm'
variant='default'
className='h-8 gap-1'
onClick={(e) => {
e.preventDefault();
//handleSentToUecko(original);
}}
>
<SendIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
{t("quotes.list.columns.actions.sent_to")}
</span>
</Button>
)}
{!isSent && (
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={(e) => {
e.preventDefault();
handleEditQuote(original);
}}
>
<FilePenLineIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
{t("quotes.list.columns.actions.edit")}
</span>
</Button>
)}
</>
<Button
variant='ghost'
size='icon'
disabled={isSent}
onClick={(e) => {
e.preventDefault();
handleEditQuote(original);
}}
>
<EditIcon className='w-4 h-4' />
<span className='sr-only'>Editar</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("quotes.list.columns.actions.sent_to_uecko")}</p>
<p>Editar</p>
</TooltipContent>
</Tooltip>
<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'>{t("common.more")}</span>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
onClick={(e) => {
e.preventDefault();
//handleDuplicateQuote(original);
}}
>
<CopyIcon className='w-4 h-4' />
<span className='sr-only'>Duplicar</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem
</TooltipTrigger>
<TooltipContent>
<p>Duplicar</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
onClick={() => {
download(original.id, getQuotePDFFilename(original));
}}
>
Download
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>{t("common.archive")}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DownloadIcon className='w-4 h-4' />
<span className='sr-only'>Descargar</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Descargar</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
disabled={isSent}
onClick={(e) => {
e.preventDefault();
//handleArchiveQuote(original)
}}
>
<ArchiveIcon className='w-4 h-4' />
<span className='sr-only'>Archivar</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Archivar</p>
</TooltipContent>
</Tooltip>
</ButtonGroup>
);
},

View File

@ -1,3 +1,4 @@
import { QuoteStatusBadge } from "@/components";
import { cn } from "@/lib/utils";
import {
Button,
@ -12,7 +13,6 @@ import {
Switch,
} from "@/ui";
import { DialogDescription } from "@radix-ui/react-dialog";
import { IGetQuote_Response_DTO } from "@shared/contexts";
import { t } from "i18next";
import { RefreshCwIcon } from "lucide-react";
import { useEffect, useState } from "react";
@ -31,37 +31,43 @@ const quoteStatusTransitions: Record<TQuoteStatus, TQuoteStatus[]> = {
};
export const QuoteStatusEditor = ({
quote,
type = "button",
status,
onChangeStatus,
}: {
quote: IGetQuote_Response_DTO;
onChangeStatus: (quoteId: string, newStatus: string) => void;
type?: "button" | "badge";
status: string;
onChangeStatus: (newStatus: string, oldStatus: string) => void;
}) => {
const [newStatus, setNewStatus] = useState<string>("");
const [newStatus, setNewStatus] = useState<string>(status);
const changeStatus = (newStatus: string) => setNewStatus(newStatus);
useEffect(() => {
if (quote) {
setNewStatus(quote.status);
if (status) {
setNewStatus(status);
}
}, [quote]);
}, [status]);
const handleChangeStatus = () => {
if (newStatus !== quote.status) {
onChangeStatus(quote.id, newStatus);
if (newStatus !== status) {
onChangeStatus(newStatus, status);
}
};
return (
<Dialog>
<DialogTrigger asChild>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<RefreshCwIcon className='h-3.5 w-3.5' />
<span className='sr-only md:not-sr-only md:whitespace-nowrap'>
{t("quotes.quote_status_editor.trigger_button")}
</span>
</Button>
{type === "button" ? (
<Button size='sm' variant='outline' className='h-8 gap-1'>
<RefreshCwIcon className='h-3.5 w-3.5' />
<span className='sr-only md:not-sr-only md:whitespace-nowrap'>
{t("quotes.quote_status_editor.trigger_button")}
</span>
</Button>
) : (
<QuoteStatusBadge status={status} isEditable />
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
@ -71,9 +77,10 @@ export const QuoteStatusEditor = ({
<div className='grid gap-4 py-4'>
{QuoteStatus.map((_status) => {
const isDisabled = !quoteStatusTransitions[quote.status as TQuoteStatus].includes(
const isDisabled = false;
/*const isDisabled = !quoteStatusTransitions[status as TQuoteStatus].includes(
_status as TQuoteStatus
);
);*/
return (
<div key={_status} className='flex items-start space-x-4'>
@ -108,7 +115,7 @@ export const QuoteStatusEditor = ({
</DialogClose>
<DialogClose asChild>
<Button onClick={handleChangeStatus} disabled={newStatus === quote.status}>
<Button onClick={handleChangeStatus} disabled={newStatus === status}>
{t("quotes.quote_status_editor.submit_button")}
</Button>
</DialogClose>

View File

@ -147,13 +147,12 @@ export const useQuotes = () => {
});
},
useSetStatus: (id?: string) => {
useSetStatus: () => {
const queryClient = useQueryClient();
return useMutation<void, TDataSourceError, ISetStatusQuote_Request_DTO>({
mutationKey: keys().data().resource("quotes").action("one").id(id).params().get(),
return useMutation<void, TDataSourceError, ISetStatusQuote_Request_DTO & { id: string }>({
mutationFn: (data) => {
const { newStatus } = data;
const { id, newStatus } = data;
return dataSource.custom({
url: `${dataSource.getApiUrl()}/quotes/${id}/setStatus`,

View File

@ -0,0 +1,80 @@
import { cn } from "@/lib/utils";
import { Badge } from "@/ui";
import { t } from "i18next";
import { RefreshCwIcon } from "lucide-react";
export type QuoteStatusBadgeProps = {
status: string;
isEditable?: boolean;
className?: string;
};
type QuoteStatus = "draft" | "ready" | "delivered" | "accepted" | "rejected" | "archived";
const statusColorConfig: Record<
QuoteStatus,
{ color: string; bgColor: string; hoverColor: string; hoverBgColor: string }
> = {
draft: {
color: "text-gray-700",
bgColor: "bg-gray-200",
hoverColor: "hover:text-gray-900",
hoverBgColor: "hover:bg-gray-300",
},
ready: {
color: "text-blue-700",
bgColor: "bg-blue-200",
hoverColor: "hover:text-blue-900",
hoverBgColor: "hover:bg-blue-300",
},
delivered: {
color: "text-yellow-700",
bgColor: "bg-yellow-200",
hoverColor: "hover:text-yellow-900",
hoverBgColor: "hover:bg-yellow-300",
},
accepted: {
color: "text-green-700",
bgColor: "bg-green-200",
hoverColor: "hover:text-green-900",
hoverBgColor: "hover:bg-green-300",
},
rejected: {
color: "text-red-700",
bgColor: "bg-red-200",
hoverColor: "hover:text-red-900",
hoverBgColor: "hover:bg-red-300",
},
archived: {
color: "text-purple-700",
bgColor: "bg-purple-200",
hoverColor: "hover:text-purple-900",
hoverBgColor: "hover:bg-purple-300",
},
};
export const QuoteStatusBadge = ({
status,
isEditable,
className,
...props
}: QuoteStatusBadgeProps) => {
return (
<Badge
className={cn(
statusColorConfig[status as QuoteStatus].bgColor,
statusColorConfig[status as QuoteStatus].color,
statusColorConfig[status as QuoteStatus].hoverBgColor,
statusColorConfig[status as QuoteStatus].hoverColor,
"transition-colors duration-200 cursor-pointer flex items-center group",
className
)}
{...props}
>
{t(`quotes.status.${status}`)}
{isEditable && (
<RefreshCwIcon className='w-3 h-3 ml-2 transition-opacity opacity-0 group-hover:opacity-100' />
)}
</Badge>
);
};

View File

@ -0,0 +1 @@
export * from "./QuoteStatusBadge";

View File

@ -13,6 +13,8 @@ export * from "./LoadingOverlay";
export * from "./LoadingSpinner";
export * from "./PDFViewer";
export * from "./ProtectedRoute";
export * from "./QuoteStatusBadge";
//export * from "./SorteableDataTable";
export * from "./TailwindIndicator";