Presupuestador_web/client/src/app/quotes/components/QuotesDataTable.tsx
David Arranz c77a3a3af5 feat: internationalize button labels and UI components
- Updated CancelButton and SubmitButton to use i18next for localization.
- Modified DataTableColumnHeader and DataTableFacetedFilter to include translations for sorting and filtering messages.
- Enhanced DataTableSelectionColumn with localized aria-labels for accessibility.
- Localized titles and messages in SimpleEmptyState and ErrorOverlay components.
- Updated LayoutHeader to use i18next for navigation toggle label.
- Integrated i18next into useLogout for error messages.
- Localized alert dialog actions in useRemoveAlertDialog.
- Added new translations in en.json and es.json for various UI components.
- Updated breadcrumb and carousel components to use localized strings.
- Enhanced pagination component with i18next for navigation labels.
- Localized sheet component close button text.
2026-10-05 13:58:03 +02:00

435 lines
14 KiB
TypeScript

import {
ButtonGroup,
ColorBadge,
DataTable,
DataTableSkeleton,
ErrorOverlay,
SimpleEmptyState,
} from "@/components";
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
import { useDataTable, useDataTableContext } from "@/lib/hooks";
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 { 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 { QuoteSentToEditor, QuoteStatusEditor } from "./editors";
export const QuotesDataTable = ({
status = "all",
preview = false,
}: {
status?: string;
preview?: boolean;
}) => {
const navigate = useNavigate();
const { toast } = useToast();
const tableId = useId();
const previewId = useId();
const { pagination, globalFilter, isFiltered } = useDataTableContext();
const [activeRow, setActiveRow] = useState<Row<IListQuotes_Response_DTO> | undefined>(undefined);
const { useList, useDownloader, useSetStatus, useDuplicate, getQuotePDFFilename } = useQuotes();
const { mutate: setStatusMutation } = useSetStatus();
const { mutate: duplicate } = useDuplicate();
const { data, isPending, isError, error } = useList({
pagination: {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
},
status,
quickSearchTerm: globalFilter,
});
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" });
},
}
);
};
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) => {
setStatusMutation(
{ id, newStatus },
{
onSuccess: () => {
toast({
description: t("quotes.quote_status_editor.toast_status_changed", {
newStatus: t(`quotes.status.${newStatus}`),
}),
variant: "success",
});
},
}
);
};
const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, unknown>[]>(() => {
const columns = [
{
id: "reference" as const,
accessorKey: "reference",
header: () => <>{t("quotes.list.columns.reference")}</>,
cell: ({
row: { original },
renderValue,
}: {
row: { original: IListQuotes_Response_DTO };
renderValue: () => any;
}) => {
//const allowToSent = original?.status === "accepted" && !original?.date_sent;
const isSent = Boolean(original?.status === "accepted" && original?.date_sent);
return (
<Button
size='sm'
variant='link'
disabled={isSent}
className='h-8 gap-1 px-0 text-left text-ellipsis'
onClick={(e) => {
e.preventDefault();
handleEditQuote(original);
}}
>
<div className=''>{renderValue()}</div>
</Button>
);
},
},
{
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 } }) => (
<QuoteStatusEditor
type='badge'
status={original.status}
onChangeStatus={(newStatus: string) => handleOnChangeStatus(original.id, newStatus)}
/>
),
},
{
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);
const isSuccess = quoteDate.isSuccess && !quoteDate.object.isEmpty();
return (
<div className='text-left text-ellipsis'>
{isSuccess ? (
<ColorBadge
label={quoteDate.object.toLocaleDateString("es-ES")}
className='text-green-800 bg-green-100 hover:text-green-800 hover:bg-green-100'
/>
) : (
<></>
)}
</div>
);
},
},
{
id: "date" as const,
accessor: "date",
header: () => (
<div className='text-left text-ellipsis'>{t("quotes.list.columns.date")}</div>
),
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
const quoteDate = UTCDateValue.create(original.date);
return (
<div className='text-left text-ellipsis'>
{quoteDate.isSuccess ? quoteDate.object.toLocaleDateString("es-ES") : "-"}
</div>
);
},
},
{
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>
),
},
{
id: "customer_information" as const,
accessorKey: "customer_information",
header: () => <>{t("quotes.list.columns.customer_information")}</>,
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => (
<div className='text-left text-ellipsis'>
{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"
}
>
{item}
<br />
</span>
);
})}
</div>
),
size: 500,
},
/*{
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 = [
{
id: "row-actions",
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 isArchived = original?.status === "archived";
return (
<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>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.edit")}</p>
</TooltipContent>
</Tooltip>
<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>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.duplicate")}</p>
</TooltipContent>
</Tooltip>
<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>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.download")}</p>
</TooltipContent>
</Tooltip>
<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>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.archive")}</p>
</TooltipContent>
</Tooltip>
</ButtonGroup>
<ButtonGroup>
<QuoteSentToEditor
id={original.id}
onSentTo={() => null}
disabled={isSent || !allowToSent}
/>
</ButtonGroup>
</div>
);
},
},
];
return preview ? columns : [...columns, ...actionColumn];
}, [preview]);
const { table } = useDataTable({
data: data?.items ?? [],
columns: columns,
pageCount: data?.total_pages ?? -1,
});
useEffect(() => {
if (table && data && data?.total_pages > 0) {
setActiveRow(table.getRowModel().rows[0]);
}
}, [data, table]);
if (isError) {
return <ErrorOverlay subtitle={(error as Error).message} />;
}
if (isPending) {
return (
<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>
);
}
if (data?.total_items === 0 && !isFiltered) {
return (
<Card>
<CardContent>
<SimpleEmptyState
subtitle={t("quotes.list.empty_state.subtitle")}
buttonText={t("quotes.create.title")}
onButtonClick={() => navigate("add", { relative: "path" })}
/>
</CardContent>
</Card>
);
}
return (
<>
<div className='flex flex-col items-stretch flex-1 gap-4 xl:flex-row'>
<div id={tableId} className='flex items-stretch flex-1'>
<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>
</div>
{preview && (
<div id={previewId} className='flex items-stretch '>
<QuoteResume quoteId={activeRow?.original.id} />
</div>
)}
</div>
<DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} />
</>
);
};