.
This commit is contained in:
parent
52de165a52
commit
d5281fb1f5
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@ -13,7 +13,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Launch Chrome localhost",
|
"name": "Launch Chrome localhost",
|
||||||
"type": "pwa-chrome",
|
"type": "pwa-chrome",
|
||||||
"port": 9222
|
"request": "launch",
|
||||||
|
"reAttach": true,
|
||||||
|
"url": "http://localhost:5173",
|
||||||
|
"webRoot": "${workspaceFolder}/client"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="es">
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import {
|
|||||||
DealersList,
|
DealersList,
|
||||||
LoginPage,
|
LoginPage,
|
||||||
LogoutPage,
|
LogoutPage,
|
||||||
|
QuoteCreate,
|
||||||
SettingsEditor,
|
SettingsEditor,
|
||||||
SettingsLayout,
|
SettingsLayout,
|
||||||
StartPage,
|
StartPage,
|
||||||
} from "./app";
|
} from "./app";
|
||||||
import { CatalogLayout, CatalogList } from "./app/catalog";
|
import { CatalogLayout, CatalogList } from "./app/catalog";
|
||||||
import { DashboardPage } from "./app/dashboard";
|
import { DashboardPage } from "./app/dashboard";
|
||||||
|
import { QuotesLayout } from "./app/quotes/layout";
|
||||||
import { QuotesList } from "./app/quotes/list";
|
import { QuotesList } from "./app/quotes/list";
|
||||||
import { ProtectedRoute } from "./components";
|
import { ProtectedRoute } from "./components";
|
||||||
|
|
||||||
@ -68,9 +70,21 @@ export const Routes = () => {
|
|||||||
path: "/quotes",
|
path: "/quotes",
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<QuotesList />
|
<QuotesLayout>
|
||||||
|
<Outlet />
|
||||||
|
</QuotesLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <QuotesList />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "add",
|
||||||
|
element: <QuoteCreate />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
@ -107,11 +121,12 @@ export const Routes = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Combine and conditionally include routes based on authentication status
|
// Combine and conditionally include routes based on authentication status
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter(
|
||||||
...routesForPublic,
|
[...routesForPublic, ...routesForAuthenticatedOnly, ...routesForNotAuthenticatedOnly],
|
||||||
...routesForAuthenticatedOnly,
|
{
|
||||||
...routesForNotAuthenticatedOnly,
|
//basename: "/app",
|
||||||
]);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Provide the router configuration using RouterProvider
|
// Provide the router configuration using RouterProvider
|
||||||
return <RouterProvider router={router} />;
|
return <RouterProvider router={router} />;
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { DataTablaRowActionFunction, DataTableRowActions } from "@/components";
|
|||||||
import { Badge } from "@/ui";
|
import { Badge } from "@/ui";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export const useCustomerInvoiceDataTableColumns = (
|
export const useCatalogTableColumns = (
|
||||||
actions: DataTablaRowActionFunction<IListArticles_Response_DTO>
|
actions: DataTablaRowActionFunction<IListArticles_Response_DTO, unknown>
|
||||||
): ColumnDef<IListArticles_Response_DTO>[] => {
|
): ColumnDef<IListArticles_Response_DTO>[] => {
|
||||||
const customerColumns: ColumnDef<IListArticles_Response_DTO>[] = useMemo(
|
const customerColumns: ColumnDef<IListArticles_Response_DTO>[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
21
client/src/app/quotes/QuotesContext.tsx
Normal file
21
client/src/app/quotes/QuotesContext.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { usePagination } from "@/lib/hooks";
|
||||||
|
import { PropsWithChildren, createContext } from "react";
|
||||||
|
|
||||||
|
export interface IQuotesContextState {}
|
||||||
|
|
||||||
|
export const QuotesContext = createContext<IQuotesContextState | null>(null);
|
||||||
|
|
||||||
|
export const QuotesProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [pagination, setPagination] = usePagination();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QuotesContext.Provider
|
||||||
|
value={{
|
||||||
|
pagination,
|
||||||
|
setPagination,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</QuotesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
client/src/app/quotes/components/AddNewRowButton.tsx
Normal file
30
client/src/app/quotes/components/AddNewRowButton.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button, ButtonProps } from "@/ui";
|
||||||
|
import { PlusCircleIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export interface AddNewRowButtonProps extends ButtonProps {
|
||||||
|
label?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddNewRowButton = ({
|
||||||
|
label = "Añade nueva fila",
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: AddNewRowButtonProps): JSX.Element => (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
className={cn(
|
||||||
|
"w-full gap-1 border-dashed text-muted-foreground border-muted-foreground/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||||
|
{label && <>{label}</>}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
AddNewRowButton.displayName = "AddNewRowButton";
|
||||||
122
client/src/app/quotes/components/QuotesDataTable.tsx
Normal file
122
client/src/app/quotes/components/QuotesDataTable.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { Card, CardContent } from "@/ui";
|
||||||
|
|
||||||
|
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
|
||||||
|
|
||||||
|
import { DataTable } from "@/components";
|
||||||
|
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
|
||||||
|
import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
||||||
|
import { IListQuotes_Response_DTO, MoneyValue } from "@shared/contexts";
|
||||||
|
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useQuotesList } from "../hooks";
|
||||||
|
|
||||||
|
export const QuotesDataTable = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { pagination, globalFilter, isFiltered } = useDataTableContext();
|
||||||
|
|
||||||
|
const { data, isPending, isError, error } = useQuotesList({
|
||||||
|
pagination: {
|
||||||
|
pageIndex: pagination.pageIndex,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
},
|
||||||
|
searchTerm: globalFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, any>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "id" as const,
|
||||||
|
accessorKey: "id",
|
||||||
|
enableResizing: false,
|
||||||
|
size: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "article_id" as const,
|
||||||
|
accessorKey: "id_article",
|
||||||
|
enableResizing: false,
|
||||||
|
size: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog_name" as const,
|
||||||
|
accessorKey: "catalog_name",
|
||||||
|
enableResizing: false,
|
||||||
|
size: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "description" as const,
|
||||||
|
accessorKey: "description",
|
||||||
|
header: () => <>{t("catalog.list.columns.description")}</>,
|
||||||
|
enableResizing: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "points" as const,
|
||||||
|
accessorKey: "points",
|
||||||
|
header: () => <div className='text-right'>{t("catalog.list.columns.points")}</div>,
|
||||||
|
cell: ({ renderValue }: { renderValue: () => any }) => (
|
||||||
|
<div className='text-right'>{renderValue()}</div>
|
||||||
|
),
|
||||||
|
enableResizing: false,
|
||||||
|
size: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "retail_price" as const,
|
||||||
|
accessorKey: "retail_price",
|
||||||
|
header: () => <div className='text-right'>{t("catalog.list.columns.retail_price")}</div>,
|
||||||
|
cell: ({ row }: { row: Row<any> }) => {
|
||||||
|
const price = MoneyValue.create(row.original.retail_price).object;
|
||||||
|
return <div className='text-right'>{price.toFormat()}</div>;
|
||||||
|
},
|
||||||
|
enableResizing: false,
|
||||||
|
size: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: data?.items ?? [],
|
||||||
|
columns: columns,
|
||||||
|
pageCount: data?.total_pages ?? -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <ErrorOverlay subtitle={(error as Error).message} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<DataTableSkeleton
|
||||||
|
columnCount={6}
|
||||||
|
searchableColumnCount={1}
|
||||||
|
filterableColumnCount={2}
|
||||||
|
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
|
||||||
|
shrinkZero
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.total_items === 0 && !isFiltered) {
|
||||||
|
return (
|
||||||
|
<SimpleEmptyState
|
||||||
|
subtitle='Empieza creando alguna cotización'
|
||||||
|
buttonText=''
|
||||||
|
onButtonClick={() => navigate("add", { relative: "path" })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DataTable table={table} paginationOptions={{ visible: true }}>
|
||||||
|
<DataTableToolbar table={table} />
|
||||||
|
</DataTable>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
462
client/src/app/quotes/components/SortableDataTable.tsx
Normal file
462
client/src/app/quotes/components/SortableDataTable.tsx
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
import { DataTableColumnHeader } from "@/components";
|
||||||
|
import { Badge } from "@/ui";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/ui/table";
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
DragOverlay,
|
||||||
|
DragStartEvent,
|
||||||
|
DropAnimation,
|
||||||
|
KeyboardSensor,
|
||||||
|
MeasuringStrategy,
|
||||||
|
MouseSensor,
|
||||||
|
PointerSensor,
|
||||||
|
TouchSensor,
|
||||||
|
UniqueIdentifier,
|
||||||
|
closestCenter,
|
||||||
|
defaultDropAnimation,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
Row,
|
||||||
|
RowData,
|
||||||
|
RowSelectionState,
|
||||||
|
VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
|
||||||
|
import { AddNewRowButton } from "./AddNewRowButton";
|
||||||
|
import { SortableDataTableToolbar } from "./SortableDataTableToolbar";
|
||||||
|
import { SortableTableRow } from "./SortableTableRow";
|
||||||
|
|
||||||
|
declare module "@tanstack/react-table" {
|
||||||
|
interface TableMeta<TData extends RowData> {
|
||||||
|
insertItem: (rowIndex: number, data: TData) => void;
|
||||||
|
appendItem: (data: TData) => void;
|
||||||
|
duplicateItems: (rowIndex?: number) => void;
|
||||||
|
deleteItems: (rowIndex?: number | number[]) => void;
|
||||||
|
updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortableProps {
|
||||||
|
id: UniqueIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SortableDataTableProps = {
|
||||||
|
columns: ColumnDef<unknown, unknown>[];
|
||||||
|
data: Record<"id", string>[];
|
||||||
|
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields">;
|
||||||
|
};
|
||||||
|
|
||||||
|
const measuringConfig = {
|
||||||
|
droppable: {
|
||||||
|
strategy: MeasuringStrategy.Always,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropAnimationConfig: DropAnimation = {
|
||||||
|
keyframes({ transform }) {
|
||||||
|
return [
|
||||||
|
{ opacity: 1, transform: CSS.Transform.toString(transform.initial) },
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
transform: CSS.Transform.toString({
|
||||||
|
...transform.final,
|
||||||
|
x: transform.final.x + 5,
|
||||||
|
y: transform.final.y + 5,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
easing: "ease-out",
|
||||||
|
sideEffects({ active }) {
|
||||||
|
active.node.animate([{ opacity: 0 }, { opacity: 1 }], {
|
||||||
|
duration: defaultDropAnimation.duration,
|
||||||
|
easing: defaultDropAnimation.easing,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/*const defaultColumn: Partial<ColumnDef<unknown>> = {
|
||||||
|
cell: ({ table, row: { index, original }, column, getValue }) => {
|
||||||
|
const initialValue = getValue();
|
||||||
|
|
||||||
|
// We need to keep and update the state of the cell normally
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
|
// If the initialValue is changed external, sync it up with our state
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(initialValue);
|
||||||
|
}, [initialValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
value={value as string}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onBlur={() => {
|
||||||
|
console.log(column.id, value);
|
||||||
|
table.options.meta?.updateItem(index, original, column.id, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};*/
|
||||||
|
|
||||||
|
export function SortableDataTable({ columns, data, actions }: SortableDataTableProps) {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||||
|
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
|
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
enableColumnResizing: false,
|
||||||
|
columnResizeMode: "onChange",
|
||||||
|
//defaultColumn,
|
||||||
|
state: {
|
||||||
|
rowSelection,
|
||||||
|
columnVisibility,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
enableMultiRowSelection: true,
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getRowId: (originalRow: unknown) => originalRow?.id,
|
||||||
|
debugHeaders: true,
|
||||||
|
debugColumns: true,
|
||||||
|
meta: {
|
||||||
|
insertItem: (rowIndex: number, data: object = {}) => {
|
||||||
|
actions.insert(rowIndex, data, { shouldFocus: true });
|
||||||
|
},
|
||||||
|
appendItem: (data: object = {}) => {
|
||||||
|
actions.append(data, { shouldFocus: true });
|
||||||
|
},
|
||||||
|
duplicateItems: (rowIndex?: number) => {
|
||||||
|
if (rowIndex != undefined) {
|
||||||
|
const originalData = table.getRowModel().rows[rowIndex].original;
|
||||||
|
actions.insert(rowIndex + 1, originalData, { shouldFocus: true });
|
||||||
|
} else if (table.getSelectedRowModel().rows.length) {
|
||||||
|
const lastIndex =
|
||||||
|
table.getSelectedRowModel().rows[table.getSelectedRowModel().rows.length - 1].index;
|
||||||
|
const data = table
|
||||||
|
.getSelectedRowModel()
|
||||||
|
.rows.map((row) => ({ ...row.original, id: undefined }));
|
||||||
|
|
||||||
|
if (table.getRowModel().rows.length < lastIndex + 1) {
|
||||||
|
actions.append(data);
|
||||||
|
} else {
|
||||||
|
actions.insert(lastIndex + 1, data, { shouldFocus: true });
|
||||||
|
}
|
||||||
|
table.resetRowSelection();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteItems: (rowIndex?: number | number[]) => {
|
||||||
|
if (rowIndex != undefined) {
|
||||||
|
actions.remove(rowIndex);
|
||||||
|
} else if (table.getSelectedRowModel().rows.length > 0) {
|
||||||
|
let start = table.getSelectedRowModel().rows.length - 1;
|
||||||
|
for (; start >= 0; start--) {
|
||||||
|
const oldIndex = sorteableRowIds.indexOf(
|
||||||
|
String(table.getSelectedRowModel().rows[start].id)
|
||||||
|
);
|
||||||
|
actions.remove(oldIndex);
|
||||||
|
sorteableRowIds.splice(oldIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*table.getSelectedRowModel().rows.forEach((row) => {
|
||||||
|
});*/
|
||||||
|
table.resetRowSelection();
|
||||||
|
} else {
|
||||||
|
actions.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateItem: (rowIndex: number, rowData: unknown, fieldName: string, value: unknown) => {
|
||||||
|
// Skip page index reset until after next rerender
|
||||||
|
// skipAutoResetPageIndex();
|
||||||
|
console.log({
|
||||||
|
rowIndex,
|
||||||
|
rowData,
|
||||||
|
fieldName,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.update(rowIndex, { ...rowData, [`${fieldName}`]: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(MouseSensor, {}),
|
||||||
|
useSensor(TouchSensor, {}),
|
||||||
|
useSensor(KeyboardSensor, {}),
|
||||||
|
useSensor(PointerSensor, {})
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleDragEnd(event: DragEndEvent) {
|
||||||
|
let activeId = event.active.id;
|
||||||
|
let overId = event.over?.id;
|
||||||
|
|
||||||
|
if (overId !== undefined && activeId !== overId) {
|
||||||
|
let newIndex = sorteableRowIds.indexOf(String(overId));
|
||||||
|
|
||||||
|
if (table.getSelectedRowModel().rows.length > 1) {
|
||||||
|
table.getSelectedRowModel().rows.forEach((row, index) => {
|
||||||
|
const oldIndex = sorteableRowIds.indexOf(String(row.id));
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
activeId = row.id;
|
||||||
|
newIndex = sorteableRowIds.indexOf(String(overId));
|
||||||
|
if (newIndex < oldIndex) {
|
||||||
|
newIndex = newIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.move(oldIndex, newIndex);
|
||||||
|
sorteableRowIds.splice(newIndex, 0, sorteableRowIds.splice(oldIndex, 1)[0]);
|
||||||
|
|
||||||
|
overId = row.id;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const oldIndex = sorteableRowIds.indexOf(String(activeId));
|
||||||
|
actions.move(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragStart({ active }: DragStartEvent) {
|
||||||
|
if (!table.getSelectedRowModel().rowsById[active.id]) {
|
||||||
|
table.resetRowSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveId(active.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragCancel() {
|
||||||
|
setActiveId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hadleNewItem = useCallback(() => {
|
||||||
|
actions.append([
|
||||||
|
{
|
||||||
|
description: "a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "b",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "d",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}, [actions]);
|
||||||
|
|
||||||
|
function filterItems(items: string[] | Row<unknown>[]) {
|
||||||
|
if (!activeId) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.filter((idOrRow) => {
|
||||||
|
const id = typeof idOrRow === "string" ? idOrRow : idOrRow.id;
|
||||||
|
return id === activeId || !table.getSelectedRowModel().rowsById[id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
measuring={measuringConfig}
|
||||||
|
sensors={sensors}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragCancel={handleDragCancel}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
>
|
||||||
|
<SortableDataTableToolbar table={table} />
|
||||||
|
<Table className='table-auto'>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id} className='hover:bg-transparent'>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id} className='px-1'>
|
||||||
|
{header.isPlaceholder ? null : (
|
||||||
|
<DataTableColumnHeader table={table} header={header} />
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<SortableContext
|
||||||
|
items={filterItems(sorteableRowIds)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{filterItems(table.getRowModel().rows).map((row) => (
|
||||||
|
<SortableTableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</SortableTableRow>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter className='bg-default'>
|
||||||
|
<TableRow className='hover:bg-default'>
|
||||||
|
<TableCell colSpan={6} className='py-6'>
|
||||||
|
<AddNewRowButton onClick={hadleNewItem} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{createPortal(
|
||||||
|
<DragOverlay dropAnimation={dropAnimationConfig} className={"z-40 opacity-100"}>
|
||||||
|
{activeId && (
|
||||||
|
<div className='relative flex flex-wrap'>
|
||||||
|
{table.getSelectedRowModel().rows.length ? (
|
||||||
|
<Badge
|
||||||
|
variant='destructive'
|
||||||
|
className='absolute z-50 flex items-center justify-center w-2 h-2 p-3 rounded-full top left -left-2 -top-2'
|
||||||
|
>
|
||||||
|
{table.getSelectedRowModel().rows.length}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
<div className='absolute z-40 bg-white border rounded shadow opacity-100 top left hover:bg-white border-muted-foreground/50'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map(
|
||||||
|
(row) =>
|
||||||
|
row.id === activeId && (
|
||||||
|
<TableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{table.getSelectedRowModel().rows.length > 1 && (
|
||||||
|
<div className='absolute z-30 transform -translate-x-1 translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-1'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map(
|
||||||
|
(row) =>
|
||||||
|
row.id === activeId && (
|
||||||
|
<TableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{table.getSelectedRowModel().rows.length > 2 && (
|
||||||
|
<div className='absolute z-20 transform translate-x-1 -translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left -rotate-1'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map(
|
||||||
|
(row) =>
|
||||||
|
row.id === activeId && (
|
||||||
|
<TableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{table.getSelectedRowModel().rows.length > 3 && (
|
||||||
|
<div className='absolute z-10 transform translate-x-2 -translate-y-2 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-2'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map(
|
||||||
|
(row) =>
|
||||||
|
row.id === activeId && (
|
||||||
|
<TableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
200
client/src/app/quotes/components/SortableDataTableToolbar.tsx
Normal file
200
client/src/app/quotes/components/SortableDataTableToolbar.tsx
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Separator,
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/ui";
|
||||||
|
import { Table } from "@tanstack/react-table";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
CirclePlusIcon,
|
||||||
|
ClockIcon,
|
||||||
|
CopyPlusIcon,
|
||||||
|
ForwardIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
ReplyAllIcon,
|
||||||
|
ReplyIcon,
|
||||||
|
ScanIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export const SortableDataTableToolbar = ({ table }: { table: Table<unknown> }) => {
|
||||||
|
const selectedRowsCount = table.getSelectedRowModel().rows.length;
|
||||||
|
|
||||||
|
if (selectedRowsCount) {
|
||||||
|
return (
|
||||||
|
<div className='flex items-center h-12 p-1 text-white rounded-md bg-primary '>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Button>{`${selectedRowsCount} filas seleccionadas`}</Button>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
size='icon'
|
||||||
|
disabled={!table.getSelectedRowModel().rows.length}
|
||||||
|
onClick={() => table.options.meta?.duplicateItems()}
|
||||||
|
>
|
||||||
|
<CopyPlusIcon className='w-4 h-4' />
|
||||||
|
<span className='sm:sr-only'>{t("common.duplicate_rows")}</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t("common.duplicate_rows_tooltip")}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
size='icon'
|
||||||
|
disabled={!table.getSelectedRowModel().rows.length}
|
||||||
|
onClick={() => table.options.meta?.deleteItems()}
|
||||||
|
>
|
||||||
|
<Trash2Icon className='w-4 h-4' />
|
||||||
|
<span className='sm:sr-only'>Eliminar</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Elimina las fila(s) seleccionada(s)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Separator orientation='vertical' className='h-6 mx-1 bg-muted/50' />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
size='sm'
|
||||||
|
disabled={!table.getSelectedRowModel().rows.length}
|
||||||
|
onClick={() => table.resetRowSelection()}
|
||||||
|
>
|
||||||
|
<ScanIcon className='w-4 h-4 md:mr-2' />
|
||||||
|
<span className='sm:sr-only'>Quitar selección</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Quita la selección</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center h-12 p-1 rounded-md bg-accent/50 text-muted-foreground'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
size='icon'
|
||||||
|
onClick={() => table.options.meta?.appendItem()}
|
||||||
|
>
|
||||||
|
<CirclePlusIcon className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Añadir</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Añadir fila</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Separator orientation='vertical' className='h-6 mx-1 bg-muted-foreground/30' />
|
||||||
|
<Tooltip>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant='ghost' size='icon'>
|
||||||
|
<ClockIcon className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Snooze</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className='flex w-[535px] p-0'>
|
||||||
|
<div className='flex flex-col gap-2 px-2 py-4 border-r'>
|
||||||
|
<div className='px-4 text-sm font-medium'>Snooze until</div>
|
||||||
|
<div className='grid min-w-[250px] gap-1'>
|
||||||
|
<Button variant='ghost' className='justify-start font-normal'>
|
||||||
|
Later today <span className='ml-auto text-muted-foreground'></span>
|
||||||
|
</Button>
|
||||||
|
<Button variant='ghost' className='justify-start font-normal'>
|
||||||
|
Tomorrow
|
||||||
|
<span className='ml-auto text-muted-foreground'></span>
|
||||||
|
</Button>
|
||||||
|
<Button variant='ghost' className='justify-start font-normal'>
|
||||||
|
This weekend
|
||||||
|
<span className='ml-auto text-muted-foreground'></span>
|
||||||
|
</Button>
|
||||||
|
<Button variant='ghost' className='justify-start font-normal'>
|
||||||
|
Next week
|
||||||
|
<span className='ml-auto text-muted-foreground'></span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-2'>
|
||||||
|
<CalendarIcon />
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<TooltipContent>Snooze</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2 ml-auto'>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant='ghost' size='icon'>
|
||||||
|
<ReplyIcon className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Reply</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Reply</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant='ghost' size='icon'>
|
||||||
|
<ReplyAllIcon className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Reply all</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Reply all</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant='ghost' size='icon'>
|
||||||
|
<ForwardIcon className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Forward</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Forward</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='h-6 mx-1 bg-muted-foreground/30' />
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant='ghost' size='icon'>
|
||||||
|
<MoreVerticalIcon className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Columnas</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
{table.getAllColumns().map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
disabled={!column.getCanHide()}
|
||||||
|
className='capitalize'
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
80
client/src/app/quotes/components/SortableTableRow.tsx
Normal file
80
client/src/app/quotes/components/SortableTableRow.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
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 { SortableProps } from "./SortableDataTable";
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
listeners: DraggableSyntheticListeners;
|
||||||
|
ref(node: HTMLElement | null): void;
|
||||||
|
}
|
||||||
|
export const SortableTableRowContext = createContext<Context>({
|
||||||
|
attributes: {},
|
||||||
|
listeners: undefined,
|
||||||
|
ref() {},
|
||||||
|
});
|
||||||
|
|
||||||
|
function animateLayoutChanges(args) {
|
||||||
|
if (args.isSorting || args.wasDragging) {
|
||||||
|
return defaultAnimateLayoutChanges(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortableTableRow({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<SortableProps>) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
isDragging,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
setActivatorNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
} = useSortable({
|
||||||
|
animateLayoutChanges,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style: CSSProperties = {
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = useMemo(
|
||||||
|
() => ({
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
ref: setActivatorNodeRef,
|
||||||
|
}),
|
||||||
|
[attributes, listeners, setActivatorNodeRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableTableRowContext.Provider value={context}>
|
||||||
|
<TableRow
|
||||||
|
key={id}
|
||||||
|
id={String(id)}
|
||||||
|
className={cn(
|
||||||
|
isDragging ? "opacity-40" : "opacity-100",
|
||||||
|
"hover:bg-muted/30 m-0",
|
||||||
|
)}
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TableRow>
|
||||||
|
</SortableTableRowContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
ButtonGroup,
|
||||||
|
CancelButton,
|
||||||
|
FormGroup,
|
||||||
|
FormMoneyField,
|
||||||
|
FormTextAreaField,
|
||||||
|
FormTextField,
|
||||||
|
SubmitButton,
|
||||||
|
} from "@/components";
|
||||||
|
import { Input } from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { HashIcon } from "lucide-react";
|
||||||
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import { useDetailColumns } from "../../hooks";
|
||||||
|
import { SortableDataTable } from "../SortableDataTable";
|
||||||
|
|
||||||
|
export const QuoteDetailsCardEditor = () => {
|
||||||
|
const { control, register, formState } = useFormContext();
|
||||||
|
|
||||||
|
const { fields, ...fieldActions } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "items",
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = useDetailColumns(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "row_id" as const,
|
||||||
|
header: () => (
|
||||||
|
<HashIcon aria-label='Orden de fila' className='items-center justify-center w-4 h-4' />
|
||||||
|
),
|
||||||
|
accessorFn: (originalRow: unknown, index: number) => index + 1,
|
||||||
|
size: 26,
|
||||||
|
enableHiding: false,
|
||||||
|
enableSorting: false,
|
||||||
|
enableResizing: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "description" as const,
|
||||||
|
accessorKey: "description",
|
||||||
|
size: 400,
|
||||||
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
|
return (
|
||||||
|
<FormTextAreaField
|
||||||
|
autoSize
|
||||||
|
control={control}
|
||||||
|
{...register(`items.${index}.description`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "quantity" as const,
|
||||||
|
accessorKey: "quantity",
|
||||||
|
header: "quantity",
|
||||||
|
size: 60,
|
||||||
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
|
return (
|
||||||
|
<FormTextField
|
||||||
|
type='number'
|
||||||
|
control={control}
|
||||||
|
{...register(`items.${index}.quantity`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unit_measure" as const,
|
||||||
|
accessorKey: "unit_measure",
|
||||||
|
header: "unit_measure",
|
||||||
|
size: 60,
|
||||||
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
|
return <Input key={id} {...register(`items.${index}.unit_measure`)} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unit_price" as const,
|
||||||
|
accessorKey: "unit_price",
|
||||||
|
header: "unit_price",
|
||||||
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
|
return <FormMoneyField control={control} {...register(`items.${index}.unit_price`)} />;
|
||||||
|
},
|
||||||
|
} /*
|
||||||
|
{
|
||||||
|
id: "subtotal" as const,
|
||||||
|
accessorKey: "subtotal",
|
||||||
|
header: "subtotal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tax_amount" as const,
|
||||||
|
accessorKey: "tax_amount",
|
||||||
|
header: "tax_amount",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "total" as const,
|
||||||
|
accessorKey: "total",
|
||||||
|
header: "total",
|
||||||
|
},*/,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
enableDragHandleColumn: true,
|
||||||
|
enableSelectionColumn: true,
|
||||||
|
enableActionsColumn: true,
|
||||||
|
rowActionFn: (props) => {
|
||||||
|
const { table, row } = props;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "Duplicar",
|
||||||
|
onClick: () => table.options.meta?.duplicateItems(row.index),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Insertar fila encima",
|
||||||
|
onClick: () => table.options.meta?.insertItem(row.index),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Insertar fila debajo",
|
||||||
|
onClick: () => table.options.meta?.insertItem(row.index + 1),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Eliminar",
|
||||||
|
shortcut: "⌘⌫",
|
||||||
|
onClick: () => {
|
||||||
|
table.options.meta?.deleteItems(row.index);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
title={t("quotes.create.tabs.items.title")}
|
||||||
|
description={t("quotes.create.tabs.items.desc")}
|
||||||
|
actions={
|
||||||
|
<ButtonGroup className='md:hidden'>
|
||||||
|
<CancelButton onClick={() => null} size='sm' />
|
||||||
|
<SubmitButton
|
||||||
|
disabled={!formState.isDirty || formState.isSubmitting || formState.isLoading}
|
||||||
|
label='Guardar'
|
||||||
|
size='sm'
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='gap-0'>
|
||||||
|
<SortableDataTable actions={fieldActions} columns={columns} data={fields} />
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { FormGroup } from "@/components";
|
||||||
|
import { Button, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { Upload } from "lucide-react";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
export const QuoteDocumentsCardEditor = () => {
|
||||||
|
/*const { uploadFiles, progresses, uploadedFiles, isUploading } = useUploadFile("imageUploader", {
|
||||||
|
defaultUploadedFiles: [],
|
||||||
|
});*/
|
||||||
|
|
||||||
|
const { control, register, formState } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid gap-6 md:grid-cols-6'>
|
||||||
|
<FormGroup
|
||||||
|
className='md:col-span-4'
|
||||||
|
title={t("quotes.create.form_groups.documents.title")}
|
||||||
|
description={t("quotes.create.form_groups.documents.desc")}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name='images'
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<FormItem className='w-full'>
|
||||||
|
<FormLabel>Images</FormLabel>
|
||||||
|
<FormControl></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button className='w-fit' disabled={formState.disabled}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid gap-6 md:grid-cols-6'>
|
||||||
|
<FormGroup
|
||||||
|
className='md:col-span-4'
|
||||||
|
title={t("quotes.create.form_groups.documents.title")}
|
||||||
|
description={t("quotes.create.form_groups.documents.desc")}
|
||||||
|
>
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover w-full rounded-md aspect-square'
|
||||||
|
height='300'
|
||||||
|
src='https://placehold.co/300'
|
||||||
|
width='300'
|
||||||
|
/>
|
||||||
|
<div className='grid grid-cols-3 gap-2'>
|
||||||
|
<button>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover w-full rounded-md aspect-square'
|
||||||
|
height='84'
|
||||||
|
src='https://placehold.co/84'
|
||||||
|
width='84'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover w-full rounded-md aspect-square'
|
||||||
|
height='84'
|
||||||
|
src='https://placehold.co/84'
|
||||||
|
width='84'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button className='flex items-center justify-center w-full border border-dashed rounded-md aspect-square'>
|
||||||
|
<Upload className='w-4 h-4 text-muted-foreground' />
|
||||||
|
<span className='sr-only'>{t("common.upload")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import { FormDatePickerField, FormGroup, FormTextAreaField, FormTextField } from "@/components";
|
||||||
|
import { Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
export const QuoteGeneralCardEditor = () => {
|
||||||
|
const { register, formState } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid gap-6 md:grid-cols-6'>
|
||||||
|
<FormGroup
|
||||||
|
className='md:col-span-4'
|
||||||
|
title={t("quotes.create.form_groups.general.title")}
|
||||||
|
description={t("quotes.create.form_groups.general.desc")}
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-2 grid-rows-2 gap-6'>
|
||||||
|
<FormTextAreaField
|
||||||
|
className='row-span-2'
|
||||||
|
required
|
||||||
|
label={t("quotes.create.form_fields.customer_information.label")}
|
||||||
|
description={t("quotes.create.form_fields.customer_information.desc")}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
|
||||||
|
{...register("customer_information", {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
|
errors={formState.errors}
|
||||||
|
/>
|
||||||
|
<FormTextField
|
||||||
|
label={t("quotes.create.form_fields.reference.label")}
|
||||||
|
description={t("quotes.create.form_fields.reference.desc")}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
placeholder={t("quotes.create.form_fields.reference.placeholder")}
|
||||||
|
{...register("reference", {
|
||||||
|
required: false,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<FormDatePickerField
|
||||||
|
required
|
||||||
|
label={t("quotes.create.form_fields.date.label")}
|
||||||
|
description={t("quotes.create.form_fields.date.desc")}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
placeholder={t("quotes.create.form_fields.date.placeholder")}
|
||||||
|
{...register("date", {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-2 grid-rows-2 gap-6'>
|
||||||
|
<FormTextField
|
||||||
|
label={t("quotes.create.form_fields.validity.label")}
|
||||||
|
description={t("quotes.create.form_fields.validity.desc")}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
placeholder={t("quotes.create.form_fields.validity.placeholder")}
|
||||||
|
{...register("validity", {
|
||||||
|
required: false,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormTextAreaField
|
||||||
|
label={t("quotes.create.form_fields.payment_method.label")}
|
||||||
|
description={t("quotes.create.form_fields.payment_method.desc")}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
placeholder={t("quotes.create.form_fields.payment_method.placeholder")}
|
||||||
|
{...register("payment_method", {
|
||||||
|
required: false,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormTextAreaField
|
||||||
|
className='col-span-2'
|
||||||
|
label={t("quotes.create.form_fields.notes.label")}
|
||||||
|
description={t("quotes.create.form_fields.notes.desc")}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
placeholder={t("quotes.create.form_fields.notes.placeholder")}
|
||||||
|
{...register("notes", {
|
||||||
|
required: false,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
className='md:col-span-2'
|
||||||
|
title={t("quotes.create.form_groups.status.title")}
|
||||||
|
description={t("quotes.create.form_groups.status.desc")}
|
||||||
|
>
|
||||||
|
<div className='grid gap-6'>
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<Label htmlFor='status'>Status</Label>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger id='status' aria-label='Select status'>
|
||||||
|
<SelectValue placeholder='Select status' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='draft'>Draft</SelectItem>
|
||||||
|
<SelectItem value='published'>Active</SelectItem>
|
||||||
|
<SelectItem value='archived'>Archived</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<FormTextField
|
||||||
|
required
|
||||||
|
label={t("quotes.create.form_fields.lang_code.label")}
|
||||||
|
description={t("quotes.create.form_fields.lang_code.desc")}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
placeholder={t("quotes.create.form_fields.lang_code.placeholder")}
|
||||||
|
{...register("lang_code", {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<FormTextField
|
||||||
|
required
|
||||||
|
label={t("quotes.create.form_fields.currency_code.label")}
|
||||||
|
description={t("quotes.create.form_fields.currency_code.desc")}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
placeholder={t("quotes.create.form_fields.currency_code.placeholder")}
|
||||||
|
{...register("currency_code", {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
client/src/app/quotes/components/editors/index.ts
Normal file
3
client/src/app/quotes/components/editors/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./QuoteDetailsCardEditor";
|
||||||
|
export * from "./QuoteDocumentsCardEditor";
|
||||||
|
export * from "./QuoteGeneralCardEditor";
|
||||||
1
client/src/app/quotes/components/index.ts
Normal file
1
client/src/app/quotes/components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./QuotesDataTable";
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { IListQuotes_Response_DTO } from "@shared/contexts";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import { DataTablaRowActionFunction, DataTableRowActions } from "@/components";
|
||||||
|
import { Badge } from "@/ui";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export const useQuoteDataTableColumns = (
|
||||||
|
actions: DataTablaRowActionFunction<IListQuotes_Response_DTO, unknown>
|
||||||
|
): ColumnDef<IListQuotes_Response_DTO>[] => {
|
||||||
|
const customerColumns: ColumnDef<IListQuotes_Response_DTO>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
/*{
|
||||||
|
id: "complete_name",
|
||||||
|
header: "Nombre",
|
||||||
|
accessorFn: (row) => (
|
||||||
|
<div className="flex items-center justify-between space-x-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarImage src={row.photo_url} />
|
||||||
|
<AvatarFallback>
|
||||||
|
{acronym(`${row.first_name} ${row.last_name}`)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<p className="text-base font-semibold">{`${row.first_name} ${row.last_name}`}</p>
|
||||||
|
<p className="mt-1">{row.job_title}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: "alphanumeric",
|
||||||
|
enableHiding: false,
|
||||||
|
|
||||||
|
cell: ({ renderValue }) => (
|
||||||
|
<span className="w-full">
|
||||||
|
<>{renderValue()}</>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},*/
|
||||||
|
{
|
||||||
|
id: "state",
|
||||||
|
accessorKey: "state",
|
||||||
|
header: "Estado",
|
||||||
|
cell: ({ renderValue }) => (
|
||||||
|
<Badge variant={"destructive"}>
|
||||||
|
<>{renderValue()}</>
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "client",
|
||||||
|
accessorKey: "client",
|
||||||
|
header: "Cliente",
|
||||||
|
enableSorting: false,
|
||||||
|
sortingFn: "alphanumeric",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "Acciones",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <DataTableRowActions row={row} actions={actions} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[actions]
|
||||||
|
);
|
||||||
|
return customerColumns;
|
||||||
|
};
|
||||||
116
client/src/app/quotes/create.tsx
Normal file
116
client/src/app/quotes/create.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
|
||||||
|
import { SubmitButton } from "@/components";
|
||||||
|
import { useGetIdentity } from "@/lib/hooks";
|
||||||
|
import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { QuoteGeneralCardEditor } from "./components/editors";
|
||||||
|
import { useQuotes } from "./hooks";
|
||||||
|
|
||||||
|
type QuoteDataForm = {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
date: string;
|
||||||
|
reference: string;
|
||||||
|
customer_information: string;
|
||||||
|
lang_code: string;
|
||||||
|
currency_code: string;
|
||||||
|
payment_method: string;
|
||||||
|
notes: string;
|
||||||
|
validity: string;
|
||||||
|
items: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type QuoteCreateProps = {
|
||||||
|
isOverModal?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuoteCreate = ({ isOverModal }: QuoteCreateProps) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { data: userIdentity } = useGetIdentity();
|
||||||
|
console.log(userIdentity);
|
||||||
|
|
||||||
|
const { useQuery, useMutation } = useQuotes();
|
||||||
|
|
||||||
|
const { data } = useQuery;
|
||||||
|
const { mutate } = useMutation;
|
||||||
|
|
||||||
|
const form = useForm<QuoteDataForm>({
|
||||||
|
mode: "onBlur",
|
||||||
|
values: data,
|
||||||
|
defaultValues: {
|
||||||
|
date: "",
|
||||||
|
reference: "",
|
||||||
|
customer_information: "",
|
||||||
|
lang_code: "",
|
||||||
|
currency_code: "",
|
||||||
|
payment_method: "",
|
||||||
|
notes: "",
|
||||||
|
validity: "",
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<QuoteDataForm> = async (data) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
data.currency_code = "EUR";
|
||||||
|
data.lang_code = String(userIdentity?.language);
|
||||||
|
mutate(data);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
<Button variant='outline' size='icon' className='h-7 w-7'>
|
||||||
|
<ChevronLeft className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>{t("quotes.common.back")}</span>
|
||||||
|
</Button>
|
||||||
|
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
||||||
|
{t("quotes.create.title")}
|
||||||
|
</h1>
|
||||||
|
<Badge variant='default' className='ml-auto sm:ml-0'>
|
||||||
|
{t("quotes.status.draft")}
|
||||||
|
</Badge>
|
||||||
|
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
||||||
|
<Button variant='outline' size='sm'>
|
||||||
|
{t("quotes.create.buttons.discard")}
|
||||||
|
</Button>
|
||||||
|
<SubmitButton variant={form.formState.isDirty ? "default" : "outline"} size='sm'>
|
||||||
|
{t("quotes.create.buttons.save_quote")}
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tabs defaultValue='general' className='space-y-4'>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
|
||||||
|
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
|
||||||
|
<TabsTrigger value='documents'>{t("quotes.create.tabs.documents")}</TabsTrigger>
|
||||||
|
<TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value='general'>
|
||||||
|
<QuoteGeneralCardEditor />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='items'></TabsContent>
|
||||||
|
<TabsContent value='documents'></TabsContent>
|
||||||
|
<TabsContent value='history'></TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
<div className='flex items-center justify-center gap-2 md:hidden'>
|
||||||
|
<Button variant='outline' size='sm'>
|
||||||
|
{t("quotes.create.buttons.discard")}
|
||||||
|
</Button>
|
||||||
|
<Button size='sm'>{t("quotes.create.buttons.save_quote")}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
client/src/app/quotes/hooks/index.ts
Normal file
3
client/src/app/quotes/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./useDetailColumns";
|
||||||
|
export * from "./useQuotes";
|
||||||
|
export * from "./useQuotesList";
|
||||||
124
client/src/app/quotes/hooks/useDetailColumns.tsx
Normal file
124
client/src/app/quotes/hooks/useDetailColumns.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
DataTablaRowActionFunction,
|
||||||
|
DataTableRowActions,
|
||||||
|
DataTableRowDragHandleCell,
|
||||||
|
} from "@/components";
|
||||||
|
import { Checkbox } from "@/ui";
|
||||||
|
import { ColumnDef, Row, Table } from "@tanstack/react-table";
|
||||||
|
import { MoreHorizontalIcon, UnfoldVertical } from "lucide-react";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
/*function getSelectedRowRange<T>(
|
||||||
|
rows: Row<T>[],
|
||||||
|
currentID: number,
|
||||||
|
selectedID: number,
|
||||||
|
): Row<T>[] {
|
||||||
|
const rangeStart = selectedID > currentID ? currentID : selectedID;
|
||||||
|
const rangeEnd = rangeStart === currentID ? selectedID : currentID;
|
||||||
|
return rows.slice(rangeStart, rangeEnd + 1);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
export function useDetailColumns<TData, TValue>(
|
||||||
|
columns: ColumnDef<TData>[],
|
||||||
|
options: {
|
||||||
|
enableDragHandleColumn?: boolean;
|
||||||
|
enableSelectionColumn?: boolean;
|
||||||
|
enableActionsColumn?: boolean;
|
||||||
|
rowActionFn?: DataTablaRowActionFunction<TData, TValue>;
|
||||||
|
} = {}
|
||||||
|
): ColumnDef<TData>[] {
|
||||||
|
const {
|
||||||
|
enableDragHandleColumn = false,
|
||||||
|
enableSelectionColumn = false,
|
||||||
|
enableActionsColumn = false,
|
||||||
|
rowActionFn = undefined,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// const lastSelectedId = "";
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (enableSelectionColumn) {
|
||||||
|
columns.unshift({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
id='select-all'
|
||||||
|
checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
|
aria-label='Seleccionar todo'
|
||||||
|
className='translate-y-[2px]'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
cell: ({ row, table }: { row: Row<TData>; table: Table<TData> }) => (
|
||||||
|
<Checkbox
|
||||||
|
id={`select-row-${row.id}`}
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
disabled={!row.getCanSelect()}
|
||||||
|
onCheckedChange={row.getToggleSelectedHandler()}
|
||||||
|
aria-label='Seleccionar fila'
|
||||||
|
className='translate-y-[2px]'
|
||||||
|
/*onClick={(e) => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
const { rows, rowsById } = table.getRowModel();
|
||||||
|
const rowsToToggle = getSelectedRowRange(
|
||||||
|
rows,
|
||||||
|
Number(row.id),
|
||||||
|
Number(lastSelectedId),
|
||||||
|
);
|
||||||
|
const isCellSelected = rowsById[row.id].getIsSelected();
|
||||||
|
rowsToToggle.forEach((_row) =>
|
||||||
|
_row.toggleSelected(!isCellSelected),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
row.toggleSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSelectedId = row.id;
|
||||||
|
}}*/
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
size: 30,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableDragHandleColumn) {
|
||||||
|
columns.unshift({
|
||||||
|
id: "row_drag_handle",
|
||||||
|
header: () => (
|
||||||
|
<UnfoldVertical aria-label='Mover fila' className='items-center justify-center w-4 h-4' />
|
||||||
|
),
|
||||||
|
cell: ({ row }: { row: Row<TData> }) => <DataTableRowDragHandleCell rowId={row.id} />,
|
||||||
|
size: 16,
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableActionsColumn) {
|
||||||
|
columns.push({
|
||||||
|
id: "row_actions",
|
||||||
|
header: () => (
|
||||||
|
<MoreHorizontalIcon
|
||||||
|
aria-label='Acciones'
|
||||||
|
className='items-center justify-center w-4 h-4'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: (props) => {
|
||||||
|
return <DataTableRowActions {...props} actions={rowActionFn} />;
|
||||||
|
},
|
||||||
|
size: 16,
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
48
client/src/app/quotes/hooks/useQuotes.tsx
Normal file
48
client/src/app/quotes/hooks/useQuotes.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { useOne, useSave } from "@/lib/hooks/useDataSource";
|
||||||
|
import { TDataSourceError } from "@/lib/hooks/useDataSource/types";
|
||||||
|
import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource";
|
||||||
|
import { useQueryKey } from "@/lib/hooks/useQueryKey";
|
||||||
|
import {
|
||||||
|
ICreateQuote_Request_DTO,
|
||||||
|
ICreateQuote_Response_DTO,
|
||||||
|
IGetQuote_Response_DTO,
|
||||||
|
UniqueID,
|
||||||
|
} from "@shared/contexts";
|
||||||
|
|
||||||
|
export type UseQuotesGetParamsType = {
|
||||||
|
enabled?: boolean;
|
||||||
|
queryOptions?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useQuotes = (params?: UseQuotesGetParamsType) => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const keys = useQueryKey();
|
||||||
|
|
||||||
|
return {
|
||||||
|
useQuery: useOne<IGetQuote_Response_DTO>({
|
||||||
|
queryKey: keys().data().resource("quotes").action("one").id("").params().get(),
|
||||||
|
queryFn: () =>
|
||||||
|
dataSource.getOne({
|
||||||
|
resource: "quotes",
|
||||||
|
id: "",
|
||||||
|
}),
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
useMutation: useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({
|
||||||
|
mutationKey: keys().data().resource("quotes").action("one").id("").params().get(),
|
||||||
|
mutationFn: (data) => {
|
||||||
|
let { id } = data;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
id = UniqueID.generateNewID().object.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource.updateOne({
|
||||||
|
resource: "quotes",
|
||||||
|
data,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
39
client/src/app/quotes/hooks/useQuotesList.tsx
Normal file
39
client/src/app/quotes/hooks/useQuotesList.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { UseListQueryResult, useList } from "@/lib/hooks/useDataSource";
|
||||||
|
import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource";
|
||||||
|
import { useQueryKey } from "@/lib/hooks/useQueryKey";
|
||||||
|
import { IListQuotes_Response_DTO, IListResponse_DTO } from "@shared/contexts";
|
||||||
|
|
||||||
|
export type UseQuotesListParams = {
|
||||||
|
pagination: {
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
};
|
||||||
|
searchTerm?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
queryOptions?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseQuotesListResponse = UseListQueryResult<
|
||||||
|
IListResponse_DTO<IListQuotes_Response_DTO>,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useQuotesList = (params: UseQuotesListParams): UseQuotesListResponse => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const keys = useQueryKey();
|
||||||
|
|
||||||
|
const { pagination, searchTerm = undefined, enabled = true, queryOptions } = params;
|
||||||
|
|
||||||
|
return useList({
|
||||||
|
queryKey: keys().data().resource("quotes").action("list").params(params).get(),
|
||||||
|
queryFn: () => {
|
||||||
|
return dataSource.getList({
|
||||||
|
resource: "quotes",
|
||||||
|
quickSearchTerm: searchTerm,
|
||||||
|
pagination,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
queryOptions,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./create";
|
||||||
export * from "./list";
|
export * from "./list";
|
||||||
|
|||||||
14
client/src/app/quotes/layout.tsx
Normal file
14
client/src/app/quotes/layout.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Layout, LayoutContent, LayoutHeader } from "@/components";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
import { QuotesProvider } from "./QuotesContext";
|
||||||
|
|
||||||
|
export const QuotesLayout = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<QuotesProvider>
|
||||||
|
<Layout>
|
||||||
|
<LayoutHeader />
|
||||||
|
<LayoutContent>{children}</LayoutContent>
|
||||||
|
</Layout>
|
||||||
|
</QuotesProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import { Layout, LayoutContent, LayoutHeader } from "@/components";
|
import { DataTableProvider } from "@/lib/hooks";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import { QuotesDataTable } from "./components";
|
||||||
|
|
||||||
export const QuotesList = () => {
|
export const QuotesList = () => (
|
||||||
return (
|
<DataTableProvider>
|
||||||
<Layout>
|
<div className='flex items-center'>
|
||||||
<LayoutHeader />
|
<h1 className='text-lg font-semibold md:text-2xl'>
|
||||||
<LayoutContent>
|
<Trans i18nKey='quotes.title' />
|
||||||
<div className='flex items-center'>
|
</h1>
|
||||||
<h1 className='text-lg font-semibold md:text-2xl'>Quotes</h1>
|
</div>
|
||||||
</div>
|
|
||||||
</LayoutContent>
|
<QuotesDataTable />
|
||||||
</Layout>
|
</DataTableProvider>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|||||||
8
client/src/app/quotes/useQuotesContext.tsx
Normal file
8
client/src/app/quotes/useQuotesContext.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { QuotesContext } from "./QuotesContext";
|
||||||
|
|
||||||
|
export const useQuotesContext = () => {
|
||||||
|
const context = useContext(QuotesContext);
|
||||||
|
if (context === null) throw new Error("useQuotes must be used within a QuotesProvider");
|
||||||
|
return context;
|
||||||
|
};
|
||||||
18
client/src/components/Buttons/CancelButton.tsx
Normal file
18
client/src/components/Buttons/CancelButton.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Button, ButtonProps } from "@/ui";
|
||||||
|
|
||||||
|
export interface CancelButtonProps extends ButtonProps {
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CancelButton = ({
|
||||||
|
label = "Cancelar",
|
||||||
|
...props
|
||||||
|
}: CancelButtonProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Button type="button" variant="secondary" {...props}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CancelButton.displayName = "CancelButton";
|
||||||
39
client/src/components/Buttons/CustomButton.tsx
Normal file
39
client/src/components/Buttons/CustomButton.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button, ButtonProps } from "@/ui";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { LucideIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const customButtonVariants = cva("", {
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
default: "w-4 h-4",
|
||||||
|
sm: "h-3.5 w-3.5",
|
||||||
|
lg: "h-6 w-6",
|
||||||
|
icon: "w-7 h-7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface CustomButtonProps
|
||||||
|
extends ButtonProps,
|
||||||
|
VariantProps<typeof customButtonVariants> {
|
||||||
|
icon: LucideIcon; // Propiedad para proporcionar el icono personalizado
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomButton = React.forwardRef<HTMLButtonElement, CustomButtonProps>(
|
||||||
|
({ className, label, size, icon: Icon, children, ...props }, ref) => (
|
||||||
|
<Button ref={ref} size={size} className={cn("gap-1", className)} {...props}>
|
||||||
|
<Icon className={cn(customButtonVariants({ size }))} />
|
||||||
|
<>{label ? label : children}</>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CustomButton.displayName = "CustomButton";
|
||||||
|
|
||||||
|
export { CustomButton, customButtonVariants };
|
||||||
13
client/src/components/Buttons/SubmitButton.tsx
Normal file
13
client/src/components/Buttons/SubmitButton.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ButtonProps } from "@/ui";
|
||||||
|
import { SaveIcon } from "lucide-react";
|
||||||
|
import { CustomButton } from "./CustomButton";
|
||||||
|
|
||||||
|
export interface SubmitButtonProps extends ButtonProps {
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubmitButton = ({ label = "Guardar", ...props }: SubmitButtonProps) => (
|
||||||
|
<CustomButton type='submit' label={label} icon={SaveIcon} variant='default' {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
SubmitButton.displayName = "SubmitButton";
|
||||||
2
client/src/components/Buttons/index.ts
Normal file
2
client/src/components/Buttons/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./CancelButton";
|
||||||
|
export * from "./SubmitButton";
|
||||||
@ -10,30 +10,30 @@ import {
|
|||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
import { CellContext } from "@tanstack/react-table";
|
import { CellContext, Row } from "@tanstack/react-table";
|
||||||
|
import { t } from "i18next";
|
||||||
import { MoreHorizontalIcon } from "lucide-react";
|
import { MoreHorizontalIcon } from "lucide-react";
|
||||||
|
|
||||||
type DataTableRowActionContext<TData, TValue> = CellContext<TData, TValue>;
|
type DataTableRowActionContext<TData, TValue> = CellContext<TData, TValue> & {
|
||||||
|
row: Row<TData>;
|
||||||
|
};
|
||||||
|
|
||||||
export type DataTablaRowActionFunction<TData, TValue> = (
|
export type DataTablaRowActionFunction<TData, TValue> = (
|
||||||
props: DataTableRowActionContext<TData, TValue>,
|
props: DataTableRowActionContext<TData, TValue>
|
||||||
) => DataTableRowActionDefinition<TData, TValue>[];
|
) => DataTableRowActionDefinition<TData, TValue>[];
|
||||||
|
|
||||||
export type DataTableRowActionDefinition<TData, TValue> = {
|
export type DataTableRowActionDefinition<TData, TValue> = {
|
||||||
label: string | "-";
|
label: string | "-";
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
onClick?: (
|
onClick?: (props: DataTableRowActionContext<TData, TValue>, e: React.BaseSyntheticEvent) => void;
|
||||||
props: DataTableRowActionContext<TData, TValue>,
|
|
||||||
e: React.BaseSyntheticEvent,
|
|
||||||
) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataTableRowActionsProps<TData, TValue> = {
|
export type DataTableRowActionsProps<TData, TValue> = {
|
||||||
props: DataTableRowActionContext<TData, TValue>;
|
|
||||||
actions?: DataTablaRowActionFunction<TData, TValue>;
|
actions?: DataTablaRowActionFunction<TData, TValue>;
|
||||||
|
row?: DataTableRowActionContext<TData, TValue>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DataTableRowActions<TData, TValue>({
|
export function DataTableRowActions<TData = any, TValue = any>({
|
||||||
actions,
|
actions,
|
||||||
...props
|
...props
|
||||||
}: DataTableRowActionsProps<TData, TValue>) {
|
}: DataTableRowActionsProps<TData, TValue>) {
|
||||||
@ -41,18 +41,18 @@ export function DataTableRowActions<TData, TValue>({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-haspopup="true"
|
aria-haspopup='true'
|
||||||
size="icon"
|
size='icon'
|
||||||
variant="link"
|
variant='link'
|
||||||
className="w-4 h-4 translate-y-[2px]"
|
className='w-4 h-4 translate-y-[2px]'
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="w-4 h-4" />
|
<MoreHorizontalIcon className='w-4 h-4' />
|
||||||
<span className="sr-only">Abrir menú</span>
|
<span className='sr-only'>{t("common.open_menu")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align='end'>
|
||||||
<DropdownMenuLabel>Acciones</DropdownMenuLabel>
|
<DropdownMenuLabel>{t("common.actions")} </DropdownMenuLabel>
|
||||||
{actions &&
|
{actions &&
|
||||||
actions(props).map((action, index) =>
|
actions(props).map((action, index) =>
|
||||||
action.label === "-" ? (
|
action.label === "-" ? (
|
||||||
@ -60,14 +60,12 @@ export function DataTableRowActions<TData, TValue>({
|
|||||||
) : (
|
) : (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={index}
|
key={index}
|
||||||
onClick={(event) =>
|
onClick={(event) => (action.onClick ? action.onClick(props, event) : null)}
|
||||||
action.onClick ? action.onClick(props, event) : null
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
<DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
|
<DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@ -9,29 +9,29 @@ export const SimpleEmptyState = ({
|
|||||||
actions = undefined,
|
actions = undefined,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className='text-center'>
|
||||||
<svg
|
<svg
|
||||||
className="w-12 h-12 mx-auto text-slate-400"
|
className='w-12 h-12 mx-auto text-slate-400'
|
||||||
fill="none"
|
fill='none'
|
||||||
viewBox="0 0 24 24"
|
viewBox='0 0 24 24'
|
||||||
stroke="currentColor"
|
stroke='currentColor'
|
||||||
aria-hidden="true"
|
aria-hidden='true'
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
vectorEffect="non-scaling-stroke"
|
vectorEffect='non-scaling-stroke'
|
||||||
strokeLinecap="round"
|
strokeLinecap='round'
|
||||||
strokeLinejoin="round"
|
strokeLinejoin='round'
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
d='M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z'
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 className="mt-2 text-sm font-semibold text-slate-900">{title}</h3>
|
<h3 className='mt-2 text-lg font-semibold text-slate-900'>{title}</h3>
|
||||||
<p className="mt-1 text-sm text-slate-500">{subtitle}</p>
|
<p className='mt-1 text-base text-slate-500'>{subtitle}</p>
|
||||||
|
|
||||||
<div className="items-center mt-6">
|
<div className='items-center mt-6'>
|
||||||
{actions && <>{actions}</>}
|
{actions && <>{actions}</>}
|
||||||
{!actions && (
|
{!actions && (
|
||||||
<Button className="my-4" onClick={onButtonClick}>
|
<Button className='my-4' onClick={onButtonClick}>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
126
client/src/components/Forms/FormDatePickerField.tsx
Normal file
126
client/src/components/Forms/FormDatePickerField.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { FieldPath, FieldValues, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
// ShadCn
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Calendar,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
InputProps,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/ui";
|
||||||
|
|
||||||
|
import { UseControllerProps } from "react-hook-form";
|
||||||
|
|
||||||
|
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||||||
|
import { FormInputProps } from "./FormProps";
|
||||||
|
|
||||||
|
import { CalendarIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
|
type FormDatePickerFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
> = InputProps &
|
||||||
|
FormInputProps &
|
||||||
|
Partial<FormLabelProps> &
|
||||||
|
UseControllerProps<TFieldValues, TName> & {};
|
||||||
|
|
||||||
|
/*const loadDateFnsLocale = async (locale: Locale) => {
|
||||||
|
return await import(`date-fns/locale/${locale.code}/index.js`);
|
||||||
|
};*/
|
||||||
|
|
||||||
|
export const FormDatePickerField = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & FormDatePickerFieldProps
|
||||||
|
>((props: FormDatePickerFieldProps, ref) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
hint,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
errors,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
} = props;
|
||||||
|
const { control } = useFormContext();
|
||||||
|
//const { locale } = loadDateFnsLocale();
|
||||||
|
|
||||||
|
/*if (locale) {
|
||||||
|
setDefaultOptions({ locale: locale.default });
|
||||||
|
}*/
|
||||||
|
|
||||||
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
rules={{ required }}
|
||||||
|
disabled={disabled}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
render={({ field, fieldState, formState }) => (
|
||||||
|
<FormItem ref={ref} className={cn(className, "flex flex-col")}>
|
||||||
|
{label && <FormLabel label={label} hint={hint} />}
|
||||||
|
|
||||||
|
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
className={cn(
|
||||||
|
"pl-3 text-left font-normal",
|
||||||
|
!field.value && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value ? (
|
||||||
|
new Date(field.value).toLocaleDateString() //"en-US", DATE_OPTIONS)
|
||||||
|
) : (
|
||||||
|
<span>{t("common.pick_date")}</span>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className='w-4 h-4 ml-auto text-input' />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className='w-auto p-0' align='start'>
|
||||||
|
<Calendar
|
||||||
|
className='bg-background'
|
||||||
|
mode='single'
|
||||||
|
captionLayout='buttons'
|
||||||
|
defaultMonth={field.value}
|
||||||
|
selected={new Date(field.value)}
|
||||||
|
onSelect={(e) => {
|
||||||
|
field.onChange(e);
|
||||||
|
setIsPopoverOpen(false);
|
||||||
|
}}
|
||||||
|
disabled={(date) => date < new Date("2024-06-01")}
|
||||||
|
weekStartsOn={1}
|
||||||
|
fixedWeeks={true}
|
||||||
|
fromYear={2024}
|
||||||
|
toYear={new Date().getFullYear() + 1}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{description && <FormDescription>{description}</FormDescription>}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -15,10 +15,11 @@ export const FormLabel = React.forwardRef<
|
|||||||
Pick<FormInputProps, "required">
|
Pick<FormInputProps, "required">
|
||||||
>(({ label, hint, required, ...props }, ref) => {
|
>(({ label, hint, required, ...props }, ref) => {
|
||||||
const _hint = hint ? hint : required ? "obligatorio" : undefined;
|
const _hint = hint ? hint : required ? "obligatorio" : undefined;
|
||||||
|
const _hintClassName = required ? "text-destructive" : "";
|
||||||
return (
|
return (
|
||||||
<UI.FormLabel ref={ref} className="flex justify-between" {...props}>
|
<UI.FormLabel ref={ref} className='flex justify-between text-sm' {...props}>
|
||||||
<span className="block font-semibold">{label}</span>
|
<span className='block font-semibold'>{label}</span>
|
||||||
{_hint && <span className="font-normal">{_hint}</span>}
|
{_hint && <span className={`text-sm font-medium ${_hintClassName}`}>{_hint}</span>}
|
||||||
</UI.FormLabel>
|
</UI.FormLabel>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,7 +10,13 @@ import {
|
|||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { FieldErrors, FieldPath, FieldValues, UseControllerProps } from "react-hook-form";
|
import {
|
||||||
|
FieldErrors,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
UseControllerProps,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import { FormLabel, FormLabelProps } from "./FormLabel";
|
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||||||
|
|
||||||
export type FormTextAreaFieldProps<
|
export type FormTextAreaFieldProps<
|
||||||
@ -29,36 +35,50 @@ export type FormTextAreaFieldProps<
|
|||||||
export const FormTextAreaField = React.forwardRef<
|
export const FormTextAreaField = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.TextareaHTMLAttributes<HTMLTextAreaElement> & FormTextAreaFieldProps
|
React.TextareaHTMLAttributes<HTMLTextAreaElement> & FormTextAreaFieldProps
|
||||||
>(({ label, hint, placeholder, description, autoSize, className, ...props }, ref) => {
|
>(
|
||||||
return (
|
(
|
||||||
<FormField
|
{
|
||||||
control={props.control}
|
name,
|
||||||
name={props.name}
|
label,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
hint,
|
||||||
render={({ field, fieldState, formState }) => (
|
description,
|
||||||
<FormItem ref={ref} className={cn(className, "flex flex-col")}>
|
placeholder,
|
||||||
{label && <FormLabel label={label} hint={hint} />}
|
required,
|
||||||
<FormControl>
|
disabled,
|
||||||
{autoSize ? (
|
autoSize,
|
||||||
<AutosizeTextarea
|
className,
|
||||||
disabled={props.disabled}
|
...props
|
||||||
placeholder={placeholder}
|
},
|
||||||
className='resize-y'
|
ref
|
||||||
{...field}
|
) => {
|
||||||
/>
|
const { control } = useFormContext();
|
||||||
) : (
|
return (
|
||||||
<Textarea
|
<FormField
|
||||||
disabled={props.disabled}
|
control={control}
|
||||||
placeholder={placeholder}
|
name={name}
|
||||||
className='resize-y'
|
rules={{ required }}
|
||||||
{...field}
|
disabled={disabled}
|
||||||
/>
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
)}
|
render={({ field, fieldState, formState }) => (
|
||||||
</FormControl>
|
<FormItem ref={ref} className={cn(className, "flex flex-col space-y-3")}>
|
||||||
{description && <FormDescription>{description}</FormDescription>}
|
{label && <FormLabel label={label} hint={hint} required={required} />}
|
||||||
<FormMessage />
|
<FormControl className='grow'>
|
||||||
</FormItem>
|
{autoSize ? (
|
||||||
)}
|
<AutosizeTextarea
|
||||||
/>
|
{...field}
|
||||||
);
|
placeholder={placeholder}
|
||||||
});
|
className='resize-y'
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Textarea {...field} placeholder={placeholder} className='resize-y' {...props} />
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
{description && <FormDescription>{description}</FormDescription>}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@ -11,7 +11,13 @@ import {
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { createElement } from "react";
|
import { createElement } from "react";
|
||||||
import { FieldErrors, FieldPath, FieldValues, UseControllerProps } from "react-hook-form";
|
import {
|
||||||
|
FieldErrors,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
UseControllerProps,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
import { FormLabel, FormLabelProps } from "./FormLabel";
|
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||||||
import { FormInputProps, FormInputWithIconProps } from "./FormProps";
|
import { FormInputProps, FormInputWithIconProps } from "./FormProps";
|
||||||
|
|
||||||
@ -46,10 +52,11 @@ export const FormTextField = React.forwardRef<
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
errors,
|
errors,
|
||||||
name,
|
name,
|
||||||
control,
|
|
||||||
type,
|
type,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
@ -59,7 +66,7 @@ export const FormTextField = React.forwardRef<
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
render={({ field, fieldState, formState }) => (
|
render={({ field, fieldState, formState }) => (
|
||||||
<FormItem ref={ref} className={cn(className, "space-y-3")}>
|
<FormItem ref={ref} className={cn(className, "space-y-3")}>
|
||||||
{label && <FormLabel label={label} hint={hint} />}
|
{label && <FormLabel label={label} hint={hint} required={required} />}
|
||||||
<div className={cn(button ? "flex" : null)}>
|
<div className={cn(button ? "flex" : null)}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./FormDatePickerField";
|
||||||
export * from "./FormGroup";
|
export * from "./FormGroup";
|
||||||
export * from "./FormLabel";
|
export * from "./FormLabel";
|
||||||
export * from "./FormMoneyField";
|
export * from "./FormMoneyField";
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { UserButton } from "./components/UserButton";
|
|||||||
|
|
||||||
export const LayoutHeader = () => {
|
export const LayoutHeader = () => {
|
||||||
return (
|
return (
|
||||||
<header className='sticky top-0 z-10 flex items-center h-16 gap-8 px-4 border-b bg-background md:px-6'>
|
<header className='sticky top-0 z-10 flex items-center h-16 gap-8 px-4 border-b bg-primary md:px-6'>
|
||||||
<nav className='flex-col hidden gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6'>
|
<nav className='flex-col hidden gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6'>
|
||||||
<Link to='/' className='flex items-center font-semibold'>
|
<Link to='/' className='flex items-center font-semibold'>
|
||||||
<UeckoLogo className='w-24' />
|
<UeckoLogo className='w-24' />
|
||||||
@ -78,7 +78,7 @@ export const LayoutHeader = () => {
|
|||||||
<Input
|
<Input
|
||||||
type='search'
|
type='search'
|
||||||
placeholder={t("main_menu.search_placeholder")}
|
placeholder={t("main_menu.search_placeholder")}
|
||||||
className='pl-8 sm:w-[300px] md:w-[200px] lg:w-[350px] xl:w-[550px] 2xl:w-[750px]'
|
className='pl-8 sm:w-[300px] md:w-[200px] lg:w-[300px] xl:w-[550px] 2xl:w-[750px]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -33,7 +33,6 @@ export const UserButton = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { data, status } = useGetIdentity();
|
const { data, status } = useGetIdentity();
|
||||||
|
|
||||||
console.log(data, status);
|
console.log(data, status);
|
||||||
|
|
||||||
const openUserMenu = (event: SyntheticEvent) => {
|
const openUserMenu = (event: SyntheticEvent) => {
|
||||||
@ -41,57 +40,53 @@ export const UserButton = () => {
|
|||||||
setUserMenuOpened(true);
|
setUserMenuOpened(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*const closeUserMenu = (event: SyntheticEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setUserMenuOpened(false);
|
|
||||||
};*/
|
|
||||||
|
|
||||||
/*if (status !== "success") {
|
|
||||||
return <></>;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu open={userMenuOpened} onOpenChange={setUserMenuOpened}>
|
<>
|
||||||
<DropdownMenuTrigger asChild>
|
{status === "success" && (
|
||||||
<>
|
<div className='grid gap-1 text-right'>
|
||||||
{status === "success" && (
|
<p className='text-xs font-medium leading-none'>{data?.name}</p>
|
||||||
<div className='grid gap-1 text-right'>
|
<p className='text-xs text-muted-foreground'>{data?.email}</p>
|
||||||
<p className='text-sm font-medium leading-none'>{data?.name}</p>
|
</div>
|
||||||
<p className='text-sm text-muted-foreground'>{data?.email}</p>
|
)}
|
||||||
</div>
|
<DropdownMenu open={userMenuOpened} onOpenChange={setUserMenuOpened}>
|
||||||
)}
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant='secondary' size='icon' className='rounded-full' onClick={openUserMenu}>
|
<Button
|
||||||
<CircleUserIcon className='w-5 h-5' />
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
className='overflow-hidden rounded-full'
|
||||||
|
onClick={openUserMenu}
|
||||||
|
>
|
||||||
|
<CircleUserIcon className='w-5 h-5 accent-current' />
|
||||||
<span className='sr-only'>{t("main_menu.user.user_menu")}</span>
|
<span className='sr-only'>{t("main_menu.user.user_menu")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent align='end' className='w-56'>
|
||||||
<DropdownMenuContent align='end' className='w-56'>
|
<DropdownMenuLabel>{t("main_menu.user.my_account")}</DropdownMenuLabel>
|
||||||
<DropdownMenuLabel>{t("main_menu.user.my_account")}</DropdownMenuLabel>
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<UserIcon className='w-4 h-4 mr-2' />
|
||||||
<UserIcon className='w-4 h-4 mr-2' />
|
<span>{t("main_menu.user.profile")}</span>
|
||||||
<span>{t("main_menu.user.profile")}</span>
|
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<SettingsIcon className='w-4 h-4 mr-2' />
|
||||||
|
<span>{t("main_menu.user.settings")}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<MessageCircleQuestionIcon className='w-4 h-4 mr-2' />
|
||||||
|
<span>{t("main_menu.user.support")}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onSelect={() => openLogoutDialog()}>
|
||||||
|
<LogOutIcon className='w-4 h-4 mr-2' />
|
||||||
|
<span>{t("main_menu.user.logout")}</span>
|
||||||
|
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
</DropdownMenuContent>
|
||||||
<SettingsIcon className='w-4 h-4 mr-2' />
|
{LogoutDialog}
|
||||||
<span>{t("main_menu.user.settings")}</span>
|
</DropdownMenu>
|
||||||
</DropdownMenuItem>
|
</>
|
||||||
<DropdownMenuItem>
|
|
||||||
<MessageCircleQuestionIcon className='w-4 h-4 mr-2' />
|
|
||||||
<span>{t("main_menu.user.support")}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onSelect={() => openLogoutDialog()}>
|
|
||||||
<LogOutIcon className='w-4 h-4 mr-2' />
|
|
||||||
<span>{t("main_menu.user.logout")}</span>
|
|
||||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
{LogoutDialog}
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./ButtonGroup";
|
export * from "./ButtonGroup";
|
||||||
|
export * from "./Buttons";
|
||||||
export * from "./Container";
|
export * from "./Container";
|
||||||
export * from "./CustomButtons";
|
export * from "./CustomButtons";
|
||||||
export * from "./CustomDialog";
|
export * from "./CustomDialog";
|
||||||
|
|||||||
@ -2,51 +2,51 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* https://ui.jln.dev/ Shivering Harpy Brown */
|
/* https://ui.jln.dev/ Teriyaki */
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 30 65% 100%;
|
--background: 25 31% 100%;
|
||||||
--foreground: 30 75% 0%;
|
--foreground: 25 67% 4%;
|
||||||
--muted: 30 17% 95%;
|
--muted: 25 30% 95%;
|
||||||
--muted-foreground: 30 15% 26%;
|
--muted-foreground: 25 2% 29%;
|
||||||
--popover: 0 0% 99%;
|
--popover: 25 70% 98%;
|
||||||
--popover-foreground: 0 0% 0%;
|
--popover-foreground: 25 67% 4%;
|
||||||
--card: 0 0% 99%;
|
--card: 25 70% 98%;
|
||||||
--card-foreground: 0 0% 0%;
|
--card-foreground: 25 67% 4%;
|
||||||
--border: 30 4% 91%;
|
--border: 220 13% 91%;
|
||||||
--input: 30 4% 91%;
|
--input: 25 31% 75%;
|
||||||
--primary: 30 26% 23%;
|
--primary: 25 31% 75%;
|
||||||
--primary-foreground: 30 26% 83%;
|
--primary-foreground: 25 31% 15%;
|
||||||
--secondary: 30 12% 82%;
|
--secondary: 25 18% 90%;
|
||||||
--secondary-foreground: 30 12% 22%;
|
--secondary-foreground: 25 18% 30%;
|
||||||
--accent: 30 22% 76%;
|
--accent: 25 23% 83%;
|
||||||
--accent-foreground: 30 22% 16%;
|
--accent-foreground: 25 23% 23%;
|
||||||
--destructive: 18 91% 33%;
|
--destructive: 13 96% 20%;
|
||||||
--destructive-foreground: 0 0% 100%;
|
--destructive-foreground: 13 96% 80%;
|
||||||
--ring: 30 26% 23%;
|
--ring: 25 31% 75%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 30 53% 1%;
|
--background: 25 41% 2%;
|
||||||
--foreground: 30 11% 98%;
|
--foreground: 25 21% 98%;
|
||||||
--muted: 30 17% 5%;
|
--muted: 25 30% 5%;
|
||||||
--muted-foreground: 30 15% 74%;
|
--muted-foreground: 25 2% 71%;
|
||||||
--popover: 30 53% 2%;
|
--popover: 25 41% 2%;
|
||||||
--popover-foreground: 30 11% 99%;
|
--popover-foreground: 25 21% 98%;
|
||||||
--card: 30 53% 2%;
|
--card: 25 41% 2%;
|
||||||
--card-foreground: 30 11% 99%;
|
--card-foreground: 25 21% 98%;
|
||||||
--border: 30 4% 14%;
|
--border: 215 27.9% 16.9%;
|
||||||
--input: 30 4% 14%;
|
--input: 215 27.9% 16.9%;
|
||||||
--primary: 30 26% 23%;
|
--primary: 25 31% 75%;
|
||||||
--primary-foreground: 30 26% 83%;
|
--primary-foreground: 25 31% 15%;
|
||||||
--secondary: 30 7% 15%;
|
--secondary: 25 5% 14%;
|
||||||
--secondary-foreground: 30 7% 75%;
|
--secondary-foreground: 25 5% 74%;
|
||||||
--accent: 30 16% 24%;
|
--accent: 25 11% 20%;
|
||||||
--accent-foreground: 30 16% 84%;
|
--accent-foreground: 25 11% 80%;
|
||||||
--destructive: 18 91% 59%;
|
--destructive: 13 96% 49%;
|
||||||
--destructive-foreground: 0 0% 0%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
--ring: 30 26% 23%;
|
--ring: 25 31% 75%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export const createAxiosAuthActions = (
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data,
|
data,
|
||||||
redirectTo: "/",
|
redirectTo: "/home",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
@ -60,6 +60,11 @@ export const createAxiosAuthActions = (
|
|||||||
},
|
},
|
||||||
|
|
||||||
getIdentity: async () => {
|
getIdentity: async () => {
|
||||||
|
const errorResult = {
|
||||||
|
message: "Identification failed",
|
||||||
|
name: "Invalid profile or identification",
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await httpClient.request<IIdentity_Response_DTO>({
|
const result = await httpClient.request<IIdentity_Response_DTO>({
|
||||||
url: `${apiUrl}/auth/identity`,
|
url: `${apiUrl}/auth/identity`,
|
||||||
@ -71,11 +76,11 @@ export const createAxiosAuthActions = (
|
|||||||
|
|
||||||
if (profile?.id === data?.id) {
|
if (profile?.id === data?.id) {
|
||||||
secureLocalStorage.setItem("uecko.profile", data);
|
secureLocalStorage.setItem("uecko.profile", data);
|
||||||
return data;
|
return Promise.resolve(data);
|
||||||
}
|
}
|
||||||
return undefined;
|
return Promise.reject(errorResult);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return undefined;
|
return Promise.reject(errorResult);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,7 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
|
|||||||
break;
|
break;
|
||||||
case 401:
|
case 401:
|
||||||
console.error("UnAuthorized");
|
console.error("UnAuthorized");
|
||||||
|
//return (window.location.href = "/logout");
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
console.error("Forbidden");
|
console.error("Forbidden");
|
||||||
|
|||||||
@ -14,5 +14,6 @@ export * from "./useUrlId";
|
|||||||
export * from "./useAuth";
|
export * from "./useAuth";
|
||||||
export * from "./useCustomDialog";
|
export * from "./useCustomDialog";
|
||||||
export * from "./useDataTable";
|
export * from "./useDataTable";
|
||||||
|
export * from "./useLocalization";
|
||||||
export * from "./usePagination";
|
export * from "./usePagination";
|
||||||
export * from "./useTheme";
|
export * from "./useTheme";
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { IIdentity_Response_DTO } from "@shared/contexts";
|
||||||
|
|
||||||
export type SuccessNotificationResponse = {
|
export type SuccessNotificationResponse = {
|
||||||
message: string;
|
message: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -5,7 +7,7 @@ export type SuccessNotificationResponse = {
|
|||||||
|
|
||||||
export type PermissionResponse = unknown;
|
export type PermissionResponse = unknown;
|
||||||
|
|
||||||
export type IdentityResponse = unknown;
|
export type IdentityResponse = IIdentity_Response_DTO;
|
||||||
|
|
||||||
export type AuthActionCheckResponse = {
|
export type AuthActionCheckResponse = {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
@ -31,7 +33,7 @@ export type AuthActionResponse = {
|
|||||||
export interface IAuthActions {
|
export interface IAuthActions {
|
||||||
login: (params: any) => Promise<AuthActionResponse>;
|
login: (params: any) => Promise<AuthActionResponse>;
|
||||||
logout: (params: any) => Promise<AuthActionResponse>;
|
logout: (params: any) => Promise<AuthActionResponse>;
|
||||||
check: (params?: any) => Promise<AuthActionCheckResponse>;
|
check: () => Promise<AuthActionCheckResponse>;
|
||||||
onError?: (error: any) => Promise<AuthActionOnErrorResponse>;
|
onError?: (error: any) => Promise<AuthActionOnErrorResponse>;
|
||||||
register?: (params: unknown) => Promise<AuthActionResponse>;
|
register?: (params: unknown) => Promise<AuthActionResponse>;
|
||||||
forgotPassword?: (params: unknown) => Promise<AuthActionResponse>;
|
forgotPassword?: (params: unknown) => Promise<AuthActionResponse>;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { PropsWithChildren, createContext } from "react";
|
import { PropsWithChildren, createContext } from "react";
|
||||||
import { IAuthActions } from "./AuthActions";
|
import { IAuthActions } from "./AuthActions";
|
||||||
|
|
||||||
export interface IAuthContextState extends IAuthActions {}
|
export interface IAuthContextState extends Partial<IAuthActions> {}
|
||||||
|
|
||||||
export const AuthContext = createContext<IAuthContextState | null>(null);
|
export const AuthContext = createContext<Partial<IAuthContextState>>({});
|
||||||
|
|
||||||
export const AuthProvider = ({
|
export const AuthProvider = ({
|
||||||
children,
|
children,
|
||||||
@ -27,9 +27,9 @@ export const AuthProvider = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheck = async (params: unknown) => {
|
const handleCheck = async () => {
|
||||||
try {
|
try {
|
||||||
return Promise.resolve(authActions.check?.(params));
|
return Promise.resolve(authActions.check?.());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|||||||
@ -2,15 +2,15 @@ import { IdentityResponse, useAuth } from "@/lib/hooks";
|
|||||||
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
|
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||||
import { useQueryKey } from "../useQueryKey";
|
import { useQueryKey } from "../useQueryKey";
|
||||||
|
|
||||||
export const useGetIdentity = (queryOptions?: UseQueryOptions) => {
|
export const useGetIdentity = (queryOptions?: UseQueryOptions<IdentityResponse>) => {
|
||||||
const keys = useQueryKey();
|
const keys = useQueryKey();
|
||||||
const { getIdentity } = useAuth();
|
const { getIdentity } = useAuth();
|
||||||
|
|
||||||
return useQuery<IdentityResponse, Error>({
|
const result = useQuery<IdentityResponse>({
|
||||||
queryKey: keys().auth().action("identity").get(),
|
queryKey: keys().auth().action("identity").get(),
|
||||||
queryFn: getIdentity,
|
queryFn: getIdentity,
|
||||||
...queryOptions,
|
...queryOptions,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const isAdmin = () => true;
|
return result;
|
||||||
|
};
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { useAuth } from "@/lib/hooks";
|
import { AuthActionCheckResponse, useAuth } from "@/lib/hooks";
|
||||||
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
|
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||||
import { useQueryKey } from "../useQueryKey";
|
import { useQueryKey } from "../useQueryKey";
|
||||||
|
|
||||||
export const useIsLoggedIn = (queryOptions?: UseQueryOptions) => {
|
export const useIsLoggedIn = (queryOptions?: UseQueryOptions<AuthActionCheckResponse>) => {
|
||||||
const keys = useQueryKey();
|
const keys = useQueryKey();
|
||||||
const { check } = useAuth();
|
const { check } = useAuth();
|
||||||
|
|
||||||
return useQuery({
|
const result = useQuery<AuthActionCheckResponse>({
|
||||||
queryKey: keys().auth().action("check").get(),
|
queryKey: keys().auth().action("check").get(),
|
||||||
queryFn: check,
|
queryFn: check,
|
||||||
...queryOptions,
|
...queryOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,10 +14,10 @@ export const useLogin = (params?: UseMutationOptions<AuthActionResponse, Error,
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: keys().auth().action("login").get(),
|
mutationKey: keys().auth().action("login").get(),
|
||||||
mutationFn: login,
|
mutationFn: login,
|
||||||
onSuccess: async (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
const { success, redirectTo } = data;
|
const { success, redirectTo } = data;
|
||||||
if (success && redirectTo) {
|
if (success && redirectTo) {
|
||||||
navigate(redirectTo);
|
navigate(redirectTo, { replace: true });
|
||||||
}
|
}
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess(data, variables, context);
|
onSuccess(data, variables, context);
|
||||||
|
|||||||
20
client/src/lib/hooks/useDataSource/types.ts
Normal file
20
client/src/lib/hooks/useDataSource/types.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { DefaultError } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export type TDataSourceRecords = {
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
total_pages: number;
|
||||||
|
total_items: number;
|
||||||
|
items: TDataSourceRecord[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDataSourceRecord = {
|
||||||
|
id?: string | number;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDataSourceError = DefaultError;
|
||||||
|
|
||||||
|
export type TDataSourceVariables = Record<string, any>;
|
||||||
|
|
||||||
|
export type TDataSourceContext = unknown;
|
||||||
@ -1,49 +1,14 @@
|
|||||||
import {
|
import { UseQueryOptions, UseQueryResult, keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||||
QueryFunctionContext,
|
|
||||||
QueryKey,
|
|
||||||
UseQueryResult,
|
|
||||||
keepPreviousData,
|
|
||||||
useQuery,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import {
|
import { TDataSourceError, TDataSourceRecord } from "./types";
|
||||||
UseLoadingOvertimeOptionsProps,
|
|
||||||
UseLoadingOvertimeReturnType,
|
|
||||||
} from "../useLoadingOvertime/useLoadingOvertime";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
export function useOne<
|
||||||
export type UseOneQueryOptions<TUseOneQueryData, TUseOneQueryError> = {
|
TQueryFnData extends TDataSourceRecord = TDataSourceRecord,
|
||||||
queryKey: QueryKey;
|
TError = TDataSourceError,
|
||||||
queryFn: (context: QueryFunctionContext) => Promise<TUseOneQueryData>;
|
TData extends TDataSourceRecord = TQueryFnData
|
||||||
enabled?: boolean;
|
>(options: UseQueryOptions<TQueryFnData, TError, TData>): UseQueryResult<TData, TError> {
|
||||||
autoRefresh?: boolean;
|
return useQuery<TQueryFnData, TError, TData>({
|
||||||
select?: (data: TUseOneQueryData) => TUseOneQueryData;
|
|
||||||
queryOptions?: Record<string, unknown>;
|
|
||||||
} & UseLoadingOvertimeOptionsProps;
|
|
||||||
|
|
||||||
export type UseOneQueryResult<TUseOneQueryData, TUseOneQueryError> = UseQueryResult<
|
|
||||||
TUseOneQueryData,
|
|
||||||
TUseOneQueryError
|
|
||||||
> & {
|
|
||||||
isEmpty: boolean;
|
|
||||||
} & UseLoadingOvertimeReturnType;
|
|
||||||
|
|
||||||
export function useOne<TUseOneQueryData, TUseOneQueryError = Error>({
|
|
||||||
queryKey,
|
|
||||||
queryFn,
|
|
||||||
enabled,
|
|
||||||
select,
|
|
||||||
queryOptions = {},
|
|
||||||
}: UseOneQueryOptions<TUseOneQueryData, TUseOneQueryError>): UseQueryResult<
|
|
||||||
TUseOneQueryData,
|
|
||||||
TUseOneQueryError
|
|
||||||
> {
|
|
||||||
return useQuery<TUseOneQueryData, TUseOneQueryError>({
|
|
||||||
queryKey,
|
|
||||||
queryFn,
|
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
enabled,
|
...options,
|
||||||
select,
|
|
||||||
...queryOptions,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,43 +1,18 @@
|
|||||||
|
import { UseMutationOptions, useMutation } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
DefaultError,
|
TDataSourceContext,
|
||||||
UseMutationOptions,
|
TDataSourceError,
|
||||||
useMutation,
|
TDataSourceRecord,
|
||||||
} from "@tanstack/react-query";
|
TDataSourceVariables,
|
||||||
|
} from "./types";
|
||||||
export interface IUseSaveMutationOptions<
|
|
||||||
TUseSaveMutationData = unknown,
|
|
||||||
TUseSaveMutationError = DefaultError,
|
|
||||||
TUseSaveMutationVariables = unknown,
|
|
||||||
TUseSaveMutationContext = unknown,
|
|
||||||
> extends UseMutationOptions<
|
|
||||||
TUseSaveMutationData,
|
|
||||||
TUseSaveMutationError,
|
|
||||||
TUseSaveMutationVariables,
|
|
||||||
TUseSaveMutationContext
|
|
||||||
> {}
|
|
||||||
|
|
||||||
export function useSave<
|
export function useSave<
|
||||||
TUseSaveMutationData = unknown,
|
TData extends TDataSourceRecord = TDataSourceRecord,
|
||||||
TUseSaveMutationError = DefaultError,
|
TError extends TDataSourceError = TDataSourceError,
|
||||||
TUseSaveMutationVariables = unknown,
|
TVariables extends TDataSourceVariables = TDataSourceVariables,
|
||||||
TUseSaveMutationContext = unknown,
|
TContext extends TDataSourceContext = TDataSourceContext
|
||||||
>(
|
>(options: UseMutationOptions<TData, TError, TVariables, TContext>) {
|
||||||
options: IUseSaveMutationOptions<
|
return useMutation<TData, TError, TVariables, TContext>({
|
||||||
TUseSaveMutationData,
|
...options,
|
||||||
TUseSaveMutationError,
|
|
||||||
TUseSaveMutationVariables,
|
|
||||||
TUseSaveMutationContext
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
const { mutationFn, ...params } = options;
|
|
||||||
|
|
||||||
return useMutation<
|
|
||||||
TUseSaveMutationData,
|
|
||||||
TUseSaveMutationError,
|
|
||||||
TUseSaveMutationVariables,
|
|
||||||
TUseSaveMutationContext
|
|
||||||
>({
|
|
||||||
mutationFn,
|
|
||||||
...params,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
1
client/src/lib/hooks/useLocalization/index.ts
Normal file
1
client/src/lib/hooks/useLocalization/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./useLocatlization";
|
||||||
53
client/src/lib/hooks/useLocalization/useLocatlization.tsx
Normal file
53
client/src/lib/hooks/useLocalization/useLocatlization.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */
|
||||||
|
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { LocaleToCurrencyTable, rtlLangsList } from "./utils";
|
||||||
|
|
||||||
|
type UseLocalizationProps = {
|
||||||
|
locale: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLocalization = (props: UseLocalizationProps) => {
|
||||||
|
const { locale } = props;
|
||||||
|
const [lang, loc] = locale.split("-");
|
||||||
|
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
// Obtener el idioma actual
|
||||||
|
const currentLanguage = i18n.language;
|
||||||
|
|
||||||
|
const formatCurrency = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
return new Intl.NumberFormat(locale, {
|
||||||
|
style: "currency",
|
||||||
|
currency: LocaleToCurrencyTable[locale],
|
||||||
|
}).format(value);
|
||||||
|
},
|
||||||
|
[locale]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatNumber = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
return new Intl.NumberFormat(locale).format(value);
|
||||||
|
},
|
||||||
|
[locale]
|
||||||
|
);
|
||||||
|
|
||||||
|
const flag = useMemo(
|
||||||
|
() =>
|
||||||
|
typeof String.fromCodePoint !== "undefined"
|
||||||
|
? loc
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397))
|
||||||
|
: "",
|
||||||
|
[loc]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatCurrency,
|
||||||
|
formatNumber,
|
||||||
|
flag,
|
||||||
|
isRTL: rtlLangsList.includes(lang),
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { useGetIdentity } from "../useAuth";
|
||||||
|
|
||||||
|
export const usePreferredLanguage = () => {
|
||||||
|
const { data } = useGetIdentity();
|
||||||
|
|
||||||
|
if (!data || !data.language) {
|
||||||
|
return navigator.languages && navigator.languages.length
|
||||||
|
? navigator.languages[0]
|
||||||
|
: navigator.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.language;
|
||||||
|
};
|
||||||
110
client/src/lib/hooks/useLocalization/utils.ts
Normal file
110
client/src/lib/hooks/useLocalization/utils.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
export const LocaleToCurrencyTable: { [key: string]: string } = {
|
||||||
|
"sq-AL": "ALL", // ALBANIA
|
||||||
|
"ar-DZ": "DZD", // ALGERIA
|
||||||
|
"ar-BH": "BHD", // BAHRAIN
|
||||||
|
"ar-EG": "EGP", // EGYPT
|
||||||
|
"ar-IQ": "IQD", // IRAQ
|
||||||
|
"ar-JO": "JOD", // JORDAN
|
||||||
|
"ar-KW": "KWD", // KUWAIT
|
||||||
|
"ar-LB": "LBP", // LEBANON
|
||||||
|
"ar-LY": "LYD", // LIBYA
|
||||||
|
"ar-MA": "MAD", // MOROCCO
|
||||||
|
"ar-OM": "OMR", // OMAN
|
||||||
|
"ar-QA": "QAR", // QATAR
|
||||||
|
"ar-SA": "SAR", // SAUDI ARABIA
|
||||||
|
"ar-SD": "SDG", // SUDAN
|
||||||
|
"ar-SY": "SYP", // SYRIA
|
||||||
|
"ar-TN": "TND", // TUNISIA
|
||||||
|
"ar-AE": "AED", // UAE
|
||||||
|
"ar-YE": "YER", // YEMEN
|
||||||
|
"be-BY": "BYN", // BELARUS
|
||||||
|
"bg-BG": "BGN", // BULGARIA
|
||||||
|
"ca-ES": "EUR", // SPAIN
|
||||||
|
"zh-CN": "CNY", // CHINA
|
||||||
|
"zh-HK": "HKD", // HONG KONG
|
||||||
|
"zh-SG": "SGD", // SINGAPORE
|
||||||
|
"zh-TW": "TWD", // TAIWAN
|
||||||
|
"hr-HR": "HRK", // CROATIA
|
||||||
|
"cs-CZ": "CZK", // CZECH REPUBLIC
|
||||||
|
"da-DK": "DKK", // DENMARK
|
||||||
|
"nl-BE": "EUR", // BELIGIUM
|
||||||
|
"nl-NL": "EUR", // NETHERLANDS
|
||||||
|
"en-AU": "AUD", // AUSTRALIA
|
||||||
|
"en-CA": "CAD", // CANADA
|
||||||
|
"en-IN": "INR", // INDIA
|
||||||
|
"en-IE": "EUR", // IRELAND
|
||||||
|
"en-MT": "EUR", // MALTA
|
||||||
|
"en-NZ": "NZD", // NEW ZEALAND
|
||||||
|
"en-PH": "PHP", // PHILIPPINES
|
||||||
|
"en-SG": "SGD", // SINGAPORE
|
||||||
|
"en-ZA": "ZAR", // SOUTH AFRICA
|
||||||
|
"en-GB": "GBP", // UNITED KINGDOM
|
||||||
|
"en-US": "USD", // US
|
||||||
|
"et-EE": "EUR", // ESTONIA
|
||||||
|
"fi-FI": "EUR", // FINLAND
|
||||||
|
"fr-BE": "EUR", // BELGIUM
|
||||||
|
"fr-CA": "CAD", // CANADA
|
||||||
|
"fr-FR": "EUR", // FRANCE
|
||||||
|
"fr-LU": "EUR", // LUXEMBOURG
|
||||||
|
"fr-CH": "CHF", // SWITZERLAND
|
||||||
|
"de-AT": "EUR", // AUSTRIA
|
||||||
|
"de-DE": "EUR", // GERMANY
|
||||||
|
"de-LU": "EUR", // LUXEMBOURG
|
||||||
|
"de-CH": "CHF", // SWITZERLAND
|
||||||
|
"el-CY": "EUR", // CYPRUS
|
||||||
|
"el-GR": "EUR", // GREECE
|
||||||
|
"iw-IL": "ILS", // ISRAEL
|
||||||
|
"hi-IN": "INR", // INDIA
|
||||||
|
"hu-HU": "HUF", // HUNGARY
|
||||||
|
"is-IS": "ISK", // ICELAND
|
||||||
|
"in-ID": "IDR", // INDONESIA
|
||||||
|
"ga-IE": "EUR", // IRELAND
|
||||||
|
"it-IT": "EUR", // ITALY
|
||||||
|
"it-CH": "CHF", // SWITZERLAND
|
||||||
|
"ja-JP": "JPY", // JAPAN
|
||||||
|
"ko-KR": "KRW", // SOUTH KOREA
|
||||||
|
"lv-LV": "EUR", // LATVIA
|
||||||
|
"lt-LT": "EUR", // LITHUANIA
|
||||||
|
"mk-MK": "MKD", // MACEDONIA
|
||||||
|
"ms-MY": "MYR", // MALAYSIA
|
||||||
|
"mt-MT": "EUR", // MALTA
|
||||||
|
"no-NO": "NOK", // NORWAY
|
||||||
|
"pl-PL": "PLN", // POLAND
|
||||||
|
"pt-BR": "BRL", // BRAZIL
|
||||||
|
"pt-PT": "EUR", // PORTUGAL
|
||||||
|
"ro-RO": "RON", // ROMANIA
|
||||||
|
"ru-RU": "RUB", // RUSSIA
|
||||||
|
"sr-BA": "BAM", // BOSNIA AND HERZEGOVINA
|
||||||
|
"sr-ME": "EUR", // MONTENEGRO
|
||||||
|
"sr-CS": "EURO", // SERBIA AND MONTENEGRO
|
||||||
|
"sr-RS": "RSD", // SERBIA
|
||||||
|
"sk-SK": "EUR", // SLOVAKIA
|
||||||
|
"sl-SI": "EUR", // SLOVENIA
|
||||||
|
"es-AR": "ARS", // ARGENTINA
|
||||||
|
"es-BO": "BOB", // BOLIVIA
|
||||||
|
"es-CL": "CLP", // CHILE
|
||||||
|
"es-CO": "COP", // COLOMBIA
|
||||||
|
"es-CR": "CRC", // COSTA RICA
|
||||||
|
"es-DO": "DOP", // DOMINICAN REPUBLIC
|
||||||
|
"es-EC": "USD", // ECUADOR
|
||||||
|
"es-SV": "USD", // EL SALVADOR
|
||||||
|
"es-GT": "GTQ", // GUATEMALA
|
||||||
|
"es-HN": "HNL", // HONDURAS
|
||||||
|
"es-MX": "MXN", // MEXICO
|
||||||
|
"es-NI": "NIO", // NICARAGUA
|
||||||
|
"es-PA": "USD", // PANAMA
|
||||||
|
"es-PY": "PYG", // PARAGUAY
|
||||||
|
"es-PE": "PEN", // PERU
|
||||||
|
"es-PR": "USD", // PUERTO RICO
|
||||||
|
"es-ES": "EUR", // SPAIN
|
||||||
|
"es-US": "USD", // US
|
||||||
|
"es-UY": "UYU", // URUGUAY
|
||||||
|
"es-VE": "VEF", // VENEZUELA
|
||||||
|
"sv-SE": "SEK", // SWEDEN
|
||||||
|
"th-TH": "THB", // THAILAND
|
||||||
|
"tr-TR": "TRY", // TURKEY
|
||||||
|
"uk-UA": "UAH", // UKRAINE
|
||||||
|
"vi-VN": "VND", // VIETNAM
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rtlLangsList = ["ar", "fa", "he", "ps", "ur"];
|
||||||
1
client/src/lib/hooks/useUploadFile/index.ts
Normal file
1
client/src/lib/hooks/useUploadFile/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./useUploadFile";
|
||||||
57
client/src/lib/hooks/useUploadFile/useUploadFile.ts
Normal file
57
client/src/lib/hooks/useUploadFile/useUploadFile.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -7,6 +7,8 @@
|
|||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"accept": "Aceptar",
|
"accept": "Aceptar",
|
||||||
"hide": "Ocultar",
|
"hide": "Ocultar",
|
||||||
|
"back": "Volver",
|
||||||
|
"upload": "Cargar",
|
||||||
"sort_asc": "Asc",
|
"sort_asc": "Asc",
|
||||||
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
|
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
|
||||||
"sort_desc": "Desc",
|
"sort_desc": "Desc",
|
||||||
@ -21,7 +23,12 @@
|
|||||||
"go_to_last_page": "Ir a la última página",
|
"go_to_last_page": "Ir a la última página",
|
||||||
"filter_placeholder": "Escribe aquí para filtrar...",
|
"filter_placeholder": "Escribe aquí para filtrar...",
|
||||||
"reset_filter": "Quitar el filtro",
|
"reset_filter": "Quitar el filtro",
|
||||||
"error": "Error"
|
"error": "Error",
|
||||||
|
"actions": "Acciones",
|
||||||
|
"open_menu": "Abrir el menú",
|
||||||
|
"duplicate_rows": "Duplicar",
|
||||||
|
"duplicate_rows_tooltip": "Duplica las fila(s) seleccionadas(s)",
|
||||||
|
"pick_date": "Elige una fecha"
|
||||||
},
|
},
|
||||||
"main_menu": {
|
"main_menu": {
|
||||||
"home": "Inicio",
|
"home": "Inicio",
|
||||||
@ -64,6 +71,81 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"quotes": {
|
||||||
|
"list": {
|
||||||
|
"title": "Cotizaciones"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"draft": "Borrador"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"title": "Nueva cotización",
|
||||||
|
"buttons": {
|
||||||
|
"save_quote": "Guardar cotización",
|
||||||
|
"discard": "Descartar"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"general": "Datos generales",
|
||||||
|
"items": "Contenido",
|
||||||
|
"documents": "Documentos",
|
||||||
|
"history": "Historial"
|
||||||
|
},
|
||||||
|
"form_groups": {
|
||||||
|
"general": {
|
||||||
|
"title": "Datos generales",
|
||||||
|
"desc": "Datos generales y cliente al que va la cotización"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "Estado",
|
||||||
|
"desc": "Estado de la cotización"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"title": "Contenido de la cotización",
|
||||||
|
"desc": "Líneas de detalle de la cotización. Ayúdese del catálogo para rellenar más fácilmente el contenido."
|
||||||
|
},
|
||||||
|
"documents": {
|
||||||
|
"title": "Documentos",
|
||||||
|
"desc": "Añada adjuntar con su cotización documentos como fotos, planos, croquis, etc."
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "",
|
||||||
|
"desc": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"form_fields": {
|
||||||
|
"date": {
|
||||||
|
"label": "Fecha",
|
||||||
|
"desc": "Fecha de esta cotización",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"label": "Referencia",
|
||||||
|
"desc": "Referencia para esta cotización",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"customer_information": {
|
||||||
|
"label": "Cliente",
|
||||||
|
"desc": "Datos del cliente de esta cotización",
|
||||||
|
"placeholder": "Nombre\nDirección\n..."
|
||||||
|
},
|
||||||
|
"payment_method": {
|
||||||
|
"label": "Forma de pago",
|
||||||
|
"placeholder": "placeholder",
|
||||||
|
"desc": "desc"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"label": "Notas",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "desc"
|
||||||
|
},
|
||||||
|
"validity": {
|
||||||
|
"label": "Validez de la cotización",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Ajustes",
|
"title": "Ajustes",
|
||||||
"quotes": {
|
"quotes": {
|
||||||
@ -91,7 +173,7 @@
|
|||||||
"default_quote_validity": {
|
"default_quote_validity": {
|
||||||
"label": "Validez por defecto",
|
"label": "Validez por defecto",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"desc": "desc"
|
"desc": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import catResources from "../locales/ca.json";
|
import catResources from "./ca.json";
|
||||||
import enResources from "../locales/en.json";
|
import enResources from "./en.json";
|
||||||
import esResources from "../locales/es.json";
|
import esResources from "./es.json";
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
// detect user language
|
// detect user language
|
||||||
@ -14,8 +14,8 @@ i18n
|
|||||||
// init i18next
|
// init i18next
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
.init({
|
.init({
|
||||||
debug: true,
|
debug: false,
|
||||||
fallbackLng: "en",
|
fallbackLng: "es",
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
|
|||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
import "./lib/i18n";
|
import "./locales/i18n.ts";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("uecko")!).render(
|
ReactDOM.createRoot(document.getElementById("uecko")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|||||||
@ -1,79 +1,56 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
HTMLDivElement,
|
({ className, ...props }, ref) => (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
ref={ref}
|
||||||
<div
|
className={cn("rounded-md border bg-card text-card-foreground shadow", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn(
|
/>
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
)
|
||||||
className
|
);
|
||||||
)}
|
Card.displayName = "Card";
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Card.displayName = "Card"
|
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
HTMLDivElement,
|
({ className, ...props }, ref) => (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||||
>(({ className, ...props }, ref) => (
|
)
|
||||||
<div
|
);
|
||||||
ref={ref}
|
CardHeader.displayName = "CardHeader";
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardHeader.displayName = "CardHeader"
|
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||||
HTMLParagraphElement,
|
({ className, ...props }, ref) => (
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
<h3
|
||||||
>(({ className, ...props }, ref) => (
|
ref={ref}
|
||||||
<h3
|
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn(
|
/>
|
||||||
"text-2xl font-semibold leading-none tracking-tight",
|
)
|
||||||
className
|
);
|
||||||
)}
|
CardTitle.displayName = "CardTitle";
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardTitle.displayName = "CardTitle"
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
const CardDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<p
|
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
ref={ref}
|
));
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
CardDescription.displayName = "CardDescription";
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardDescription.displayName = "CardDescription"
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
HTMLDivElement,
|
({ className, ...props }, ref) => (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
>(({ className, ...props }, ref) => (
|
)
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
);
|
||||||
))
|
CardContent.displayName = "CardContent";
|
||||||
CardContent.displayName = "CardContent"
|
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
HTMLDivElement,
|
({ className, ...props }, ref) => (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||||
>(({ className, ...props }, ref) => (
|
)
|
||||||
<div
|
);
|
||||||
ref={ref}
|
CardFooter.displayName = "CardFooter";
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardFooter.displayName = "CardFooter"
|
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
import * as React from "react"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
@ -32,11 +32,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<ChevronRight className='w-4 h-4 ml-auto' />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
))
|
));
|
||||||
DropdownMenuSubTrigger.displayName =
|
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
@ -50,9 +49,8 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSubContent.displayName =
|
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
@ -69,13 +67,13 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
));
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
const DropdownMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
@ -87,8 +85,8 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
@ -103,16 +101,15 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<Check className='w-4 h-4' />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
))
|
));
|
||||||
DropdownMenuCheckboxItem.displayName =
|
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
@ -126,33 +123,29 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
<Circle className='w-2 h-2 fill-current' />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
))
|
));
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
@ -163,36 +156,30 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
);
|
||||||
{...props}
|
};
|
||||||
/>
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
)
|
|
||||||
}
|
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuTrigger,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import * as React from "react"
|
"use client";
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
const TabsList = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
@ -12,13 +14,13 @@ const TabsList = React.forwardRef<
|
|||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
"inline-flex h-9 items-center justify-center rounded-lg bg-accent p-1 text-muted-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
const TabsTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
@ -27,13 +29,13 @@ const TabsTrigger = React.forwardRef<
|
|||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
const TabsContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
@ -47,7 +49,7 @@ const TabsContent = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||||
|
|||||||
@ -77,8 +77,6 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
|
|||||||
queryCriteria,
|
queryCriteria,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(query);
|
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
...query,
|
...query,
|
||||||
distinct: true,
|
distinct: true,
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
|
import {
|
||||||
|
CreationOptional,
|
||||||
|
DataTypes,
|
||||||
|
InferAttributes,
|
||||||
|
InferCreationAttributes,
|
||||||
|
Model,
|
||||||
|
Sequelize,
|
||||||
|
} from "sequelize";
|
||||||
|
|
||||||
export type ProfileCreationAttributes = InferCreationAttributes<Profile_Model>;
|
export type ProfileCreationAttributes = InferCreationAttributes<Profile_Model>;
|
||||||
|
|
||||||
@ -15,11 +22,11 @@ export class Profile_Model extends Model<
|
|||||||
static associate(connection: Sequelize) {}
|
static associate(connection: Sequelize) {}
|
||||||
|
|
||||||
declare id: string;
|
declare id: string;
|
||||||
declare contact_information: string;
|
declare contact_information: CreationOptional<string>;
|
||||||
declare default_payment_method: string;
|
declare default_payment_method: CreationOptional<string>;
|
||||||
declare default_notes: string;
|
declare default_notes: CreationOptional<string>;
|
||||||
declare default_legal_terms: string;
|
declare default_legal_terms: CreationOptional<string>;
|
||||||
declare default_quote_validity: string;
|
declare default_quote_validity: CreationOptional<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
export default (sequelize: Sequelize) => {
|
||||||
|
|||||||
@ -14,8 +14,14 @@ import { QuoteStatus } from "./QuoteStatus";
|
|||||||
export interface IQuoteProps {
|
export interface IQuoteProps {
|
||||||
status: QuoteStatus;
|
status: QuoteStatus;
|
||||||
date: UTCDateValue;
|
date: UTCDateValue;
|
||||||
|
reference: string;
|
||||||
language: Language;
|
language: Language;
|
||||||
|
customer: string;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
paymentMethod: string;
|
||||||
|
notes: string;
|
||||||
|
validity: string;
|
||||||
|
|
||||||
items: ICollection<QuoteItem>;
|
items: ICollection<QuoteItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +30,7 @@ export interface IQuote {
|
|||||||
|
|
||||||
status: QuoteStatus;
|
status: QuoteStatus;
|
||||||
date: UTCDateValue;
|
date: UTCDateValue;
|
||||||
|
|
||||||
language: Language;
|
language: Language;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
items: ICollection<QuoteItem>;
|
items: ICollection<QuoteItem>;
|
||||||
|
|||||||
@ -53,13 +53,9 @@ export class QuoteRepository extends SequelizeRepository<Quote> implements IQuot
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<any>> {
|
public async findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<any>> {
|
||||||
const { rows, count } = await this._findAll(
|
const { rows, count } = await this._findAll("Quote_Model", queryCriteria, {
|
||||||
"Quote_Model",
|
include: [], // esto es para quitar las asociaciones al hacer la consulta
|
||||||
queryCriteria
|
});
|
||||||
/*{
|
|
||||||
include: [], // esto es para quitar las asociaciones al hacer la consulta
|
|
||||||
}*/
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.mapper.mapArrayAndCountToDomain(rows, count);
|
return this.mapper.mapArrayAndCountToDomain(rows, count);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,8 +59,6 @@ export class ListDealersController extends ExpressController {
|
|||||||
try {
|
try {
|
||||||
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
||||||
|
|
||||||
console.log(queryCriteria);
|
|
||||||
|
|
||||||
const result: ListDealersResult = await this.useCase.execute({
|
const result: ListDealersResult = await this.useCase.execute({
|
||||||
queryCriteria,
|
queryCriteria,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -59,8 +59,6 @@ export class ListQuotesController extends ExpressController {
|
|||||||
try {
|
try {
|
||||||
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
||||||
|
|
||||||
console.log(queryCriteria);
|
|
||||||
|
|
||||||
const result: ListQuotesResult = await this.useCase.execute({
|
const result: ListQuotesResult = await this.useCase.execute({
|
||||||
queryCriteria,
|
queryCriteria,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,23 +1,17 @@
|
|||||||
import { ListQuotesUseCase } from "@/contexts/sales/application";
|
import { ListQuotesUseCase } from "@/contexts/sales/application";
|
||||||
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
|
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
|
||||||
import Express from "express";
|
|
||||||
import { ISalesContext } from "../../../../Sales.context";
|
import { ISalesContext } from "../../../../Sales.context";
|
||||||
import { ListQuotesController } from "./ListQuotes.controller";
|
import { ListQuotesController } from "./ListQuotes.controller";
|
||||||
import { ListQuotesPresenter } from "./presenter";
|
import { ListQuotesPresenter } from "./presenter";
|
||||||
|
|
||||||
export const listQuotesController = (
|
export const listQuotesController = (context: ISalesContext) => {
|
||||||
req: Express.Request,
|
|
||||||
res: Express.Response,
|
|
||||||
next: Express.NextFunction
|
|
||||||
) => {
|
|
||||||
const context: ISalesContext = res.locals.context;
|
|
||||||
|
|
||||||
registerQuoteRepository(context);
|
registerQuoteRepository(context);
|
||||||
|
|
||||||
return new ListQuotesController(
|
return new ListQuotesController(
|
||||||
{
|
{
|
||||||
useCase: new ListQuotesUseCase(context),
|
useCase: new ListQuotesUseCase(context),
|
||||||
presenter: ListQuotesPresenter,
|
presenter: ListQuotesPresenter,
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
).execute(req, res, next);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
import { ICollection, IUpdateQuote_Response_DTO } from "@shared/contexts";
|
import { ICollection, IListQuotes_Response_DTO, IListResponse_DTO } from "@shared/contexts";
|
||||||
import { Quote, QuoteItem } from "../../../../../../domain";
|
import { Quote, QuoteItem } from "../../../../../../domain";
|
||||||
import { ISalesContext } from "../../../../../Sales.context";
|
import { ISalesContext } from "../../../../../Sales.context";
|
||||||
|
|
||||||
export interface IListQuotesPresenter {
|
export interface IListQuotesPresenter {
|
||||||
map: (quote: Quote, context: ISalesContext) => IUpdateQuote_Response_DTO;
|
map: (quote: Quote, context: ISalesContext) => IListQuotes_Response_DTO;
|
||||||
|
mapArray: (
|
||||||
|
articles: ICollection<Quote>,
|
||||||
|
context: ISalesContext,
|
||||||
|
params: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
) => IListResponse_DTO<IListQuotes_Response_DTO>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ListQuotesPresenter: IListQuotesPresenter = {
|
export const ListQuotesPresenter: IListQuotesPresenter = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
map: (quote: Quote, context: ISalesContext): IUpdateQuote_Response_DTO => {
|
map: (quote: Quote, context: ISalesContext): IListQuotes_Response_DTO => {
|
||||||
return {
|
return {
|
||||||
id: quote.id.toString(),
|
id: quote.id.toString(),
|
||||||
status: quote.status.toString(),
|
status: quote.status.toString(),
|
||||||
@ -25,9 +33,33 @@ export const ListQuotesPresenter: IListQuotesPresenter = {
|
|||||||
precision: 2,
|
precision: 2,
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
},
|
},
|
||||||
items: quoteItemPresenter(quote.items, context),
|
//items: quoteItemPresenter(quote.items, context),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mapArray: (
|
||||||
|
quotes: ICollection<Quote>,
|
||||||
|
context: ISalesContext,
|
||||||
|
params: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
): IListResponse_DTO<IListQuotes_Response_DTO> => {
|
||||||
|
const { page, limit } = params;
|
||||||
|
|
||||||
|
const totalCount = quotes.totalCount ?? 0;
|
||||||
|
const items = quotes.items.map((quote: Quote) => ListQuotesPresenter.map(quote, context));
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
page,
|
||||||
|
per_page: limit,
|
||||||
|
total_pages: Math.ceil(totalCount / limit),
|
||||||
|
total_items: totalCount,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
InferCreationAttributes,
|
InferCreationAttributes,
|
||||||
Model,
|
Model,
|
||||||
NonAttribute,
|
NonAttribute,
|
||||||
|
Op,
|
||||||
Sequelize,
|
Sequelize,
|
||||||
} from "sequelize";
|
} from "sequelize";
|
||||||
import { Dealer_Model } from "./dealer.model";
|
import { Dealer_Model } from "./dealer.model";
|
||||||
@ -36,12 +37,19 @@ export class Quote_Model extends Model<
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare id: string;
|
declare id: string;
|
||||||
declare status: string;
|
declare status: CreationOptional<string>;
|
||||||
declare date: CreationOptional<string>;
|
declare date: CreationOptional<string>;
|
||||||
declare language_code: string;
|
declare reference: CreationOptional<string>;
|
||||||
declare currency_code: string;
|
declare lang_code: CreationOptional<string>;
|
||||||
declare subtotal: number;
|
declare customer_information: CreationOptional<string>;
|
||||||
declare total: number;
|
declare currency_code: CreationOptional<string>;
|
||||||
|
declare payment_method: CreationOptional<string>;
|
||||||
|
declare notes: CreationOptional<string>;
|
||||||
|
declare validity: CreationOptional<string>;
|
||||||
|
|
||||||
|
declare subtotal: CreationOptional<number>;
|
||||||
|
declare discount: CreationOptional<number>;
|
||||||
|
declare total: CreationOptional<number>;
|
||||||
|
|
||||||
declare items?: NonAttribute<QuoteItem_Model[]>;
|
declare items?: NonAttribute<QuoteItem_Model[]>;
|
||||||
declare dealer?: NonAttribute<Dealer_Model>;
|
declare dealer?: NonAttribute<Dealer_Model>;
|
||||||
@ -65,14 +73,36 @@ export default (sequelize: Sequelize) => {
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
language_code: {
|
reference: {
|
||||||
type: new DataTypes.STRING(),
|
type: new DataTypes.STRING(),
|
||||||
|
},
|
||||||
|
|
||||||
|
lang_code: {
|
||||||
|
type: DataTypes.STRING(2),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "es",
|
||||||
},
|
},
|
||||||
|
|
||||||
currency_code: {
|
currency_code: {
|
||||||
type: new DataTypes.STRING(),
|
type: new DataTypes.STRING(3),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "EUR",
|
||||||
|
},
|
||||||
|
|
||||||
|
customer_information: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
payment_method: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
notes: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
validity: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
},
|
},
|
||||||
|
|
||||||
subtotal: {
|
subtotal: {
|
||||||
@ -80,6 +110,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
discount: {
|
||||||
|
type: new DataTypes.BIGINT(),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
|
||||||
total: {
|
total: {
|
||||||
type: new DataTypes.BIGINT(),
|
type: new DataTypes.BIGINT(),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -99,8 +134,28 @@ export default (sequelize: Sequelize) => {
|
|||||||
|
|
||||||
indexes: [
|
indexes: [
|
||||||
{ name: "status_idx", fields: ["status"] },
|
{ name: "status_idx", fields: ["status"] },
|
||||||
|
{ name: "reference_idx", fields: ["reference"] },
|
||||||
{ name: "deleted_at_idx", fields: ["deleted_at"] },
|
{ name: "deleted_at_idx", fields: ["deleted_at"] },
|
||||||
],
|
],
|
||||||
|
|
||||||
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
|
scopes: {
|
||||||
|
quickSearch(value) {
|
||||||
|
return {
|
||||||
|
where: {
|
||||||
|
[Op.or]: {
|
||||||
|
reference: {
|
||||||
|
[Op.like]: `%${value}%`,
|
||||||
|
},
|
||||||
|
customer_information: {
|
||||||
|
[Op.like]: `%${value}%`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,7 @@ import Joi from "joi";
|
|||||||
import { QueryCriteriaService } from "@/contexts/common/application/services";
|
import { QueryCriteriaService } from "@/contexts/common/application/services";
|
||||||
import { IServerError } from "@/contexts/common/domain/errors";
|
import { IServerError } from "@/contexts/common/domain/errors";
|
||||||
import { ExpressController } from "@/contexts/common/infrastructure/express";
|
import { ExpressController } from "@/contexts/common/infrastructure/express";
|
||||||
import {
|
import { ListUsersResult, ListUsersUseCase } from "@/contexts/users/application";
|
||||||
ListUsersResult,
|
|
||||||
ListUsersUseCase,
|
|
||||||
} from "@/contexts/users/application";
|
|
||||||
import { User } from "@/contexts/users/domain";
|
import { User } from "@/contexts/users/domain";
|
||||||
import {
|
import {
|
||||||
ICollection,
|
ICollection,
|
||||||
@ -29,7 +26,7 @@ export class ListUsersController extends ExpressController {
|
|||||||
useCase: ListUsersUseCase;
|
useCase: ListUsersUseCase;
|
||||||
presenter: IListUsersPresenter;
|
presenter: IListUsersPresenter;
|
||||||
},
|
},
|
||||||
context: IUserContext,
|
context: IUserContext
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -60,10 +57,7 @@ export class ListUsersController extends ExpressController {
|
|||||||
const queryParams = queryOrError.object;
|
const queryParams = queryOrError.object;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const queryCriteria: IQueryCriteria =
|
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
||||||
QueryCriteriaService.parse(queryParams);
|
|
||||||
|
|
||||||
console.log(queryCriteria);
|
|
||||||
|
|
||||||
const result: ListUsersResult = await this.useCase.execute({
|
const result: ListUsersResult = await this.useCase.execute({
|
||||||
queryCriteria,
|
queryCriteria,
|
||||||
@ -79,7 +73,7 @@ export class ListUsersController extends ExpressController {
|
|||||||
this.presenter.mapArray(users, this.context, {
|
this.presenter.mapArray(users, this.context, {
|
||||||
page: queryCriteria.pagination.offset,
|
page: queryCriteria.pagination.offset,
|
||||||
limit: queryCriteria.pagination.limit,
|
limit: queryCriteria.pagination.limit,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return this.fail(e as IServerError);
|
return this.fail(e as IServerError);
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
import { checkisAdmin } from "@/contexts/auth";
|
import { checkUser } from "@/contexts/auth";
|
||||||
|
import { listQuotesController } from "@/contexts/sales/infrastructure/express/controllers";
|
||||||
import Express from "express";
|
import Express from "express";
|
||||||
|
|
||||||
export const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
export const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||||
|
|
||||||
/*quoteRoutes.get("/", isAdmin, listQuotesController);
|
quoteRoutes.get(
|
||||||
quoteRoutes.get("/:quoteId", isUser, getQuoteMiddleware, getQuoteController);
|
"/",
|
||||||
|
checkUser,
|
||||||
|
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
|
||||||
|
listQuotesController(res.locals["context"]).execute(req, res, next)
|
||||||
|
);
|
||||||
|
/*quoteRoutes.get("/:quoteId", isUser, getQuoteMiddleware, getQuoteController);
|
||||||
quoteRoutes.post("/", isAdmin, createQuoteController);
|
quoteRoutes.post("/", isAdmin, createQuoteController);
|
||||||
quoteRoutes.put("/:quoteId", isAdmin, updateQuoteController);
|
quoteRoutes.put("/:quoteId", isAdmin, updateQuoteController);
|
||||||
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
|
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
|
||||||
|
|
||||||
quoteRoutes.get("/", checkisAdmin, (req, res) => {
|
|
||||||
console.log(req.params);
|
|
||||||
res.status(200).json();
|
|
||||||
});
|
|
||||||
|
|
||||||
export const QuoteRouter = (appRouter: Express.Router) => {
|
export const QuoteRouter = (appRouter: Express.Router) => {
|
||||||
appRouter.use("/quotes", quoteRoutes);
|
appRouter.use("/quotes", quoteRoutes);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { IMoney_Response_DTO } from "../../../../common";
|
|||||||
export interface IListArticles_Response_DTO {
|
export interface IListArticles_Response_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
catalog_name: string;
|
catalog_name: string;
|
||||||
//id_article: string;
|
id_article: string;
|
||||||
//reference: string;
|
reference: string;
|
||||||
//family: string;
|
//family: string;
|
||||||
//subfamily: string;
|
//subfamily: string;
|
||||||
|
|
||||||
description: string;
|
description: string;
|
||||||
points: number;
|
points: number;
|
||||||
retail_price: IMoney_Response_DTO;
|
retail_price: IMoney_Response_DTO;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
import { Currencies } from "../../../../../utilities/currencies";
|
||||||
import { RuleValidator } from "../../RuleValidator";
|
import { RuleValidator } from "../../RuleValidator";
|
||||||
import { DomainError, handleDomainError } from "../../errors";
|
import { DomainError, handleDomainError } from "../../errors";
|
||||||
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
||||||
import { Result } from "../Result";
|
import { Result } from "../Result";
|
||||||
import { Currencies } from "./currencies";
|
|
||||||
|
|
||||||
export interface ICurrency {
|
export interface ICurrency {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import { Result } from "../Result";
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
|
||||||
import { UndefinedOr } from "../../../../../utilities";
|
import { UndefinedOr } from "../../../../../utilities";
|
||||||
|
import { LANGUAGES_LIST } from "../../../../../utilities/languages_data";
|
||||||
import { DomainError, handleDomainError } from "../../errors";
|
import { DomainError, handleDomainError } from "../../errors";
|
||||||
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
||||||
import { LANGUAGES_LIST } from "./languages_data";
|
|
||||||
|
|
||||||
export interface ILanguage {
|
export interface ILanguage {
|
||||||
code: string;
|
code: string;
|
||||||
@ -39,7 +39,7 @@ export class Language extends NullableValueObject<ILanguage> {
|
|||||||
public static createFromCode(languageCode: string, options: ILanguageOptions = {}) {
|
public static createFromCode(languageCode: string, options: ILanguageOptions = {}) {
|
||||||
const _options = {
|
const _options = {
|
||||||
...options,
|
...options,
|
||||||
label: options.label ? options.label : "language_code",
|
label: options.label ? options.label : "lang_code",
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationResult = Language.validate(languageCode, _options);
|
const validationResult = Language.validate(languageCode, _options);
|
||||||
|
|||||||
@ -5,8 +5,13 @@ export interface ICreateQuote_Request_DTO {
|
|||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
language_code: string;
|
reference: string;
|
||||||
|
customer_information: string;
|
||||||
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
payment_method: string;
|
||||||
|
notes: string;
|
||||||
|
validity: string;
|
||||||
|
|
||||||
items: ICreateQuoteItem_Request_DTO[];
|
items: ICreateQuoteItem_Request_DTO[];
|
||||||
}
|
}
|
||||||
@ -22,8 +27,14 @@ export function ensureCreateQuote_Request_DTOIsValid(quoteDTO: ICreateQuote_Requ
|
|||||||
const schema = Joi.object({
|
const schema = Joi.object({
|
||||||
id: Joi.string(),
|
id: Joi.string(),
|
||||||
date: Joi.string(),
|
date: Joi.string(),
|
||||||
language: Joi.string(),
|
reference: Joi.string(),
|
||||||
currency: Joi.string(),
|
lang_code: Joi.string(),
|
||||||
|
customer_information: Joi.string(),
|
||||||
|
currency_code: Joi.string(),
|
||||||
|
payment_method: Joi.string(),
|
||||||
|
notes: Joi.string(),
|
||||||
|
validity: Joi.string(),
|
||||||
|
|
||||||
items: Joi.array().items(
|
items: Joi.array().items(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
description: Joi.string(),
|
description: Joi.string(),
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
import { IMoney_Response_DTO } from "../../../../../common";
|
||||||
|
|
||||||
export interface IGetQuote_Response_DTO {
|
export interface IGetQuote_Response_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
language_code: string;
|
reference: string;
|
||||||
|
customer_information: string;
|
||||||
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
payment_method: string;
|
||||||
|
notes: string;
|
||||||
|
validity: string;
|
||||||
|
|
||||||
subtotal: IMoney_Response_DTO;
|
subtotal: IMoney_Response_DTO;
|
||||||
total: IMoney_Response_DTO;
|
total: IMoney_Response_DTO;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user