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

435 lines
14 KiB
TypeScript
Raw Normal View History

2024-08-20 15:23:25 +00:00
import {
2024-08-25 20:06:24 +00:00
ButtonGroup,
2024-08-20 15:23:25 +00:00
ColorBadge,
DataTable,
DataTableSkeleton,
ErrorOverlay,
SimpleEmptyState,
} from "@/components";
2024-06-29 19:39:25 +00:00
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
import { useDataTable, useDataTableContext } from "@/lib/hooks";
2024-11-13 16:28:58 +00:00
import { Button, Card, CardContent, Tooltip, TooltipContent, TooltipTrigger } from "@/ui";
2024-08-21 09:30:47 +00:00
import { useToast } from "@/ui/use-toast";
2024-08-26 18:29:41 +00:00
import { IListQuotes_Response_DTO, UTCDateValue } from "@shared/contexts";
2024-08-19 09:33:42 +00:00
import { ColumnDef, Row } from "@tanstack/react-table";
2024-06-29 19:39:25 +00:00
import { t } from "i18next";
2024-11-13 16:28:58 +00:00
import { ArchiveIcon, CopyIcon, DownloadIcon, EditIcon } from "lucide-react";
2024-08-22 17:23:12 +00:00
import { useCallback, useEffect, useId, useMemo, useState } from "react";
2024-06-29 19:39:25 +00:00
import { useNavigate } from "react-router-dom";
2024-07-11 18:31:01 +00:00
import { useQuotes } from "../hooks";
2024-08-21 09:30:47 +00:00
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
2024-08-26 18:29:41 +00:00
import { QuoteResume } from "./QuoteResume";
2024-11-14 16:46:31 +00:00
import { QuoteSentToEditor, QuoteStatusEditor } from "./editors";
2024-06-29 19:39:25 +00:00
2024-08-20 11:44:04 +00:00
export const QuotesDataTable = ({
status = "all",
preview = false,
}: {
status?: string;
preview?: boolean;
}) => {
2024-06-29 19:39:25 +00:00
const navigate = useNavigate();
2024-08-21 09:30:47 +00:00
const { toast } = useToast();
2024-08-22 17:23:12 +00:00
const tableId = useId();
const previewId = useId();
2024-06-29 19:39:25 +00:00
const { pagination, globalFilter, isFiltered } = useDataTableContext();
2024-08-20 15:16:41 +00:00
const [activeRow, setActiveRow] = useState<Row<IListQuotes_Response_DTO> | undefined>(undefined);
const { useList, useDownloader, useSetStatus, useDuplicate, getQuotePDFFilename } = useQuotes();
2024-11-13 16:28:58 +00:00
const { mutate: setStatusMutation } = useSetStatus();
const { mutate: duplicate } = useDuplicate();
2024-07-11 18:31:01 +00:00
const { data, isPending, isError, error } = useList({
2024-06-29 19:39:25 +00:00
pagination: {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
},
2024-07-11 18:31:01 +00:00
status,
quickSearchTerm: globalFilter,
2024-06-29 19:39:25 +00:00
});
2024-08-20 21:40:37 +00:00
const { download, ...downloadProps } = useDownloader();
const handleOnRowClick = (row: Row<IListQuotes_Response_DTO>) => {
setActiveRow(row);
};
const handleFinishDownload = useCallback(() => {
toast({
description: t("quotes.downloading_dialog.toast_success"),
});
}, [toast]);
const handleEditQuote = useCallback(
(quote: IListQuotes_Response_DTO) => {
navigate(`/quotes/edit/${quote.id}`, { relative: "path" });
},
[navigate, toast]
);
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" });
},
}
);
};
2024-11-14 16:46:31 +00:00
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",
});
},
}
);
};
2024-11-13 16:28:58 +00:00
const handleOnChangeStatus = (id: string, newStatus: string) => {
setStatusMutation(
{ id, newStatus },
{
onSuccess: () => {
toast({
description: t("quotes.quote_status_editor.toast_status_changed", {
newStatus: t(`quotes.status.${newStatus}`),
}),
variant: "success",
2024-11-13 16:28:58 +00:00
});
},
}
);
};
const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, unknown>[]>(() => {
2024-08-27 18:58:35 +00:00
const columns = [
2024-08-23 16:47:51 +00:00
{
id: "reference" as const,
accessorKey: "reference",
header: () => <>{t("quotes.list.columns.reference")}</>,
cell: ({
row: { original },
renderValue,
}: {
row: { original: IListQuotes_Response_DTO };
renderValue: () => any;
2024-10-25 09:49:10 +00:00
}) => {
2024-11-12 11:58:58 +00:00
//const allowToSent = original?.status === "accepted" && !original?.date_sent;
2024-10-25 09:49:10 +00:00
const isSent = Boolean(original?.status === "accepted" && original?.date_sent);
return (
<Button
size='sm'
variant='link'
disabled={isSent}
2024-10-25 09:49:10 +00:00
className='h-8 gap-1 px-0 text-left text-ellipsis'
onClick={(e) => {
e.preventDefault();
handleEditQuote(original);
}}
>
<div className=''>{renderValue()}</div>
</Button>
);
},
2024-08-23 16:47:51 +00:00
},
{
id: "status" as const,
accessorKey: "status",
header: () => <>{t("quotes.list.columns.status")}</>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => (
2024-11-13 16:28:58 +00:00
<QuoteStatusEditor
type='badge'
status={original.status}
2024-11-14 16:46:31 +00:00
onChangeStatus={(newStatus: string) => handleOnChangeStatus(original.id, newStatus)}
2024-11-13 16:28:58 +00:00
/>
2024-08-28 18:38:20 +00:00
),
2024-08-23 16:47:51 +00:00
},
2024-10-25 09:49:10 +00:00
{
id: "date_sent" as const,
accessor: "date_sent",
header: () => (
<div className='text-left text-ellipsis'>{t("quotes.list.columns.date_sent")}</div>
),
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
const quoteDate = UTCDateValue.create(original.date_sent);
2024-11-14 16:46:31 +00:00
const isSuccess = quoteDate.isSuccess && !quoteDate.object.isEmpty();
2024-10-25 09:49:10 +00:00
return (
<div className='text-left text-ellipsis'>
2024-11-14 16:46:31 +00:00
{isSuccess ? (
<ColorBadge
label={quoteDate.object.toLocaleDateString("es-ES")}
className='text-green-800 bg-green-100 hover:text-green-800 hover:bg-green-100'
/>
2024-10-25 09:49:10 +00:00
) : (
2024-11-14 16:46:31 +00:00
<></>
2024-10-25 09:49:10 +00:00
)}
</div>
);
},
},
2024-06-29 19:39:25 +00:00
{
2024-07-10 09:53:13 +00:00
id: "date" as const,
accessor: "date",
2024-08-23 16:47:51 +00:00
header: () => (
2024-10-25 09:49:10 +00:00
<div className='text-left text-ellipsis'>{t("quotes.list.columns.date")}</div>
2024-08-23 16:47:51 +00:00
),
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
2024-07-10 09:53:13 +00:00
const quoteDate = UTCDateValue.create(original.date);
2024-08-20 11:44:04 +00:00
return (
2024-10-25 09:49:10 +00:00
<div className='text-left text-ellipsis'>
2024-08-20 11:44:04 +00:00
{quoteDate.isSuccess ? quoteDate.object.toLocaleDateString("es-ES") : "-"}
</div>
);
2024-07-10 09:53:13 +00:00
},
2024-06-29 19:39:25 +00:00
},
2024-08-23 16:47:51 +00:00
{
id: "customer_reference" as const,
accessorKey: "customer_reference",
header: () => <>{t("quotes.list.columns.customer_reference")}</>,
cell: ({ renderValue }: { renderValue: () => any }) => (
<div className='text-left text-ellipsis'>{renderValue()}</div>
),
2024-08-23 16:47:51 +00:00
},
2024-06-29 19:39:25 +00:00
{
2024-07-10 09:53:13 +00:00
id: "customer_information" as const,
accessorKey: "customer_information",
header: () => <>{t("quotes.list.columns.customer_information")}</>,
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => (
2024-08-20 11:44:04 +00:00
<div className='text-left text-ellipsis'>
2024-07-10 10:41:00 +00:00
{original.customer_information.split("\n").map((item, index) => {
return (
<span
key={index}
className={
index === 0 ? "font-medium" : "hidden text-sm text-muted-foreground md:inline"
}
>
2024-07-10 10:41:00 +00:00
{item}
<br />
</span>
);
})}
</div>
2024-07-10 09:53:13 +00:00
),
2024-11-14 16:46:31 +00:00
size: 500,
2024-06-29 19:39:25 +00:00
},
2024-10-25 09:49:10 +00:00
2024-08-26 18:29:41 +00:00
/*{
2024-08-27 18:58:35 +00:00
id: "total_price" as const,
accessor: "total_price",
header: () => <div className='text-right'>{t("quotes.list.columns.total_price")}</div>,
cell: ({ row: { original } }) => {
const price = MoneyValue.create(original.total_price);
return (
<div className='text-right'>{price.isSuccess ? price.object.toFormat() : "-"}</div>
);
},
},*/
];
const actionColumn = [
2024-07-10 10:41:00 +00:00
{
2024-08-20 11:44:04 +00:00
id: "row-actions",
2024-07-10 10:41:00 +00:00
header: () => null,
2024-10-25 09:49:10 +00:00
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
const allowToSent = original?.status === "accepted" && !original?.date_sent;
2024-11-13 16:28:58 +00:00
const isSent = original?.status === "accepted" && !!original?.date_sent;
2024-11-14 16:46:31 +00:00
const isArchived = original?.status === "archived";
2024-10-25 09:49:10 +00:00
return (
2024-11-14 16:46:31 +00:00
<div className='flex gap-1'>
<ButtonGroup className='gap-0'>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
disabled={isSent}
onClick={(e) => {
e.preventDefault();
handleEditQuote(original);
}}
>
<EditIcon className='w-4 h-4' />
<span className='sr-only'>{t("common.edit")}</span>
2024-11-14 16:46:31 +00:00
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.edit")}</p>
2024-11-14 16:46:31 +00:00
</TooltipContent>
</Tooltip>
2024-10-25 09:49:10 +00:00
2024-11-14 16:46:31 +00:00
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
onClick={(e) => {
e.preventDefault();
handleDuplicateQuote(original.id);
}}
>
<CopyIcon className='w-4 h-4' />
<span className='sr-only'>{t("common.duplicate")}</span>
2024-11-14 16:46:31 +00:00
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.duplicate")}</p>
2024-11-14 16:46:31 +00:00
</TooltipContent>
</Tooltip>
2024-11-13 16:28:58 +00:00
2024-11-14 16:46:31 +00:00
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
onClick={() => {
download(original.id, getQuotePDFFilename(original));
}}
>
<DownloadIcon className='w-4 h-4' />
<span className='sr-only'>{t("common.download")}</span>
2024-11-14 16:46:31 +00:00
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.download")}</p>
2024-11-14 16:46:31 +00:00
</TooltipContent>
</Tooltip>
2024-11-13 16:28:58 +00:00
2024-11-14 16:46:31 +00:00
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
disabled={isArchived || isSent}
onClick={(e) => {
e.preventDefault();
handleArchiveQuote(original.id);
}}
>
<ArchiveIcon className='w-4 h-4' />
<span className='sr-only'>{t("common.archive")}</span>
2024-11-14 16:46:31 +00:00
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.archive")}</p>
2024-11-14 16:46:31 +00:00
</TooltipContent>
</Tooltip>
</ButtonGroup>
<ButtonGroup>
<QuoteSentToEditor
id={original.id}
onSentTo={() => null}
disabled={isSent || !allowToSent}
/>
</ButtonGroup>
</div>
2024-10-25 09:49:10 +00:00
);
},
2024-06-29 19:39:25 +00:00
},
2024-08-27 18:58:35 +00:00
];
return preview ? columns : [...columns, ...actionColumn];
}, [preview]);
2024-06-29 19:39:25 +00:00
const { table } = useDataTable({
data: data?.items ?? [],
columns: columns,
pageCount: data?.total_pages ?? -1,
});
2024-08-20 15:16:41 +00:00
useEffect(() => {
if (table && data && data?.total_pages > 0) {
setActiveRow(table.getRowModel().rows[0]);
}
}, [data, table]);
2024-06-29 19:39:25 +00:00
if (isError) {
return <ErrorOverlay subtitle={(error as Error).message} />;
}
if (isPending) {
return (
2024-08-18 20:39:06 +00:00
<div className='grid items-start flex-1 gap-4 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
<Card className='grid items-start gap-4 auto-rows-max md:gap-8 lg:col-span-2'>
<CardContent>
<DataTableSkeleton
columnCount={6}
searchableColumnCount={1}
filterableColumnCount={2}
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
shrinkZero
/>
</CardContent>
</Card>
<div></div>
</div>
2024-06-29 19:39:25 +00:00
);
}
if (data?.total_items === 0 && !isFiltered) {
return (
2024-08-26 15:53:58 +00:00
<Card>
<CardContent>
<SimpleEmptyState
subtitle={t("quotes.list.empty_state.subtitle")}
2024-08-26 15:53:58 +00:00
buttonText={t("quotes.create.title")}
onButtonClick={() => navigate("add", { relative: "path" })}
/>
</CardContent>
</Card>
2024-06-29 19:39:25 +00:00
);
}
return (
2024-08-20 21:40:37 +00:00
<>
2024-08-28 10:19:33 +00:00
<div className='flex flex-col items-stretch flex-1 gap-4 xl:flex-row'>
<div id={tableId} className='flex items-stretch flex-1'>
2024-08-20 21:40:37 +00:00
<DataTable
table={table}
paginationOptions={{ visible: true }}
className='grid items-start flex-1 gap-4 auto-rows-max md:gap-8 lg:col-span-2'
onRowClick={handleOnRowClick}
activeRowIndex={activeRow?.index}
>
<DataTableToolbar table={table} />
</DataTable>
2024-08-28 10:19:33 +00:00
</div>
2024-08-20 21:40:37 +00:00
{preview && (
2024-08-28 18:38:20 +00:00
<div id={previewId} className='flex items-stretch '>
2024-08-26 18:29:41 +00:00
<QuoteResume quoteId={activeRow?.original.id} />
2024-08-28 10:19:33 +00:00
</div>
2024-08-20 21:40:37 +00:00
)}
2024-08-28 10:19:33 +00:00
</div>
2024-08-21 09:30:47 +00:00
<DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} />
2024-08-20 21:40:37 +00:00
</>
2024-06-29 19:39:25 +00:00
);
};