Quote Status
This commit is contained in:
parent
adcc6e3028
commit
1be73865ea
@ -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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`,
|
||||
|
||||
80
client/src/components/QuoteStatusBadge/QuoteStatusBadge.tsx
Normal file
80
client/src/components/QuoteStatusBadge/QuoteStatusBadge.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
client/src/components/QuoteStatusBadge/index.ts
Normal file
1
client/src/components/QuoteStatusBadge/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./QuoteStatusBadge";
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user