Arreglos de errores antes de subir a producción
This commit is contained in:
parent
b1cf2fdd18
commit
9300868ce3
@ -49,6 +49,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.2.0",
|
||||
"i18next": "^23.12.2",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"joi": "^17.13.1",
|
||||
|
||||
@ -77,13 +77,7 @@ export const Routes = () => {
|
||||
},
|
||||
{
|
||||
path: "/quotes",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<QuotesLayout>
|
||||
<Outlet />
|
||||
</QuotesLayout>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
element: <QuotesLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
|
||||
@ -1,31 +1,6 @@
|
||||
import { Button } from "@/ui";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
// import Button
|
||||
|
||||
/*const isDev = process.env.REACT_APP_NODE_ENV === "development";
|
||||
const hostname = `${
|
||||
isDev ? process.env.REACT_APP_DEV_API_URL : process.env.REACT_APP_PROD_API_URL
|
||||
}`;*/
|
||||
|
||||
type ErrorPageProps = {
|
||||
error?: string;
|
||||
};
|
||||
export const ErrorPage = (props: ErrorPageProps) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const navMsg = location.state?.error;
|
||||
const msg = navMsg ? (
|
||||
<p className='text-lg'>{navMsg}</p>
|
||||
) : props.error ? (
|
||||
<p className='text-lg'>{props.error}</p>
|
||||
) : (
|
||||
<p className='text-lg'>
|
||||
The targeted page <b>"{location.pathname}"</b> was not found, please confirm the spelling and
|
||||
try again.
|
||||
</p>
|
||||
);
|
||||
|
||||
export const ErrorPage = () => {
|
||||
return (
|
||||
<div className='flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8'>
|
||||
<div className='max-w-md mx-auto text-center'>
|
||||
@ -37,37 +12,11 @@ export const ErrorPage = (props: ErrorPageProps) => {
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<div className='mt-6'>
|
||||
<Button
|
||||
className='inline-flex items-center px-4 py-2 text-sm font-medium transition-colors rounded-md shadow-sm bg-primary text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2'
|
||||
prefetch={false}
|
||||
>
|
||||
<Button className='inline-flex items-center px-4 py-2 text-sm font-medium transition-colors rounded-md shadow-sm bg-primary text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2'>
|
||||
Go to Homepage
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section id='Error' className='flex flex-col items-center w-full h-full'>
|
||||
{msg}
|
||||
<span className='flex flex-row items-center justify-center my-10 space-x-8 '>
|
||||
<Button id='backButton' onClick={() => navigate(-1)}>
|
||||
Return to Previous Page
|
||||
</Button>
|
||||
<Button id='homeButton' onClick={() => navigate("/")}>
|
||||
Return to Home Page
|
||||
</Button>
|
||||
<Button
|
||||
id='logout'
|
||||
onClick={() => {
|
||||
//const endpoint = `${hostname}/logout`;
|
||||
//window.open(endpoint, "_blank");
|
||||
}}
|
||||
>
|
||||
Reset Authentication
|
||||
</Button>
|
||||
</span>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import { Badge } from "@/ui";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useCatalogTableColumns = (
|
||||
actions: DataTablaRowActionFunction<IListArticles_Response_DTO, unknown>
|
||||
actions: DataTablaRowActionFunction<IListArticles_Response_DTO>
|
||||
): ColumnDef<IListArticles_Response_DTO>[] => {
|
||||
const customerColumns: ColumnDef<IListArticles_Response_DTO>[] = useMemo(
|
||||
() => [
|
||||
@ -66,8 +66,8 @@ export const useCatalogTableColumns = (
|
||||
{
|
||||
id: "actions",
|
||||
header: "Acciones",
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} actions={actions} />;
|
||||
cell: (context) => {
|
||||
return <DataTableRowActions rowContext={context} actions={actions} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -2,10 +2,8 @@ import { Layout, LayoutContent, LayoutHeader } from "@/components";
|
||||
import { useGetProfile } from "@/lib/hooks";
|
||||
import { Skeleton } from "@/ui";
|
||||
import { t } from "i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const DashboardPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data: userIdentity, status } = useGetProfile();
|
||||
|
||||
return (
|
||||
|
||||
@ -66,8 +66,8 @@ export const useCustomerInvoiceDataTableColumns = (
|
||||
{
|
||||
id: "actions",
|
||||
header: "Acciones",
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} actions={actions} />;
|
||||
cell: (context) => {
|
||||
return <DataTableRowActions rowContext={context} actions={actions} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
export * from "./ErrorPage";
|
||||
export * from "./StartPage";
|
||||
export * from "./auth";
|
||||
export * from "./catalog";
|
||||
export * from "./dashboard";
|
||||
export * from "./dealers";
|
||||
export * from "./ErrorPage";
|
||||
export * from "./quotes";
|
||||
export * from "./settings";
|
||||
export * from "./StartPage";
|
||||
|
||||
@ -11,7 +11,7 @@ export interface AppendCatalogArticleRowButtonProps extends ButtonProps {
|
||||
export const AppendCatalogArticleRowButton = forwardRef(
|
||||
(
|
||||
{ label = t("common.append_article"), className, ...props }: AppendCatalogArticleRowButtonProps,
|
||||
ref
|
||||
_ref
|
||||
): JSX.Element => (
|
||||
<Button type='button' variant='outline' {...props}>
|
||||
{" "}
|
||||
|
||||
@ -11,7 +11,7 @@ export interface AppendEmptyRowButtonProps extends ButtonProps {
|
||||
export const AppendEmptyRowButton = forwardRef(
|
||||
(
|
||||
{ label = t("common.append_empty_row"), className, ...props }: AppendEmptyRowButtonProps,
|
||||
ref
|
||||
_ref
|
||||
): JSX.Element => (
|
||||
<Button type='button' variant='outline' {...props}>
|
||||
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||
|
||||
@ -45,7 +45,7 @@ declare module "@tanstack/react-table" {
|
||||
interface TableMeta<TData extends RowData> {
|
||||
insertItem: (rowIndex: number, data?: unknown) => void;
|
||||
appendItem: (data?: unknown) => void;
|
||||
pickCatalogArticle: () => void;
|
||||
pickCatalogArticle?: () => void;
|
||||
duplicateItems: (rowIndex?: number) => void;
|
||||
deleteItems: (rowIndex?: number | number[]) => void;
|
||||
updateItem: (
|
||||
@ -70,7 +70,7 @@ export type QuoteItemsSortableDataTableProps<
|
||||
defaultValues: Readonly<{ [x: string]: any }> | undefined;
|
||||
initialState?: InitialTableState;
|
||||
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields"> & {
|
||||
pickCatalogArticle: () => void;
|
||||
pickCatalogArticle?: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
@ -113,8 +113,7 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||
const [columnVisibility, _setColumnVisibility] = useState<VisibilityState>(
|
||||
initialState?.columnVisibility || {}
|
||||
);
|
||||
|
||||
@ -160,7 +159,9 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
|
||||
actions.append(data || defaultValues?.items[0], { shouldFocus: true });
|
||||
},
|
||||
pickCatalogArticle: () => {
|
||||
actions.pickCatalogArticle();
|
||||
if (actions.pickCatalogArticle) {
|
||||
actions?.pickCatalogArticle();
|
||||
}
|
||||
},
|
||||
duplicateItems: (rowIndex?: number) => {
|
||||
if (rowIndex != undefined) {
|
||||
@ -460,7 +461,11 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
|
||||
<ButtonGroup>
|
||||
<AppendEmptyRowButton onClick={() => table.options.meta?.appendItem()} />
|
||||
<AppendCatalogArticleRowButton
|
||||
onClick={() => table.options.meta?.pickCatalogArticle()}
|
||||
onClick={() => {
|
||||
if (table.options.meta && table.options.meta.pickCatalogArticle) {
|
||||
table.options.meta?.pickCatalogArticle();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</CardFooter>
|
||||
|
||||
@ -80,7 +80,11 @@ export const QuoteItemsSortableDataTableToolbar = ({ table }: { table: Table<any
|
||||
<TooltipTrigger asChild>
|
||||
<AppendCatalogArticleRowButton
|
||||
variant='ghost'
|
||||
onClick={() => table.options.meta?.pickCatalogArticle()}
|
||||
onClick={() => {
|
||||
if (table.options.meta && table.options.meta.pickCatalogArticle) {
|
||||
table.options.meta?.pickCatalogArticle();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.append_article_tooltip")}</TooltipContent>
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { PDFViewer } from "@/components";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Card, CardContent, CardHeader, Skeleton } from "@/ui";
|
||||
import { useToast } from "@/ui/use-toast";
|
||||
import { IListQuotes_Response_DTO } from "@shared/contexts";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuotes } from "../hooks";
|
||||
|
||||
const QuotePDFPreview = ({
|
||||
@ -15,18 +12,18 @@ const QuotePDFPreview = ({
|
||||
quote?: IListQuotes_Response_DTO;
|
||||
className?: string;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
//const navigate = useNavigate();
|
||||
//const { toast } = useToast();
|
||||
const { useReport, getQuotePDFFilename, useDownloader } = useQuotes();
|
||||
const { download, ...downloadProps } = useDownloader();
|
||||
const { download } = useDownloader();
|
||||
const { report, preview, isInProgress } = useReport();
|
||||
const [URLReport, setURLReport] = useState<string | undefined>(undefined);
|
||||
|
||||
const handleFinishDownload = useCallback(() => {
|
||||
/*const handleFinishDownload = useCallback(() => {
|
||||
toast({
|
||||
description: t("quotes.downloading_dialog.toast_success"),
|
||||
});
|
||||
}, [toast]);
|
||||
}, [toast]);*/
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
if (quote) download(quote.id, getQuotePDFFilename(quote));
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
@ -21,7 +20,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/ui";
|
||||
import { useToast } from "@/ui/use-toast";
|
||||
import { CurrencyData } from "@shared/contexts";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -40,7 +38,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { useOne, useSetStatus, useDownloader, getQuotePDFFilename } = useQuotes();
|
||||
const { data, status, error: queryError } = useOne(quoteId);
|
||||
const { data, status } = useOne(quoteId);
|
||||
const { mutate: setStatusMutation } = useSetStatus(quoteId);
|
||||
const { download, ...downloadProps } = useDownloader();
|
||||
|
||||
@ -48,12 +46,12 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||
locale: data?.lang_code || "ES",
|
||||
});
|
||||
|
||||
const currency_symbol = useMemo(() => {
|
||||
/*const currency_symbol = useMemo(() => {
|
||||
const currencyOrError = data
|
||||
? CurrencyData.createFromCode(data?.currency_code)
|
||||
: CurrencyData.createDefaultCode();
|
||||
return currencyOrError.isSuccess ? currencyOrError.object.symbol : "";
|
||||
}, [data]);
|
||||
}, [data]);*/
|
||||
|
||||
const totals = useMemo(() => {
|
||||
return data
|
||||
@ -129,7 +127,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||
<span>{t("quotes.list.resume.title")}</span>
|
||||
<ColorBadge className='text-sm' label={t(`quotes.status.${data.status}`)} />
|
||||
</CardTitle>
|
||||
<CardDescription className='flex mr-auto text-foreground'>
|
||||
<div className='flex mr-auto text-foreground'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Button
|
||||
size='sm'
|
||||
@ -182,7 +180,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>*/}
|
||||
</div>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className='p-6 text-sm'>
|
||||
<TabsList className='grid w-full grid-cols-2'>
|
||||
|
||||
@ -39,7 +39,6 @@ export const QuotesDataTable = ({
|
||||
status?: string;
|
||||
preview?: boolean;
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -63,20 +62,44 @@ export const QuotesDataTable = ({
|
||||
|
||||
const { download, ...downloadProps } = useDownloader();
|
||||
|
||||
const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, any>[]>(() => {
|
||||
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) => {
|
||||
toast({ title: "Guardo => " + quote.id });
|
||||
navigate(`/quotes/edit/${quote.id}`, { relative: "path" });
|
||||
},
|
||||
[navigate, toast]
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
cell: ({
|
||||
row: { original },
|
||||
renderValue,
|
||||
}: {
|
||||
row: { original: IListQuotes_Response_DTO };
|
||||
renderValue: () => any;
|
||||
}) => (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='link'
|
||||
className='h-8 gap-1 px-0 text-left text-ellipsis'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(`/quotes/edit/${original.id}`, { relative: "path" });
|
||||
handleEditQuote(original);
|
||||
}}
|
||||
>
|
||||
<div className=''>{renderValue()}</div>
|
||||
@ -89,7 +112,7 @@ export const QuotesDataTable = ({
|
||||
accessorKey: "status",
|
||||
header: () => <>{t("quotes.list.columns.status")}</>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cell: ({ row: { original } }) => (
|
||||
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => (
|
||||
<ColorBadge label={t(`quotes.status.${original.status}`)} />
|
||||
),
|
||||
},
|
||||
@ -99,7 +122,7 @@ export const QuotesDataTable = ({
|
||||
header: () => (
|
||||
<div className='text-right text-ellipsis'>{t("quotes.list.columns.date")}</div>
|
||||
),
|
||||
cell: ({ row: { original } }) => {
|
||||
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => {
|
||||
const quoteDate = UTCDateValue.create(original.date);
|
||||
return (
|
||||
<div className='text-right text-ellipsis'>
|
||||
@ -112,13 +135,15 @@ export const QuotesDataTable = ({
|
||||
id: "customer_reference" as const,
|
||||
accessorKey: "customer_reference",
|
||||
header: () => <>{t("quotes.list.columns.customer_reference")}</>,
|
||||
cell: ({ renderValue }) => <div className='text-left text-ellipsis'>{renderValue()}</div>,
|
||||
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 } }) => (
|
||||
cell: ({ row: { original } }: { row: { original: IListQuotes_Response_DTO } }) => (
|
||||
<div className='text-left text-ellipsis'>
|
||||
{original.customer_information.split("\n").map((item, index) => {
|
||||
return (
|
||||
@ -164,7 +189,7 @@ export const QuotesDataTable = ({
|
||||
className='h-8 gap-1'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(`/quotes/edit/${row.original.id}`, { relative: "path" });
|
||||
handleEditQuote(row.original);
|
||||
}}
|
||||
>
|
||||
<FilePenLineIcon className='h-3.5 w-3.5' />
|
||||
@ -205,33 +230,18 @@ export const QuotesDataTable = ({
|
||||
return preview ? columns : [...columns, ...actionColumn];
|
||||
}, [preview]);
|
||||
|
||||
const handleOnPaginationChange = () => {
|
||||
//setActiveRow(undefined);
|
||||
};
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: data?.items ?? [],
|
||||
columns: columns,
|
||||
pageCount: data?.total_pages ?? -1,
|
||||
onPaginationChange: handleOnPaginationChange,
|
||||
});
|
||||
|
||||
const handleOnRowClick = (row: Row<IListQuotes_Response_DTO>) => {
|
||||
setActiveRow(row);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (table && data && data?.total_pages > 0) {
|
||||
setActiveRow(table.getRowModel().rows[0]);
|
||||
}
|
||||
}, [data, table]);
|
||||
|
||||
const handleFinishDownload = useCallback(() => {
|
||||
toast({
|
||||
description: t("quotes.downloading_dialog.toast_success"),
|
||||
});
|
||||
}, [toast]);
|
||||
|
||||
if (isError) {
|
||||
return <ErrorOverlay subtitle={(error as Error).message} />;
|
||||
}
|
||||
@ -287,7 +297,6 @@ export const QuotesDataTable = ({
|
||||
{preview && (
|
||||
<div id={previewId} className='flex items-stretch '>
|
||||
<QuoteResume quoteId={activeRow?.original.id} />
|
||||
{/*<QuotePDFPreview quote={activeRow?.original} className='flex-1' />*/}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,278 +0,0 @@
|
||||
import {
|
||||
FormCurrencyField,
|
||||
FormPercentageField,
|
||||
FormQuantityField,
|
||||
FormTextAreaField,
|
||||
} from "@/components";
|
||||
|
||||
import { DataTableProvider } from "@/lib/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui";
|
||||
import { useToast } from "@/ui/use-toast";
|
||||
import { CurrencyData, Language, Quantity } from "@shared/contexts";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useDetailColumns } from "../../hooks";
|
||||
import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
|
||||
import { QuoteItemsSortableDataTable } from "../QuoteItemsSortableDataTable";
|
||||
import { CatalogPickerDialog } from "./CatalogPickerDialog";
|
||||
|
||||
export const QuoteDetailsCardEditor = ({
|
||||
currency,
|
||||
language,
|
||||
defaultValues,
|
||||
}: {
|
||||
currency: CurrencyData;
|
||||
language: Language;
|
||||
defaultValues: Readonly<{ [x: string]: any }> | undefined;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const { control, register } = useFormContext();
|
||||
|
||||
const [pickerMode] = useState<"dialog" | "panel">("dialog");
|
||||
|
||||
const [pickerDialogOpen, setPickerDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const { fields, ...fieldActions } = useFieldArray({
|
||||
control,
|
||||
name: "items",
|
||||
});
|
||||
|
||||
const columns: ColumnDef<unknown, unknown>[] = useDetailColumns(
|
||||
[
|
||||
/*{
|
||||
id: "row_id" as const,
|
||||
header: () => (
|
||||
<HashIcon aria-label='Orden de fila' className='items-center justify-center w-4 h-4' />
|
||||
),
|
||||
accessorFn: (_: unknown, index: number) => index + 1,
|
||||
size: 5,
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
},*/
|
||||
{
|
||||
id: "description" as const,
|
||||
accessorKey: "description",
|
||||
header: t("quotes.form_fields.items.description.label"),
|
||||
cell: ({ row: { index } }) => {
|
||||
return <FormTextAreaField autoSize {...register(`items.${index}.description`)} />;
|
||||
},
|
||||
size: 500,
|
||||
},
|
||||
{
|
||||
id: "quantity" as const,
|
||||
accessorKey: "quantity",
|
||||
header: () => (
|
||||
<div className='text-right'>{t("quotes.form_fields.items.quantity.label")}</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => {
|
||||
return (
|
||||
<FormQuantityField
|
||||
scale={0}
|
||||
className='text-right'
|
||||
{...register(`items.${index}.quantity`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "unit_price" as const,
|
||||
accessorKey: "unit_price",
|
||||
header: () => (
|
||||
<div className='text-right'>{t("quotes.form_fields.items.unit_price.label")}</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => {
|
||||
return (
|
||||
<FormCurrencyField
|
||||
currency={currency}
|
||||
language={language}
|
||||
scale={4}
|
||||
className='text-right'
|
||||
{...register(`items.${index}.unit_price`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "subtotal_price" as const,
|
||||
accessorKey: "subtotal_price",
|
||||
header: () => (
|
||||
<div className='text-right'>{t("quotes.form_fields.items.subtotal_price.label")}</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => {
|
||||
return (
|
||||
<FormCurrencyField
|
||||
currency={currency}
|
||||
language={language}
|
||||
scale={4}
|
||||
readOnly
|
||||
className='text-right'
|
||||
{...register(`items.${index}.subtotal_price`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "discount" as const,
|
||||
accessorKey: "discount",
|
||||
header: () => (
|
||||
<div className='text-right'>{t("quotes.form_fields.items.discount.label")}</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => {
|
||||
return (
|
||||
<FormPercentageField
|
||||
scale={2}
|
||||
className='text-right'
|
||||
{...register(`items.${index}.discount`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "total_price" as const,
|
||||
accessorKey: "total_price",
|
||||
header: () => (
|
||||
<div className='text-right'>{t("quotes.form_fields.items.total_price.label")}</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => {
|
||||
return (
|
||||
<FormCurrencyField
|
||||
variant='ghost'
|
||||
currency={currency}
|
||||
language={language}
|
||||
scale={4}
|
||||
readOnly
|
||||
className='font-semibold text-right'
|
||||
{...register(`items.${index}.total_price`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
enableDragHandleColumn: false, // <--- Desactivado temporalmente
|
||||
enableSelectionColumn: true,
|
||||
enableActionsColumn: true,
|
||||
rowActionFn: (props) => {
|
||||
const { table, row } = props;
|
||||
return [
|
||||
{
|
||||
label: t("common.duplicate_row"),
|
||||
icon: <CopyIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.duplicateItems(row.index),
|
||||
},
|
||||
{
|
||||
label: t("common.insert_row_above"),
|
||||
icon: <ChevronUpIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.insertItem(row.index),
|
||||
},
|
||||
{
|
||||
label: t("common.insert_row_below"),
|
||||
icon: <ChevronDownIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.insertItem(row.index + 1),
|
||||
},
|
||||
|
||||
{
|
||||
label: "-",
|
||||
},
|
||||
{
|
||||
label: t("common.remove_row"),
|
||||
//shortcut: "⌘⌫",
|
||||
icon: <Trash2Icon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => {
|
||||
table.options.meta?.deleteItems(row.index);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const handleAppendCatalogArticle = useCallback(
|
||||
(article: any) => {
|
||||
fieldActions.append({
|
||||
...article,
|
||||
quantity: {
|
||||
amount: 100,
|
||||
scale: Quantity.DEFAULT_SCALE,
|
||||
},
|
||||
unit_price: article.retail_price,
|
||||
discount: {
|
||||
amount: null,
|
||||
scale: 2,
|
||||
},
|
||||
});
|
||||
toast({
|
||||
title: t("quotes.catalog_picker_dialog.toast_article_added"),
|
||||
description: article.description,
|
||||
});
|
||||
},
|
||||
[fieldActions]
|
||||
);
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
const defaultLayout = [265, 440, 655];
|
||||
const navCollapsedSize = 4;
|
||||
|
||||
if (pickerMode === "dialog") {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<QuoteItemsSortableDataTable
|
||||
actions={{
|
||||
...fieldActions,
|
||||
pickCatalogArticle: () => setPickerDialogOpen(true),
|
||||
}}
|
||||
columns={columns}
|
||||
data={fields}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
<CatalogPickerDialog
|
||||
onSelect={handleAppendCatalogArticle}
|
||||
isOpen={pickerDialogOpen}
|
||||
onOpenChange={setPickerDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
direction='horizontal'
|
||||
autoSaveId='uecko.quotes.details_layout'
|
||||
className='items-stretch h-full'
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={defaultLayout[0]}
|
||||
collapsedSize={navCollapsedSize}
|
||||
collapsible={true}
|
||||
minSize={50}
|
||||
maxSize={90}
|
||||
onCollapse={() => {
|
||||
setIsCollapsed(true);
|
||||
}}
|
||||
onExpand={() => {
|
||||
setIsCollapsed(false);
|
||||
}}
|
||||
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
|
||||
>
|
||||
<QuoteItemsSortableDataTable
|
||||
actions={fieldActions}
|
||||
columns={columns}
|
||||
data={fields}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle className='mx-3' />
|
||||
<ResizablePanel defaultSize={defaultLayout[1]} minSize={10}>
|
||||
<DataTableProvider syncWithLocation={false}>
|
||||
<CatalogPickerDataTable onSelect={handleAppendCatalogArticle} />
|
||||
</DataTableProvider>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
};
|
||||
@ -17,7 +17,7 @@ import { useCallback, useState } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useDetailColumns } from "../../hooks";
|
||||
import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
|
||||
import { QuoteItemsSortableDataTable } from "../QuoteItemsSortableDataTable";
|
||||
import { QuoteItemsSortableDataTable, RowIdData } from "../QuoteItemsSortableDataTable";
|
||||
import { CatalogPickerDialog } from "./CatalogPickerDialog";
|
||||
|
||||
export const QuoteDetailsCardEditor = ({
|
||||
@ -41,7 +41,7 @@ export const QuoteDetailsCardEditor = ({
|
||||
name: "items",
|
||||
});
|
||||
|
||||
const columns: ColumnDef<unknown, unknown>[] = useDetailColumns(
|
||||
const columns: ColumnDef<RowIdData, unknown>[] = useDetailColumns(
|
||||
[
|
||||
/*{
|
||||
id: "row_id" as const,
|
||||
|
||||
@ -9,7 +9,7 @@ export const QuoteDocumentsCardEditor = () => {
|
||||
defaultUploadedFiles: [],
|
||||
});*/
|
||||
|
||||
const { control, register, formState } = useFormContext();
|
||||
const { control, formState } = useFormContext();
|
||||
|
||||
return (
|
||||
<div className='grid gap-6 md:grid-cols-6'>
|
||||
@ -21,7 +21,7 @@ export const QuoteDocumentsCardEditor = () => {
|
||||
<FormField
|
||||
control={control}
|
||||
name='images'
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<div className='space-y-6'>
|
||||
<FormItem className='w-full'>
|
||||
<FormLabel>Images</FormLabel>
|
||||
|
||||
@ -6,7 +6,7 @@ import { Badge } from "@/ui";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useQuoteDataTableColumns = (
|
||||
actions: DataTablaRowActionFunction<IListQuotes_Response_DTO, unknown>
|
||||
actions: DataTablaRowActionFunction<IListQuotes_Response_DTO>
|
||||
): ColumnDef<IListQuotes_Response_DTO>[] => {
|
||||
const customerColumns: ColumnDef<IListQuotes_Response_DTO>[] = useMemo(
|
||||
() => [
|
||||
@ -59,8 +59,8 @@ export const useQuoteDataTableColumns = (
|
||||
{
|
||||
id: "actions",
|
||||
header: "Acciones",
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} actions={actions} />;
|
||||
cell: (context) => {
|
||||
return <DataTableRowActions rowContext={context} actions={actions} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -159,8 +159,7 @@ export const QuoteEdit = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { unsubscribe } = watch((_, { name, type }) => {
|
||||
const { unsubscribe } = watch((_, { name }) => {
|
||||
const quote = getValues();
|
||||
|
||||
if (name) {
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
DataTableRowDragHandleCell,
|
||||
} from "@/components";
|
||||
import { Checkbox } from "@/ui";
|
||||
import { ColumnDef, Row, Table } from "@tanstack/react-table";
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
@ -58,8 +58,7 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
|
||||
/>
|
||||
),
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
cell: ({ row, table }: { row: Row<TData>; table: Table<TData> }) => (
|
||||
cell: ({ row }: { row: Row<TData> }) => (
|
||||
<Checkbox
|
||||
id={`select-row-${row.id}`}
|
||||
checked={row.getIsSelected()}
|
||||
|
||||
@ -1,8 +1,36 @@
|
||||
import { Layout, LayoutContent, LayoutHeader } from "@/components";
|
||||
import { Layout, LayoutContent, LayoutHeader, ProtectedRoute } from "@/components";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { QuotesProvider } from "./QuotesContext";
|
||||
|
||||
export const QuotesLayout = ({ children }: PropsWithChildren) => {
|
||||
const location = useLocation();
|
||||
|
||||
const isEditing = location.pathname.includes("/edit");
|
||||
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<QuotesProvider>
|
||||
<Layout>
|
||||
<LayoutHeader />
|
||||
<LayoutContent>
|
||||
{" "}
|
||||
<div className={`layout ${isEditing ? "editing" : ""}`}>
|
||||
<div className='quotes-list'>
|
||||
<Outlet />
|
||||
</div>
|
||||
{isEditing && (
|
||||
<div className='quotes-editor'>
|
||||
<Outlet />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LayoutContent>
|
||||
</Layout>
|
||||
</QuotesProvider>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
|
||||
return (
|
||||
<QuotesProvider>
|
||||
<Layout>
|
||||
|
||||
@ -26,8 +26,7 @@ export type DataTableRowActionDefinition<TData> = {
|
||||
onClick?: (props: CellContext<TData, unknown>, e: React.BaseSyntheticEvent) => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type DataTableRowActionsProps<TData, TValue = unknown> = {
|
||||
export type DataTableRowActionsProps<TData, _TValue = unknown> = {
|
||||
className?: string;
|
||||
actions?: DataTablaRowActionFunction<TData>;
|
||||
rowContext: CellContext<TData, unknown>;
|
||||
@ -36,7 +35,6 @@ export type DataTableRowActionsProps<TData, TValue = unknown> = {
|
||||
export function DataTableRowActions<TData = any, TValue = unknown>({
|
||||
actions,
|
||||
rowContext,
|
||||
className,
|
||||
}: DataTableRowActionsProps<TData, TValue>) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
||||
@ -16,7 +16,7 @@ export function getDataTableSelectionColumn<TData, TError>(): DataTableColumnPro
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
cell: ({ row, table }) => (
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
id={`select-row-${row.id}`}
|
||||
checked={row.getIsSelected()}
|
||||
|
||||
@ -81,7 +81,11 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
|
||||
const result = moneyOrError.object.toString();
|
||||
return inputValue.endsWith(",") ? result.replace(/.0$/, ",") : result;
|
||||
},
|
||||
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
|
||||
output: (
|
||||
_value: string | undefined,
|
||||
_name?: string,
|
||||
values?: CurrencyInputOnChangeValues
|
||||
) => {
|
||||
const { value: amount } = values ?? { value: null };
|
||||
|
||||
setInputValue(amount ?? "");
|
||||
@ -103,7 +107,7 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
|
||||
disabled={disabled}
|
||||
rules={rules}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
render={({ field, fieldState, formState }) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem ref={ref} className={cn(className, "space-y-3")}>
|
||||
{label && (
|
||||
|
||||
@ -57,8 +57,7 @@ export const FormDatePickerField = React.forwardRef<
|
||||
control={control}
|
||||
name={name}
|
||||
rules={{ required }}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
render={({ field, fieldState, formState }) => (
|
||||
render={({ field }) => (
|
||||
<FormItem ref={ref} className={cn(className, "flex flex-col")}>
|
||||
{label && <FormLabel label={label} hint={hint} required={required} />}
|
||||
|
||||
|
||||
@ -79,7 +79,7 @@ export const FormPercentageField = React.forwardRef<
|
||||
const result = percentageOrError.object.toString();
|
||||
return inputValue.endsWith(",") ? result.replace(/.0$/, ",") : result;
|
||||
},
|
||||
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
|
||||
output: (_value: string | undefined, _name?: string, values?: CurrencyInputOnChangeValues) => {
|
||||
const { value: amount } = values ?? { value: null };
|
||||
|
||||
setInputValue(amount ?? "");
|
||||
|
||||
@ -76,7 +76,11 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
|
||||
const result = quantityOrError.object.toString();
|
||||
return inputValue.endsWith(",") ? result.replace(/.0$/, ",") : result;
|
||||
},
|
||||
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
|
||||
output: (
|
||||
_value: string | undefined,
|
||||
_name?: string,
|
||||
values?: CurrencyInputOnChangeValues
|
||||
) => {
|
||||
const { value: amount } = values ?? { value: null };
|
||||
|
||||
setInputValue(amount ?? "");
|
||||
|
||||
@ -75,7 +75,7 @@ export const FormTextAreaField = React.forwardRef<
|
||||
name={name}
|
||||
rules={{ required }}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
render={({ field, fieldState, formState }) => (
|
||||
render={({ field, fieldState }) => (
|
||||
<FormItem ref={ref} className={cn(className, "flex flex-col space-y-3")}>
|
||||
{label && <FormLabel label={label} hint={hint} required={required} />}
|
||||
<FormControl className='grow'>
|
||||
|
||||
@ -67,10 +67,8 @@ export const PDFViewer = ({ file, onThumbnailClick, className }: PDFViewerProps)
|
||||
|
||||
const goToNextPage = useCallback(() => changePage(1), [changePage]);
|
||||
const goToPrevPage = useCallback(() => changePage(-1), [changePage]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const goToFirstPage = useCallback(() => setPageNumber(1), [setPageNumber]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const goToLastPage = useCallback(() => setPageNumber(numPages), [setPageNumber, numPages]);
|
||||
//const goToFirstPage = useCallback(() => setPageNumber(1), [setPageNumber]);
|
||||
//const goToLastPage = useCallback(() => setPageNumber(numPages), [setPageNumber, numPages]);
|
||||
|
||||
const isLoading = useMemo(
|
||||
() => renderedPageNumber !== pageNumber,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { RowIdData } from "@/app/quotes/components/QuoteItemsSortableDataTable";
|
||||
import { DataTableColumnHeader } from "@/components";
|
||||
import { Badge } from "@/ui";
|
||||
import {
|
||||
@ -47,11 +48,16 @@ import { SortableTableRow } from "./SortableTableRow";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface TableMeta<TData extends RowData> {
|
||||
insertItem: (rowIndex: number, data: TData) => void;
|
||||
appendItem: (data?: TData) => void;
|
||||
insertItem: (rowIndex: number, data?: unknown) => void;
|
||||
appendItem: (data?: unknown) => void;
|
||||
duplicateItems: (rowIndex?: number) => void;
|
||||
deleteItems: (rowIndex?: number | number[]) => void;
|
||||
updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void;
|
||||
updateItem: (
|
||||
rowIndex: number,
|
||||
rowData: TData & RowIdData,
|
||||
fieldName: string,
|
||||
value: unknown
|
||||
) => void;
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,8 +131,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [columnVisibility, _setColumnVisibility] = useState<VisibilityState>({});
|
||||
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
||||
|
||||
const table = useReactTable({
|
||||
@ -149,14 +154,14 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
||||
enableHiding: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (originalRow: unknown) => originalRow?.id,
|
||||
getRowId: (originalRow: any) => originalRow?.id,
|
||||
debugHeaders: true,
|
||||
debugColumns: true,
|
||||
meta: {
|
||||
insertItem: (rowIndex: number, data: object = {}) => {
|
||||
insertItem: (rowIndex: number, data: unknown = {}) => {
|
||||
actions.insert(rowIndex, data, { shouldFocus: true });
|
||||
},
|
||||
appendItem: (data: object = {}) => {
|
||||
appendItem: (data: unknown = {}) => {
|
||||
actions.append(data, { shouldFocus: true });
|
||||
},
|
||||
duplicateItems: (rowIndex?: number) => {
|
||||
@ -168,7 +173,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
||||
table.getSelectedRowModel().rows[table.getSelectedRowModel().rows.length - 1].index;
|
||||
const data = table
|
||||
.getSelectedRowModel()
|
||||
.rows.map((row) => ({ ...row.original, id: undefined }));
|
||||
.rows.map((row: any) => ({ ...row.original, id: undefined }));
|
||||
|
||||
if (table.getRowModel().rows.length < lastIndex + 1) {
|
||||
actions.append(data);
|
||||
@ -198,7 +203,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
||||
actions.remove();
|
||||
}
|
||||
},
|
||||
updateItem: (rowIndex: number, rowData: unknown, fieldName: string, value: unknown) => {
|
||||
updateItem: (rowIndex: number, rowData: any, fieldName: string, value: unknown) => {
|
||||
// Skip page index reset until after next rerender
|
||||
// skipAutoResetPageIndex();
|
||||
console.log({
|
||||
@ -324,9 +329,9 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
||||
items={filterItems(sorteableRowIds)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{filterItems(table.getRowModel().rows).map((row) => (
|
||||
{filterItems(table.getRowModel().rows).map((row: any) => (
|
||||
<SortableTableRow key={row.id} id={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
{row.getVisibleCells().map((cell: any) => (
|
||||
<TableCell
|
||||
className='p-1 align-top'
|
||||
key={cell.id}
|
||||
|
||||
@ -3,12 +3,7 @@ import { TableRow } from "@/ui/table";
|
||||
import { DraggableSyntheticListeners } from "@dnd-kit/core";
|
||||
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import {
|
||||
CSSProperties,
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react";
|
||||
import { SortableProps } from "./SortableDataTable";
|
||||
|
||||
interface Context {
|
||||
@ -22,7 +17,7 @@ export const SortableTableRowContext = createContext<Context>({
|
||||
ref() {},
|
||||
});
|
||||
|
||||
function animateLayoutChanges(args) {
|
||||
function animateLayoutChanges(args: any) {
|
||||
if (args.isSorting || args.wasDragging) {
|
||||
return defaultAnimateLayoutChanges(args);
|
||||
}
|
||||
@ -30,10 +25,7 @@ function animateLayoutChanges(args) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function SortableTableRow({
|
||||
id,
|
||||
children,
|
||||
}: PropsWithChildren<SortableProps>) {
|
||||
export function SortableTableRow({ id, children }: PropsWithChildren<SortableProps>) {
|
||||
const {
|
||||
attributes,
|
||||
isDragging,
|
||||
@ -58,7 +50,7 @@ export function SortableTableRow({
|
||||
listeners,
|
||||
ref: setActivatorNodeRef,
|
||||
}),
|
||||
[attributes, listeners, setActivatorNodeRef],
|
||||
[attributes, listeners, setActivatorNodeRef]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -66,10 +58,7 @@ export function SortableTableRow({
|
||||
<TableRow
|
||||
key={id}
|
||||
id={String(id)}
|
||||
className={cn(
|
||||
isDragging ? "opacity-40" : "opacity-100",
|
||||
"hover:bg-muted/30 m-0",
|
||||
)}
|
||||
className={cn(isDragging ? "opacity-40" : "opacity-100", "hover:bg-muted/30 m-0")}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
>
|
||||
|
||||
@ -19,7 +19,7 @@ const DEFAULT_REFETCH_INTERVAL = 2 * 60 * 1000; // 2 minutes
|
||||
const DEFAULT_STALE_TIME = 60 * 1000; // 1 minute
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type UseListQueryOptions<TUseListQueryData, TUseListQueryError> = {
|
||||
export type UseListQueryOptions<TUseListQueryData, _TUseListQueryError> = {
|
||||
queryKey: QueryKey;
|
||||
queryFn: (context: QueryFunctionContext) => Promise<TUseListQueryData>;
|
||||
enabled?: boolean;
|
||||
@ -51,8 +51,8 @@ export const useList = <TUseListQueryData, TUseListQueryError>({
|
||||
queryKey,
|
||||
queryFn,
|
||||
placeholderData: keepPreviousData,
|
||||
//staleTime: DEFAULT_STALE_TIME,
|
||||
//refetchInterval: DEFAULT_REFETCH_INTERVAL,
|
||||
staleTime: DEFAULT_STALE_TIME,
|
||||
refetchInterval: DEFAULT_REFETCH_INTERVAL,
|
||||
refetchOnWindowFocus: true,
|
||||
enabled: enabled && !!queryFn,
|
||||
select,
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import { QueryFunction, QueryKey, UseQueryResult, useQuery } from "@tanstack/react-query";
|
||||
|
||||
export interface IUseManyQueryOptions<
|
||||
TUseManyQueryData = unknown,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TUseManyQueryError = unknown
|
||||
> {
|
||||
export interface IUseManyQueryOptions<TUseManyQueryData = unknown, _TUseManyQueryError = unknown> {
|
||||
queryKey: QueryKey;
|
||||
queryFn: QueryFunction<TUseManyQueryData, QueryKey>;
|
||||
enabled?: boolean;
|
||||
|
||||
@ -2,8 +2,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
export interface IUseRemoveMutationOptions<
|
||||
TUseRemoveMutationData,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TUseRemoveMutationError,
|
||||
_TUseRemoveMutationError,
|
||||
TUseRemoveMutationVariables
|
||||
> {
|
||||
mutationFn: (variables: TUseRemoveMutationVariables) => Promise<TUseRemoveMutationData>;
|
||||
|
||||
@ -3,11 +3,11 @@ import { useGetProfile } from "../useAuth";
|
||||
export const usePreferredLanguage = () => {
|
||||
const { data } = useGetProfile();
|
||||
|
||||
if (!data || !data.language) {
|
||||
if (!data || !data.lang_code) {
|
||||
return navigator.languages && navigator.languages.length
|
||||
? navigator.languages[0]
|
||||
: navigator.language;
|
||||
}
|
||||
|
||||
return data.language;
|
||||
return data.lang_code;
|
||||
};
|
||||
|
||||
@ -35,7 +35,7 @@ type KeySegment = string | IdType | IdsType | ParamsType;
|
||||
|
||||
export function arrayFindIndex<T>(array: T[], slice: T[]): number {
|
||||
return array.findIndex(
|
||||
(item, index) =>
|
||||
(_, index) =>
|
||||
index <= array.length - slice.length &&
|
||||
slice.every((sliceItem, sliceIndex) => array[index + sliceIndex] === sliceItem)
|
||||
);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import secureLocalStorage from "react-secure-storage";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
@ -27,7 +28,7 @@ export function ThemeProvider({
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
() => (secureLocalStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -35,8 +36,7 @@ export function ThemeProvider({
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
@ -50,7 +50,7 @@ export function ThemeProvider({
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
secureLocalStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
@ -65,8 +65,7 @@ export function ThemeProvider({
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./useUploadFile";
|
||||
@ -1,57 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
import { type OurFileRouter } from "@/app/api/uploadthing/core";
|
||||
import { getErrorMessage } from "@/lib/handle-error";
|
||||
import { uploadFiles } from "@/lib/uploadthing";
|
||||
|
||||
export interface UploadedFile {}
|
||||
|
||||
interface UseUploadFileProps
|
||||
extends Pick<
|
||||
UploadFilesOptions<OurFileRouter, keyof OurFileRouter>,
|
||||
"headers" | "onUploadBegin" | "onUploadProgress" | "skipPolling"
|
||||
> {
|
||||
defaultUploadedFiles?: UploadedFile[];
|
||||
}
|
||||
|
||||
export function useUploadFile(
|
||||
endpoint: keyof OurFileRouter,
|
||||
{ defaultUploadedFiles = [], ...props }: UseUploadFileProps = {}
|
||||
) {
|
||||
const [uploadedFiles, setUploadedFiles] = React.useState<UploadedFile[]>(defaultUploadedFiles);
|
||||
const [progresses, setProgresses] = React.useState<Record<string, number>>({});
|
||||
const [isUploading, setIsUploading] = React.useState(false);
|
||||
|
||||
async function uploadThings(files: File[]) {
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const res = await uploadFiles(endpoint, {
|
||||
...props,
|
||||
files,
|
||||
onUploadProgress: ({ file, progress }) => {
|
||||
setProgresses((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[file]: progress,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
setUploadedFiles((prev) => (prev ? [...prev, ...res] : res));
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
} finally {
|
||||
setProgresses({});
|
||||
setIsUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uploadedFiles,
|
||||
progresses,
|
||||
uploadFiles: uploadThings,
|
||||
isUploading,
|
||||
};
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { Dot } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
InputOTP.displayName = "InputOTP"
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = "InputOTPGroup"
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
InputOTPSlot.displayName = "InputOTPSlot"
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
@ -51,7 +51,7 @@ export class RuleValidator {
|
||||
}
|
||||
|
||||
public static validateFnc(ruleFnc: (value: any) => any) {
|
||||
return (value: any, helpers) => {
|
||||
return (value: any, helpers: any) => {
|
||||
const result = ruleFnc(value);
|
||||
return result.isSuccess
|
||||
? value
|
||||
|
||||
@ -4,18 +4,10 @@ import { UndefinedOr } from "../../../../../utilities";
|
||||
import { RuleValidator } from "../../RuleValidator";
|
||||
import { DomainError, handleDomainError } from "../../errors";
|
||||
import { Result } from "../Result";
|
||||
import {
|
||||
IStringValueObjectOptions,
|
||||
StringValueObject,
|
||||
} from "../StringValueObject";
|
||||
import { IStringValueObjectOptions, StringValueObject } from "../StringValueObject";
|
||||
|
||||
export class PostalCode extends StringValueObject {
|
||||
private static readonly LENGTH = 5;
|
||||
|
||||
protected static validate(
|
||||
value: UndefinedOr<string>,
|
||||
options: IStringValueObjectOptions
|
||||
) {
|
||||
protected static validate(value: UndefinedOr<string>, options: IStringValueObjectOptions) {
|
||||
const rule = Joi.string()
|
||||
.allow(null)
|
||||
.allow("")
|
||||
@ -26,10 +18,7 @@ export class PostalCode extends StringValueObject {
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static create(
|
||||
value: UndefinedOr<string>,
|
||||
options: IStringValueObjectOptions = {}
|
||||
) {
|
||||
public static create(value: UndefinedOr<string>, options: IStringValueObjectOptions = {}) {
|
||||
const _options = {
|
||||
label: "postal_code",
|
||||
...options,
|
||||
@ -39,11 +28,7 @@ export class PostalCode extends StringValueObject {
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
handleDomainError(
|
||||
DomainError.INVALID_INPUT_DATA,
|
||||
validationResult.error.message,
|
||||
_options
|
||||
)
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
return Result.ok(new PostalCode(validationResult.object));
|
||||
|
||||
@ -56,7 +56,7 @@ export abstract class Entity<T extends IEntityProps> {
|
||||
}
|
||||
|
||||
protected _flattenProps(props: T): { [s: string]: any } {
|
||||
return Object.entries(props).reduce((result, [key, valueObject]) => {
|
||||
return Object.entries(props).reduce((result: any, [key, valueObject]) => {
|
||||
console.log(key, valueObject.value);
|
||||
result[key] = valueObject.value;
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import DineroFactory, { Currency, Dinero } from "dinero.js";
|
||||
|
||||
import Joi from "joi";
|
||||
import { isNull } from "lodash";
|
||||
import { NullOr } from "../../../../utilities";
|
||||
@ -25,7 +23,7 @@ export interface MoneyValueObject {
|
||||
currency_code: string;
|
||||
}
|
||||
|
||||
type RoundingMode =
|
||||
export type RoundingMode =
|
||||
| "HALF_ODD"
|
||||
| "HALF_EVEN"
|
||||
| "HALF_UP"
|
||||
@ -34,8 +32,6 @@ type RoundingMode =
|
||||
| "HALF_AWAY_FROM_ZERO"
|
||||
| "DOWN";
|
||||
|
||||
export { RoundingMode };
|
||||
|
||||
export interface IMoneyValueProps {
|
||||
amount: NullOr<number | string>;
|
||||
currencyCode?: string;
|
||||
@ -92,6 +88,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
public static readonly DEFAULT_CURRENCY_CODE = defaultMoneyValueProps.currencyCode;
|
||||
|
||||
private readonly _isNull: boolean;
|
||||
|
||||
private readonly _options: IMoneyValueOptions;
|
||||
|
||||
protected static validate(amount: NullOr<number | string>, options: IMoneyValueOptions) {
|
||||
@ -250,12 +247,16 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
.object;
|
||||
}
|
||||
|
||||
private static _toString(value: NullOr<number>, scale: number): string {
|
||||
private static _toString(
|
||||
value: NullOr<number>,
|
||||
scale: number,
|
||||
locales?: Intl.LocalesArgument
|
||||
): string {
|
||||
if (value === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
new Intl.NumberFormat("es", {
|
||||
new Intl.NumberFormat(locales, {
|
||||
/*minimumSignificantDigits: scale,
|
||||
maximumSignificantDigits: scale,
|
||||
minimumFractionDigits: scale,*/
|
||||
@ -278,7 +279,11 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
};
|
||||
|
||||
public toString(): string {
|
||||
return MoneyValue._toString(this.isNull() ? null : this.getAmount(), this.getScale());
|
||||
return MoneyValue._toString(
|
||||
this.isNull() ? null : this.getAmount(),
|
||||
this.getScale(),
|
||||
this._options.locale
|
||||
);
|
||||
}
|
||||
|
||||
public toJSON() {
|
||||
|
||||
@ -5,16 +5,10 @@ import { UndefinedOr } from "../../../../utilities";
|
||||
import { RuleValidator } from "../RuleValidator";
|
||||
import { DomainError, handleDomainError } from "../errors";
|
||||
import { Result } from "./Result";
|
||||
import {
|
||||
IStringValueObjectOptions,
|
||||
StringValueObject,
|
||||
} from "./StringValueObject";
|
||||
import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject";
|
||||
|
||||
export class Phone extends StringValueObject {
|
||||
protected static validate(
|
||||
value: UndefinedOr<string>,
|
||||
options: IStringValueObjectOptions
|
||||
) {
|
||||
protected static validate(value: UndefinedOr<string>, options: IStringValueObjectOptions) {
|
||||
const rule = Joi.string() //.extend(JoiPhoneNumber)
|
||||
.allow(null)
|
||||
.allow("")
|
||||
@ -25,10 +19,7 @@ export class Phone extends StringValueObject {
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static create(
|
||||
value: UndefinedOr<string>,
|
||||
options: IStringValueObjectOptions = {}
|
||||
) {
|
||||
public static create(value: UndefinedOr<string>, options: IStringValueObjectOptions = {}) {
|
||||
const _options = {
|
||||
label: "phone",
|
||||
...options,
|
||||
@ -38,26 +29,21 @@ export class Phone extends StringValueObject {
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
handleDomainError(
|
||||
DomainError.INVALID_INPUT_DATA,
|
||||
validationResult.error.message,
|
||||
_options
|
||||
)
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
return Result.ok(new Phone(validationResult.object));
|
||||
}
|
||||
|
||||
public format(countryCode: string) {
|
||||
public format(/*countryCode: string*/) {
|
||||
const rule = Joi /*.extend(JoiPhoneNumber)*/.string();
|
||||
/*.phoneNumber({
|
||||
defaultCountry: countryCode,
|
||||
format: "international",
|
||||
})*/ const validationResult = RuleValidator.validate(rule, this.value);
|
||||
})*/
|
||||
const validationResult = RuleValidator.validate(rule, this.value);
|
||||
|
||||
return validationResult.isSuccess
|
||||
? validationResult.object
|
||||
: validationResult.error.message;
|
||||
return validationResult.isSuccess ? validationResult.object : validationResult.error.message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export const Currencies = {
|
||||
export const Currencies: any = {
|
||||
USD: {
|
||||
symbol: "$",
|
||||
name: "US Dollar",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// https://github.com/meikidd/iso-639-1/
|
||||
|
||||
const LANGUAGES_LIST = {
|
||||
const LANGUAGES_LIST: any = {
|
||||
aa: {
|
||||
name: "Afar",
|
||||
nativeName: "Afaraf",
|
||||
|
||||
@ -11,8 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dinero.js": "^1.9.1",
|
||||
"joi": "^17.12.3",
|
||||
"joi": "^17.13.3",
|
||||
"joi-phone-number": "^5.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"shallow-equal-object": "^1.1.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.2.2",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user