.
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",
|
||||
"type": "pwa-chrome",
|
||||
"port": 9222
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/client"
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@ -4,12 +4,14 @@ import {
|
||||
DealersList,
|
||||
LoginPage,
|
||||
LogoutPage,
|
||||
QuoteCreate,
|
||||
SettingsEditor,
|
||||
SettingsLayout,
|
||||
StartPage,
|
||||
} from "./app";
|
||||
import { CatalogLayout, CatalogList } from "./app/catalog";
|
||||
import { DashboardPage } from "./app/dashboard";
|
||||
import { QuotesLayout } from "./app/quotes/layout";
|
||||
import { QuotesList } from "./app/quotes/list";
|
||||
import { ProtectedRoute } from "./components";
|
||||
|
||||
@ -68,9 +70,21 @@ export const Routes = () => {
|
||||
path: "/quotes",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<QuotesList />
|
||||
<QuotesLayout>
|
||||
<Outlet />
|
||||
</QuotesLayout>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <QuotesList />,
|
||||
},
|
||||
{
|
||||
path: "add",
|
||||
element: <QuoteCreate />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
@ -107,11 +121,12 @@ export const Routes = () => {
|
||||
];
|
||||
|
||||
// Combine and conditionally include routes based on authentication status
|
||||
const router = createBrowserRouter([
|
||||
...routesForPublic,
|
||||
...routesForAuthenticatedOnly,
|
||||
...routesForNotAuthenticatedOnly,
|
||||
]);
|
||||
const router = createBrowserRouter(
|
||||
[...routesForPublic, ...routesForAuthenticatedOnly, ...routesForNotAuthenticatedOnly],
|
||||
{
|
||||
//basename: "/app",
|
||||
}
|
||||
);
|
||||
|
||||
// Provide the router configuration using RouterProvider
|
||||
return <RouterProvider router={router} />;
|
||||
|
||||
@ -5,8 +5,8 @@ import { DataTablaRowActionFunction, DataTableRowActions } from "@/components";
|
||||
import { Badge } from "@/ui";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useCustomerInvoiceDataTableColumns = (
|
||||
actions: DataTablaRowActionFunction<IListArticles_Response_DTO>
|
||||
export const useCatalogTableColumns = (
|
||||
actions: DataTablaRowActionFunction<IListArticles_Response_DTO, unknown>
|
||||
): ColumnDef<IListArticles_Response_DTO>[] => {
|
||||
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";
|
||||
|
||||
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 = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<LayoutHeader />
|
||||
<LayoutContent>
|
||||
<div className='flex items-center'>
|
||||
<h1 className='text-lg font-semibold md:text-2xl'>Quotes</h1>
|
||||
</div>
|
||||
</LayoutContent>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
export const QuotesList = () => (
|
||||
<DataTableProvider>
|
||||
<div className='flex items-center'>
|
||||
<h1 className='text-lg font-semibold md:text-2xl'>
|
||||
<Trans i18nKey='quotes.title' />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<QuotesDataTable />
|
||||
</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,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/ui";
|
||||
import { CellContext } from "@tanstack/react-table";
|
||||
import { CellContext, Row } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
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> = (
|
||||
props: DataTableRowActionContext<TData, TValue>,
|
||||
props: DataTableRowActionContext<TData, TValue>
|
||||
) => DataTableRowActionDefinition<TData, TValue>[];
|
||||
|
||||
export type DataTableRowActionDefinition<TData, TValue> = {
|
||||
label: string | "-";
|
||||
shortcut?: string;
|
||||
onClick?: (
|
||||
props: DataTableRowActionContext<TData, TValue>,
|
||||
e: React.BaseSyntheticEvent,
|
||||
) => void;
|
||||
onClick?: (props: DataTableRowActionContext<TData, TValue>, e: React.BaseSyntheticEvent) => void;
|
||||
};
|
||||
|
||||
export type DataTableRowActionsProps<TData, TValue> = {
|
||||
props: DataTableRowActionContext<TData, TValue>;
|
||||
actions?: DataTablaRowActionFunction<TData, TValue>;
|
||||
row?: DataTableRowActionContext<TData, TValue>;
|
||||
};
|
||||
|
||||
export function DataTableRowActions<TData, TValue>({
|
||||
export function DataTableRowActions<TData = any, TValue = any>({
|
||||
actions,
|
||||
...props
|
||||
}: DataTableRowActionsProps<TData, TValue>) {
|
||||
@ -41,18 +41,18 @@ export function DataTableRowActions<TData, TValue>({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-haspopup="true"
|
||||
size="icon"
|
||||
variant="link"
|
||||
className="w-4 h-4 translate-y-[2px]"
|
||||
aria-haspopup='true'
|
||||
size='icon'
|
||||
variant='link'
|
||||
className='w-4 h-4 translate-y-[2px]'
|
||||
>
|
||||
<MoreHorizontalIcon className="w-4 h-4" />
|
||||
<span className="sr-only">Abrir menú</span>
|
||||
<MoreHorizontalIcon className='w-4 h-4' />
|
||||
<span className='sr-only'>{t("common.open_menu")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Acciones</DropdownMenuLabel>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuLabel>{t("common.actions")} </DropdownMenuLabel>
|
||||
{actions &&
|
||||
actions(props).map((action, index) =>
|
||||
action.label === "-" ? (
|
||||
@ -60,14 +60,12 @@ export function DataTableRowActions<TData, TValue>({
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
key={index}
|
||||
onClick={(event) =>
|
||||
action.onClick ? action.onClick(props, event) : null
|
||||
}
|
||||
onClick={(event) => (action.onClick ? action.onClick(props, event) : null)}
|
||||
>
|
||||
{action.label}
|
||||
<DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -9,29 +9,29 @@ export const SimpleEmptyState = ({
|
||||
actions = undefined,
|
||||
}) => {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className='text-center'>
|
||||
<svg
|
||||
className="w-12 h-12 mx-auto text-slate-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
className='w-12 h-12 mx-auto text-slate-400'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<path
|
||||
vectorEffect="non-scaling-stroke"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
vectorEffect='non-scaling-stroke'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
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>
|
||||
<h3 className="mt-2 text-sm font-semibold text-slate-900">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{subtitle}</p>
|
||||
<h3 className='mt-2 text-lg font-semibold text-slate-900'>{title}</h3>
|
||||
<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 && (
|
||||
<Button className="my-4" onClick={onButtonClick}>
|
||||
<Button className='my-4' onClick={onButtonClick}>
|
||||
<PlusIcon />
|
||||
{buttonText}
|
||||
</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">
|
||||
>(({ label, hint, required, ...props }, ref) => {
|
||||
const _hint = hint ? hint : required ? "obligatorio" : undefined;
|
||||
const _hintClassName = required ? "text-destructive" : "";
|
||||
return (
|
||||
<UI.FormLabel ref={ref} className="flex justify-between" {...props}>
|
||||
<span className="block font-semibold">{label}</span>
|
||||
{_hint && <span className="font-normal">{_hint}</span>}
|
||||
<UI.FormLabel ref={ref} className='flex justify-between text-sm' {...props}>
|
||||
<span className='block font-semibold'>{label}</span>
|
||||
{_hint && <span className={`text-sm font-medium ${_hintClassName}`}>{_hint}</span>}
|
||||
</UI.FormLabel>
|
||||
);
|
||||
});
|
||||
|
||||
@ -10,7 +10,13 @@ import {
|
||||
} from "@/ui";
|
||||
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";
|
||||
|
||||
export type FormTextAreaFieldProps<
|
||||
@ -29,36 +35,50 @@ export type FormTextAreaFieldProps<
|
||||
export const FormTextAreaField = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement> & FormTextAreaFieldProps
|
||||
>(({ label, hint, placeholder, description, autoSize, className, ...props }, ref) => {
|
||||
return (
|
||||
<FormField
|
||||
control={props.control}
|
||||
name={props.name}
|
||||
// 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} />}
|
||||
<FormControl>
|
||||
{autoSize ? (
|
||||
<AutosizeTextarea
|
||||
disabled={props.disabled}
|
||||
placeholder={placeholder}
|
||||
className='resize-y'
|
||||
{...field}
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
disabled={props.disabled}
|
||||
placeholder={placeholder}
|
||||
className='resize-y'
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
{description && <FormDescription>{description}</FormDescription>}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
label,
|
||||
hint,
|
||||
description,
|
||||
placeholder,
|
||||
required,
|
||||
disabled,
|
||||
autoSize,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { control } = useFormContext();
|
||||
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 space-y-3")}>
|
||||
{label && <FormLabel label={label} hint={hint} required={required} />}
|
||||
<FormControl className='grow'>
|
||||
{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 { 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 { FormInputProps, FormInputWithIconProps } from "./FormProps";
|
||||
|
||||
@ -46,10 +52,11 @@ export const FormTextField = React.forwardRef<
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
errors,
|
||||
name,
|
||||
control,
|
||||
type,
|
||||
} = props;
|
||||
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
@ -59,7 +66,7 @@ export const FormTextField = React.forwardRef<
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
render={({ field, fieldState, formState }) => (
|
||||
<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(
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./FormDatePickerField";
|
||||
export * from "./FormGroup";
|
||||
export * from "./FormLabel";
|
||||
export * from "./FormMoneyField";
|
||||
|
||||
@ -9,7 +9,7 @@ import { UserButton } from "./components/UserButton";
|
||||
|
||||
export const LayoutHeader = () => {
|
||||
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'>
|
||||
<Link to='/' className='flex items-center font-semibold'>
|
||||
<UeckoLogo className='w-24' />
|
||||
@ -78,7 +78,7 @@ export const LayoutHeader = () => {
|
||||
<Input
|
||||
type='search'
|
||||
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>
|
||||
</form>
|
||||
|
||||
@ -33,7 +33,6 @@ export const UserButton = () => {
|
||||
},
|
||||
});
|
||||
const { data, status } = useGetIdentity();
|
||||
|
||||
console.log(data, status);
|
||||
|
||||
const openUserMenu = (event: SyntheticEvent) => {
|
||||
@ -41,57 +40,53 @@ export const UserButton = () => {
|
||||
setUserMenuOpened(true);
|
||||
};
|
||||
|
||||
/*const closeUserMenu = (event: SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
setUserMenuOpened(false);
|
||||
};*/
|
||||
|
||||
/*if (status !== "success") {
|
||||
return <></>;
|
||||
}*/
|
||||
|
||||
return (
|
||||
<DropdownMenu open={userMenuOpened} onOpenChange={setUserMenuOpened}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<>
|
||||
{status === "success" && (
|
||||
<div className='grid gap-1 text-right'>
|
||||
<p className='text-sm font-medium leading-none'>{data?.name}</p>
|
||||
<p className='text-sm text-muted-foreground'>{data?.email}</p>
|
||||
</div>
|
||||
)}
|
||||
<Button variant='secondary' size='icon' className='rounded-full' onClick={openUserMenu}>
|
||||
<CircleUserIcon className='w-5 h-5' />
|
||||
<>
|
||||
{status === "success" && (
|
||||
<div className='grid gap-1 text-right'>
|
||||
<p className='text-xs font-medium leading-none'>{data?.name}</p>
|
||||
<p className='text-xs text-muted-foreground'>{data?.email}</p>
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenu open={userMenuOpened} onOpenChange={setUserMenuOpened}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
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>
|
||||
</Button>
|
||||
</>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-56'>
|
||||
<DropdownMenuLabel>{t("main_menu.user.my_account")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<UserIcon className='w-4 h-4 mr-2' />
|
||||
<span>{t("main_menu.user.profile")}</span>
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-56'>
|
||||
<DropdownMenuLabel>{t("main_menu.user.my_account")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<UserIcon className='w-4 h-4 mr-2' />
|
||||
<span>{t("main_menu.user.profile")}</span>
|
||||
<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>
|
||||
<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>
|
||||
</DropdownMenuContent>
|
||||
{LogoutDialog}
|
||||
</DropdownMenu>
|
||||
</DropdownMenuContent>
|
||||
{LogoutDialog}
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./ButtonGroup";
|
||||
export * from "./Buttons";
|
||||
export * from "./Container";
|
||||
export * from "./CustomButtons";
|
||||
export * from "./CustomDialog";
|
||||
|
||||
@ -2,51 +2,51 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* https://ui.jln.dev/ Shivering Harpy Brown */
|
||||
/* https://ui.jln.dev/ Teriyaki */
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 30 65% 100%;
|
||||
--foreground: 30 75% 0%;
|
||||
--muted: 30 17% 95%;
|
||||
--muted-foreground: 30 15% 26%;
|
||||
--popover: 0 0% 99%;
|
||||
--popover-foreground: 0 0% 0%;
|
||||
--card: 0 0% 99%;
|
||||
--card-foreground: 0 0% 0%;
|
||||
--border: 30 4% 91%;
|
||||
--input: 30 4% 91%;
|
||||
--primary: 30 26% 23%;
|
||||
--primary-foreground: 30 26% 83%;
|
||||
--secondary: 30 12% 82%;
|
||||
--secondary-foreground: 30 12% 22%;
|
||||
--accent: 30 22% 76%;
|
||||
--accent-foreground: 30 22% 16%;
|
||||
--destructive: 18 91% 33%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--ring: 30 26% 23%;
|
||||
--background: 25 31% 100%;
|
||||
--foreground: 25 67% 4%;
|
||||
--muted: 25 30% 95%;
|
||||
--muted-foreground: 25 2% 29%;
|
||||
--popover: 25 70% 98%;
|
||||
--popover-foreground: 25 67% 4%;
|
||||
--card: 25 70% 98%;
|
||||
--card-foreground: 25 67% 4%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 25 31% 75%;
|
||||
--primary: 25 31% 75%;
|
||||
--primary-foreground: 25 31% 15%;
|
||||
--secondary: 25 18% 90%;
|
||||
--secondary-foreground: 25 18% 30%;
|
||||
--accent: 25 23% 83%;
|
||||
--accent-foreground: 25 23% 23%;
|
||||
--destructive: 13 96% 20%;
|
||||
--destructive-foreground: 13 96% 80%;
|
||||
--ring: 25 31% 75%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 30 53% 1%;
|
||||
--foreground: 30 11% 98%;
|
||||
--muted: 30 17% 5%;
|
||||
--muted-foreground: 30 15% 74%;
|
||||
--popover: 30 53% 2%;
|
||||
--popover-foreground: 30 11% 99%;
|
||||
--card: 30 53% 2%;
|
||||
--card-foreground: 30 11% 99%;
|
||||
--border: 30 4% 14%;
|
||||
--input: 30 4% 14%;
|
||||
--primary: 30 26% 23%;
|
||||
--primary-foreground: 30 26% 83%;
|
||||
--secondary: 30 7% 15%;
|
||||
--secondary-foreground: 30 7% 75%;
|
||||
--accent: 30 16% 24%;
|
||||
--accent-foreground: 30 16% 84%;
|
||||
--destructive: 18 91% 59%;
|
||||
--destructive-foreground: 0 0% 0%;
|
||||
--ring: 30 26% 23%;
|
||||
--background: 25 41% 2%;
|
||||
--foreground: 25 21% 98%;
|
||||
--muted: 25 30% 5%;
|
||||
--muted-foreground: 25 2% 71%;
|
||||
--popover: 25 41% 2%;
|
||||
--popover-foreground: 25 21% 98%;
|
||||
--card: 25 41% 2%;
|
||||
--card-foreground: 25 21% 98%;
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--primary: 25 31% 75%;
|
||||
--primary-foreground: 25 31% 15%;
|
||||
--secondary: 25 5% 14%;
|
||||
--secondary-foreground: 25 5% 74%;
|
||||
--accent: 25 11% 20%;
|
||||
--accent-foreground: 25 11% 80%;
|
||||
--destructive: 13 96% 49%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--ring: 25 31% 75%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export const createAxiosAuthActions = (
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
redirectTo: "/",
|
||||
redirectTo: "/home",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
@ -60,6 +60,11 @@ export const createAxiosAuthActions = (
|
||||
},
|
||||
|
||||
getIdentity: async () => {
|
||||
const errorResult = {
|
||||
message: "Identification failed",
|
||||
name: "Invalid profile or identification",
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await httpClient.request<IIdentity_Response_DTO>({
|
||||
url: `${apiUrl}/auth/identity`,
|
||||
@ -71,11 +76,11 @@ export const createAxiosAuthActions = (
|
||||
|
||||
if (profile?.id === data?.id) {
|
||||
secureLocalStorage.setItem("uecko.profile", data);
|
||||
return data;
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
return undefined;
|
||||
return Promise.reject(errorResult);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
return Promise.reject(errorResult);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -59,6 +59,7 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
|
||||
break;
|
||||
case 401:
|
||||
console.error("UnAuthorized");
|
||||
//return (window.location.href = "/logout");
|
||||
break;
|
||||
case 403:
|
||||
console.error("Forbidden");
|
||||
|
||||
@ -14,5 +14,6 @@ export * from "./useUrlId";
|
||||
export * from "./useAuth";
|
||||
export * from "./useCustomDialog";
|
||||
export * from "./useDataTable";
|
||||
export * from "./useLocalization";
|
||||
export * from "./usePagination";
|
||||
export * from "./useTheme";
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { IIdentity_Response_DTO } from "@shared/contexts";
|
||||
|
||||
export type SuccessNotificationResponse = {
|
||||
message: string;
|
||||
description?: string;
|
||||
@ -5,7 +7,7 @@ export type SuccessNotificationResponse = {
|
||||
|
||||
export type PermissionResponse = unknown;
|
||||
|
||||
export type IdentityResponse = unknown;
|
||||
export type IdentityResponse = IIdentity_Response_DTO;
|
||||
|
||||
export type AuthActionCheckResponse = {
|
||||
authenticated: boolean;
|
||||
@ -31,7 +33,7 @@ export type AuthActionResponse = {
|
||||
export interface IAuthActions {
|
||||
login: (params: any) => Promise<AuthActionResponse>;
|
||||
logout: (params: any) => Promise<AuthActionResponse>;
|
||||
check: (params?: any) => Promise<AuthActionCheckResponse>;
|
||||
check: () => Promise<AuthActionCheckResponse>;
|
||||
onError?: (error: any) => Promise<AuthActionOnErrorResponse>;
|
||||
register?: (params: unknown) => Promise<AuthActionResponse>;
|
||||
forgotPassword?: (params: unknown) => Promise<AuthActionResponse>;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { PropsWithChildren, createContext } from "react";
|
||||
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 = ({
|
||||
children,
|
||||
@ -27,9 +27,9 @@ export const AuthProvider = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheck = async (params: unknown) => {
|
||||
const handleCheck = async () => {
|
||||
try {
|
||||
return Promise.resolve(authActions.check?.(params));
|
||||
return Promise.resolve(authActions.check?.());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
|
||||
@ -2,15 +2,15 @@ import { IdentityResponse, useAuth } from "@/lib/hooks";
|
||||
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import { useQueryKey } from "../useQueryKey";
|
||||
|
||||
export const useGetIdentity = (queryOptions?: UseQueryOptions) => {
|
||||
export const useGetIdentity = (queryOptions?: UseQueryOptions<IdentityResponse>) => {
|
||||
const keys = useQueryKey();
|
||||
const { getIdentity } = useAuth();
|
||||
|
||||
return useQuery<IdentityResponse, Error>({
|
||||
const result = useQuery<IdentityResponse>({
|
||||
queryKey: keys().auth().action("identity").get(),
|
||||
queryFn: getIdentity,
|
||||
...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 { useQueryKey } from "../useQueryKey";
|
||||
|
||||
export const useIsLoggedIn = (queryOptions?: UseQueryOptions) => {
|
||||
export const useIsLoggedIn = (queryOptions?: UseQueryOptions<AuthActionCheckResponse>) => {
|
||||
const keys = useQueryKey();
|
||||
const { check } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
const result = useQuery<AuthActionCheckResponse>({
|
||||
queryKey: keys().auth().action("check").get(),
|
||||
queryFn: check,
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -14,10 +14,10 @@ export const useLogin = (params?: UseMutationOptions<AuthActionResponse, Error,
|
||||
return useMutation({
|
||||
mutationKey: keys().auth().action("login").get(),
|
||||
mutationFn: login,
|
||||
onSuccess: async (data, variables, context) => {
|
||||
onSuccess: (data, variables, context) => {
|
||||
const { success, redirectTo } = data;
|
||||
if (success && redirectTo) {
|
||||
navigate(redirectTo);
|
||||
navigate(redirectTo, { replace: true });
|
||||
}
|
||||
if (onSuccess) {
|
||||
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 {
|
||||
QueryFunctionContext,
|
||||
QueryKey,
|
||||
UseQueryResult,
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { UseQueryOptions, UseQueryResult, keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
UseLoadingOvertimeOptionsProps,
|
||||
UseLoadingOvertimeReturnType,
|
||||
} from "../useLoadingOvertime/useLoadingOvertime";
|
||||
import { TDataSourceError, TDataSourceRecord } from "./types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type UseOneQueryOptions<TUseOneQueryData, TUseOneQueryError> = {
|
||||
queryKey: QueryKey;
|
||||
queryFn: (context: QueryFunctionContext) => Promise<TUseOneQueryData>;
|
||||
enabled?: boolean;
|
||||
autoRefresh?: boolean;
|
||||
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,
|
||||
export function useOne<
|
||||
TQueryFnData extends TDataSourceRecord = TDataSourceRecord,
|
||||
TError = TDataSourceError,
|
||||
TData extends TDataSourceRecord = TQueryFnData
|
||||
>(options: UseQueryOptions<TQueryFnData, TError, TData>): UseQueryResult<TData, TError> {
|
||||
return useQuery<TQueryFnData, TError, TData>({
|
||||
placeholderData: keepPreviousData,
|
||||
enabled,
|
||||
select,
|
||||
...queryOptions,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,43 +1,18 @@
|
||||
import { UseMutationOptions, useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
DefaultError,
|
||||
UseMutationOptions,
|
||||
useMutation,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
export interface IUseSaveMutationOptions<
|
||||
TUseSaveMutationData = unknown,
|
||||
TUseSaveMutationError = DefaultError,
|
||||
TUseSaveMutationVariables = unknown,
|
||||
TUseSaveMutationContext = unknown,
|
||||
> extends UseMutationOptions<
|
||||
TUseSaveMutationData,
|
||||
TUseSaveMutationError,
|
||||
TUseSaveMutationVariables,
|
||||
TUseSaveMutationContext
|
||||
> {}
|
||||
TDataSourceContext,
|
||||
TDataSourceError,
|
||||
TDataSourceRecord,
|
||||
TDataSourceVariables,
|
||||
} from "./types";
|
||||
|
||||
export function useSave<
|
||||
TUseSaveMutationData = unknown,
|
||||
TUseSaveMutationError = DefaultError,
|
||||
TUseSaveMutationVariables = unknown,
|
||||
TUseSaveMutationContext = unknown,
|
||||
>(
|
||||
options: IUseSaveMutationOptions<
|
||||
TUseSaveMutationData,
|
||||
TUseSaveMutationError,
|
||||
TUseSaveMutationVariables,
|
||||
TUseSaveMutationContext
|
||||
>,
|
||||
) {
|
||||
const { mutationFn, ...params } = options;
|
||||
|
||||
return useMutation<
|
||||
TUseSaveMutationData,
|
||||
TUseSaveMutationError,
|
||||
TUseSaveMutationVariables,
|
||||
TUseSaveMutationContext
|
||||
>({
|
||||
mutationFn,
|
||||
...params,
|
||||
TData extends TDataSourceRecord = TDataSourceRecord,
|
||||
TError extends TDataSourceError = TDataSourceError,
|
||||
TVariables extends TDataSourceVariables = TDataSourceVariables,
|
||||
TContext extends TDataSourceContext = TDataSourceContext
|
||||
>(options: UseMutationOptions<TData, TError, TVariables, TContext>) {
|
||||
return useMutation<TData, TError, TVariables, TContext>({
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
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",
|
||||
"accept": "Aceptar",
|
||||
"hide": "Ocultar",
|
||||
"back": "Volver",
|
||||
"upload": "Cargar",
|
||||
"sort_asc": "Asc",
|
||||
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
|
||||
"sort_desc": "Desc",
|
||||
@ -21,7 +23,12 @@
|
||||
"go_to_last_page": "Ir a la última página",
|
||||
"filter_placeholder": "Escribe aquí para filtrar...",
|
||||
"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": {
|
||||
"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": {
|
||||
"title": "Ajustes",
|
||||
"quotes": {
|
||||
@ -91,7 +173,7 @@
|
||||
"default_quote_validity": {
|
||||
"label": "Validez por defecto",
|
||||
"placeholder": "",
|
||||
"desc": "desc"
|
||||
"desc": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import i18n from "i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import catResources from "../locales/ca.json";
|
||||
import enResources from "../locales/en.json";
|
||||
import esResources from "../locales/es.json";
|
||||
import catResources from "./ca.json";
|
||||
import enResources from "./en.json";
|
||||
import esResources from "./es.json";
|
||||
|
||||
i18n
|
||||
// detect user language
|
||||
@ -14,8 +14,8 @@ i18n
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
debug: true,
|
||||
fallbackLng: "en",
|
||||
debug: false,
|
||||
fallbackLng: "es",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
import "./lib/i18n";
|
||||
import "./locales/i18n.ts";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("uecko")!).render(
|
||||
<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<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-md border bg-card text-card-foreground shadow", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
)
|
||||
);
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
)
|
||||
);
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} 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 { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
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<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
@ -32,11 +32,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
<ChevronRight className='w-4 h-4 ml-auto' />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
@ -50,9 +49,8 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
));
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
@ -69,13 +67,13 @@ const DropdownMenuContent = React.forwardRef<
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@ -87,8 +85,8 @@ const DropdownMenuItem = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
@ -103,16 +101,15 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
checked={checked}
|
||||
{...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>
|
||||
<Check className="h-4 w-4" />
|
||||
<Check className='w-4 h-4' />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
@ -126,33 +123,29 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
)}
|
||||
{...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>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
<Circle className='w-2 h-2 fill-current' />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
@ -163,36 +156,30 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
"use client";
|
||||
|
||||
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<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
@ -12,13 +14,13 @@ const TabsList = React.forwardRef<
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
@ -27,13 +29,13 @@ const TabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
@ -47,7 +49,7 @@ const TabsContent = React.forwardRef<
|
||||
)}
|
||||
{...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,
|
||||
});
|
||||
|
||||
console.log(query);
|
||||
|
||||
const args = {
|
||||
...query,
|
||||
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>;
|
||||
|
||||
@ -15,11 +22,11 @@ export class Profile_Model extends Model<
|
||||
static associate(connection: Sequelize) {}
|
||||
|
||||
declare id: string;
|
||||
declare contact_information: string;
|
||||
declare default_payment_method: string;
|
||||
declare default_notes: string;
|
||||
declare default_legal_terms: string;
|
||||
declare default_quote_validity: string;
|
||||
declare contact_information: CreationOptional<string>;
|
||||
declare default_payment_method: CreationOptional<string>;
|
||||
declare default_notes: CreationOptional<string>;
|
||||
declare default_legal_terms: CreationOptional<string>;
|
||||
declare default_quote_validity: CreationOptional<string>;
|
||||
}
|
||||
|
||||
export default (sequelize: Sequelize) => {
|
||||
|
||||
@ -14,8 +14,14 @@ import { QuoteStatus } from "./QuoteStatus";
|
||||
export interface IQuoteProps {
|
||||
status: QuoteStatus;
|
||||
date: UTCDateValue;
|
||||
reference: string;
|
||||
language: Language;
|
||||
customer: string;
|
||||
currency: Currency;
|
||||
paymentMethod: string;
|
||||
notes: string;
|
||||
validity: string;
|
||||
|
||||
items: ICollection<QuoteItem>;
|
||||
}
|
||||
|
||||
@ -24,6 +30,7 @@ export interface IQuote {
|
||||
|
||||
status: QuoteStatus;
|
||||
date: UTCDateValue;
|
||||
|
||||
language: Language;
|
||||
currency: Currency;
|
||||
items: ICollection<QuoteItem>;
|
||||
|
||||
@ -53,13 +53,9 @@ export class QuoteRepository extends SequelizeRepository<Quote> implements IQuot
|
||||
}
|
||||
|
||||
public async findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<any>> {
|
||||
const { rows, count } = await this._findAll(
|
||||
"Quote_Model",
|
||||
queryCriteria
|
||||
/*{
|
||||
include: [], // esto es para quitar las asociaciones al hacer la consulta
|
||||
}*/
|
||||
);
|
||||
const { rows, count } = await this._findAll("Quote_Model", queryCriteria, {
|
||||
include: [], // esto es para quitar las asociaciones al hacer la consulta
|
||||
});
|
||||
|
||||
return this.mapper.mapArrayAndCountToDomain(rows, count);
|
||||
}
|
||||
|
||||
@ -59,8 +59,6 @@ export class ListDealersController extends ExpressController {
|
||||
try {
|
||||
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
||||
|
||||
console.log(queryCriteria);
|
||||
|
||||
const result: ListDealersResult = await this.useCase.execute({
|
||||
queryCriteria,
|
||||
});
|
||||
|
||||
@ -59,8 +59,6 @@ export class ListQuotesController extends ExpressController {
|
||||
try {
|
||||
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
||||
|
||||
console.log(queryCriteria);
|
||||
|
||||
const result: ListQuotesResult = await this.useCase.execute({
|
||||
queryCriteria,
|
||||
});
|
||||
|
||||
@ -1,23 +1,17 @@
|
||||
import { ListQuotesUseCase } from "@/contexts/sales/application";
|
||||
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
|
||||
import Express from "express";
|
||||
import { ISalesContext } from "../../../../Sales.context";
|
||||
import { ListQuotesController } from "./ListQuotes.controller";
|
||||
import { ListQuotesPresenter } from "./presenter";
|
||||
|
||||
export const listQuotesController = (
|
||||
req: Express.Request,
|
||||
res: Express.Response,
|
||||
next: Express.NextFunction
|
||||
) => {
|
||||
const context: ISalesContext = res.locals.context;
|
||||
|
||||
export const listQuotesController = (context: ISalesContext) => {
|
||||
registerQuoteRepository(context);
|
||||
|
||||
return new ListQuotesController(
|
||||
{
|
||||
useCase: new ListQuotesUseCase(context),
|
||||
presenter: ListQuotesPresenter,
|
||||
},
|
||||
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 { ISalesContext } from "../../../../../Sales.context";
|
||||
|
||||
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 = {
|
||||
// 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 {
|
||||
id: quote.id.toString(),
|
||||
status: quote.status.toString(),
|
||||
@ -25,9 +33,33 @@ export const ListQuotesPresenter: IListQuotesPresenter = {
|
||||
precision: 2,
|
||||
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
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
InferCreationAttributes,
|
||||
Model,
|
||||
NonAttribute,
|
||||
Op,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
import { Dealer_Model } from "./dealer.model";
|
||||
@ -36,12 +37,19 @@ export class Quote_Model extends Model<
|
||||
}
|
||||
|
||||
declare id: string;
|
||||
declare status: string;
|
||||
declare status: CreationOptional<string>;
|
||||
declare date: CreationOptional<string>;
|
||||
declare language_code: string;
|
||||
declare currency_code: string;
|
||||
declare subtotal: number;
|
||||
declare total: number;
|
||||
declare reference: CreationOptional<string>;
|
||||
declare lang_code: CreationOptional<string>;
|
||||
declare customer_information: CreationOptional<string>;
|
||||
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 dealer?: NonAttribute<Dealer_Model>;
|
||||
@ -65,14 +73,36 @@ export default (sequelize: Sequelize) => {
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
language_code: {
|
||||
reference: {
|
||||
type: new DataTypes.STRING(),
|
||||
},
|
||||
|
||||
lang_code: {
|
||||
type: DataTypes.STRING(2),
|
||||
allowNull: false,
|
||||
defaultValue: "es",
|
||||
},
|
||||
|
||||
currency_code: {
|
||||
type: new DataTypes.STRING(),
|
||||
type: new DataTypes.STRING(3),
|
||||
allowNull: false,
|
||||
defaultValue: "EUR",
|
||||
},
|
||||
|
||||
customer_information: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
payment_method: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
validity: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
subtotal: {
|
||||
@ -80,6 +110,11 @@ export default (sequelize: Sequelize) => {
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
discount: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
total: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
@ -99,8 +134,28 @@ export default (sequelize: Sequelize) => {
|
||||
|
||||
indexes: [
|
||||
{ name: "status_idx", fields: ["status"] },
|
||||
{ name: "reference_idx", fields: ["reference"] },
|
||||
{ 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 { IServerError } from "@/contexts/common/domain/errors";
|
||||
import { ExpressController } from "@/contexts/common/infrastructure/express";
|
||||
import {
|
||||
ListUsersResult,
|
||||
ListUsersUseCase,
|
||||
} from "@/contexts/users/application";
|
||||
import { ListUsersResult, ListUsersUseCase } from "@/contexts/users/application";
|
||||
import { User } from "@/contexts/users/domain";
|
||||
import {
|
||||
ICollection,
|
||||
@ -29,7 +26,7 @@ export class ListUsersController extends ExpressController {
|
||||
useCase: ListUsersUseCase;
|
||||
presenter: IListUsersPresenter;
|
||||
},
|
||||
context: IUserContext,
|
||||
context: IUserContext
|
||||
) {
|
||||
super();
|
||||
|
||||
@ -60,10 +57,7 @@ export class ListUsersController extends ExpressController {
|
||||
const queryParams = queryOrError.object;
|
||||
|
||||
try {
|
||||
const queryCriteria: IQueryCriteria =
|
||||
QueryCriteriaService.parse(queryParams);
|
||||
|
||||
console.log(queryCriteria);
|
||||
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
|
||||
|
||||
const result: ListUsersResult = await this.useCase.execute({
|
||||
queryCriteria,
|
||||
@ -79,7 +73,7 @@ export class ListUsersController extends ExpressController {
|
||||
this.presenter.mapArray(users, this.context, {
|
||||
page: queryCriteria.pagination.offset,
|
||||
limit: queryCriteria.pagination.limit,
|
||||
}),
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
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";
|
||||
|
||||
export const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||
|
||||
/*quoteRoutes.get("/", isAdmin, listQuotesController);
|
||||
quoteRoutes.get("/:quoteId", isUser, getQuoteMiddleware, getQuoteController);
|
||||
quoteRoutes.get(
|
||||
"/",
|
||||
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.put("/:quoteId", isAdmin, updateQuoteController);
|
||||
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
|
||||
|
||||
quoteRoutes.get("/", checkisAdmin, (req, res) => {
|
||||
console.log(req.params);
|
||||
res.status(200).json();
|
||||
});
|
||||
|
||||
export const QuoteRouter = (appRouter: Express.Router) => {
|
||||
appRouter.use("/quotes", quoteRoutes);
|
||||
};
|
||||
|
||||
@ -3,10 +3,11 @@ import { IMoney_Response_DTO } from "../../../../common";
|
||||
export interface IListArticles_Response_DTO {
|
||||
id: string;
|
||||
catalog_name: string;
|
||||
//id_article: string;
|
||||
//reference: string;
|
||||
id_article: string;
|
||||
reference: string;
|
||||
//family: string;
|
||||
//subfamily: string;
|
||||
|
||||
description: string;
|
||||
points: number;
|
||||
retail_price: IMoney_Response_DTO;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import Joi from "joi";
|
||||
import { Currencies } from "../../../../../utilities/currencies";
|
||||
import { RuleValidator } from "../../RuleValidator";
|
||||
import { DomainError, handleDomainError } from "../../errors";
|
||||
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
||||
import { Result } from "../Result";
|
||||
import { Currencies } from "./currencies";
|
||||
|
||||
export interface ICurrency {
|
||||
symbol: string;
|
||||
|
||||
@ -4,9 +4,9 @@ import { Result } from "../Result";
|
||||
import Joi from "joi";
|
||||
|
||||
import { UndefinedOr } from "../../../../../utilities";
|
||||
import { LANGUAGES_LIST } from "../../../../../utilities/languages_data";
|
||||
import { DomainError, handleDomainError } from "../../errors";
|
||||
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
||||
import { LANGUAGES_LIST } from "./languages_data";
|
||||
|
||||
export interface ILanguage {
|
||||
code: string;
|
||||
@ -39,7 +39,7 @@ export class Language extends NullableValueObject<ILanguage> {
|
||||
public static createFromCode(languageCode: string, options: ILanguageOptions = {}) {
|
||||
const _options = {
|
||||
...options,
|
||||
label: options.label ? options.label : "language_code",
|
||||
label: options.label ? options.label : "lang_code",
|
||||
};
|
||||
|
||||
const validationResult = Language.validate(languageCode, _options);
|
||||
|
||||
@ -5,8 +5,13 @@ export interface ICreateQuote_Request_DTO {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
reference: string;
|
||||
customer_information: string;
|
||||
lang_code: string;
|
||||
currency_code: string;
|
||||
payment_method: string;
|
||||
notes: string;
|
||||
validity: string;
|
||||
|
||||
items: ICreateQuoteItem_Request_DTO[];
|
||||
}
|
||||
@ -22,8 +27,14 @@ export function ensureCreateQuote_Request_DTOIsValid(quoteDTO: ICreateQuote_Requ
|
||||
const schema = Joi.object({
|
||||
id: Joi.string(),
|
||||
date: Joi.string(),
|
||||
language: Joi.string(),
|
||||
currency: Joi.string(),
|
||||
reference: 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(
|
||||
Joi.object({
|
||||
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 {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
reference: string;
|
||||
customer_information: string;
|
||||
lang_code: string;
|
||||
currency_code: string;
|
||||
payment_method: string;
|
||||
notes: string;
|
||||
validity: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user