diff --git a/client/package.json b/client/package.json index e9b4a6c..dd50aa8 100644 --- a/client/package.json +++ b/client/package.json @@ -54,6 +54,7 @@ "lucide-react": "^0.379.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", + "react-currency-input-field": "^3.8.0", "react-day-picker": "^8.10.1", "react-dom": "^18.2.0", "react-hook-form": "^7.51.5", diff --git a/client/src/app/quotes/components/CatalogPickerDataTable.tsx b/client/src/app/quotes/components/CatalogPickerDataTable.tsx index 2f9363d..c28c943 100644 --- a/client/src/app/quotes/components/CatalogPickerDataTable.tsx +++ b/client/src/app/quotes/components/CatalogPickerDataTable.tsx @@ -47,7 +47,6 @@ export const CatalogPickerDataTable = ({ onClick }: { onClick: (data: unknown) = className={cn("rounded-lg border p-3 transition-all hover:bg-accent w-full", "")} onClick={ (event) => { - console.log("hola"); event.preventDefault(); onClick && onClick(row.original); } diff --git a/client/src/app/quotes/components/SortableTableRow.tsx b/client/src/app/quotes/components/SortableTableRow.tsx index ec9e47f..b4e5546 100644 --- a/client/src/app/quotes/components/SortableTableRow.tsx +++ b/client/src/app/quotes/components/SortableTableRow.tsx @@ -3,12 +3,7 @@ import { TableRow } from "@/ui/table"; import { DraggableSyntheticListeners } from "@dnd-kit/core"; import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { - CSSProperties, - PropsWithChildren, - createContext, - useMemo, -} from "react"; +import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react"; import { SortableProps } from "./SortableDataTable"; interface Context { @@ -30,10 +25,7 @@ function animateLayoutChanges(args) { return true; } -export function SortableTableRow({ - id, - children, -}: PropsWithChildren) { +export function SortableTableRow({ id, children }: PropsWithChildren) { const { attributes, isDragging, @@ -58,7 +50,7 @@ export function SortableTableRow({ listeners, ref: setActivatorNodeRef, }), - [attributes, listeners, setActivatorNodeRef], + [attributes, listeners, setActivatorNodeRef] ); return ( @@ -66,10 +58,7 @@ export function SortableTableRow({ diff --git a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx index 6a61611..62328e6 100644 --- a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx +++ b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx @@ -1,21 +1,21 @@ import { - FormMoneyField, - FormPercentageField, + FormCurrencyField, FormQuantityField, FormTextAreaField, + FormTextField, } from "@/components"; import { DataTableProvider } from "@/lib/hooks"; import { cn } from "@/lib/utils"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui"; -import { Quantity } from "@shared/contexts"; +import { CurrencyData, Quantity } from "@shared/contexts"; import { useCallback, useState } from "react"; import { useFieldArray, useFormContext } from "react-hook-form"; import { useDetailColumns } from "../../hooks"; import { CatalogPickerDataTable } from "../CatalogPickerDataTable"; import { SortableDataTable } from "../SortableDataTable"; -export const QuoteDetailsCardEditor = () => { - const { control, register, watch, getValues, setValue } = useFormContext(); +export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData }) => { + const { control, register } = useFormContext(); const { fields, ...fieldActions } = useFieldArray({ control, @@ -59,7 +59,14 @@ export const QuoteDetailsCardEditor = () => { header: "unit_price", size: 10, cell: ({ row: { index }, column: { id } }) => { - return ; + return ( + + ); }, }, { @@ -68,7 +75,7 @@ export const QuoteDetailsCardEditor = () => { header: "subtotal_price", size: 10, cell: ({ row: { index }, column: { id } }) => { - return ; + return ; }, }, { @@ -77,7 +84,7 @@ export const QuoteDetailsCardEditor = () => { header: "discount", size: 5, cell: ({ row: { index }, column: { id } }) => { - return ; + return ; }, }, { @@ -86,7 +93,7 @@ export const QuoteDetailsCardEditor = () => { header: "total_price", size: 10, cell: ({ row: { index }, column: { id } }) => { - return ; + return ; }, }, ], diff --git a/client/src/app/quotes/edit.tsx b/client/src/app/quotes/edit.tsx index 97f4d1b..d8fc74a 100644 --- a/client/src/app/quotes/edit.tsx +++ b/client/src/app/quotes/edit.tsx @@ -1,8 +1,8 @@ -import { ErrorOverlay, FormMoneyField, LoadingOverlay, SubmitButton } from "@/components"; +import { ErrorOverlay, FormTextField, LoadingOverlay, SubmitButton } from "@/components"; import { calculateItemTotals } from "@/lib/calc"; import { useUrlId } from "@/lib/hooks/useUrlId"; import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui"; -import { IUpdateQuote_Request_DTO, MoneyValue } from "@shared/contexts"; +import { CurrencyData, IUpdateQuote_Request_DTO, MoneyValue } from "@shared/contexts"; import { t } from "i18next"; import { ChevronLeftIcon } from "lucide-react"; import { useEffect, useState } from "react"; @@ -15,40 +15,28 @@ type EndsWith = T extends `${infer f}${b}` ? T : never; const endsWith = (str: T, prefix: b): str is EndsWith => str.endsWith(prefix); -interface QuoteDataForm extends IUpdateQuote_Request_DTO { - /*status: string; - date: string; - reference: string; - customer_information: string; - lang_code: string; - currency_code: string; - payment_method: string; - notes: string; - validity: string; - discount: IPercentage; - - subtotal: IMoney; - - items: { - quantity: IQuantity; - description: string; - unit_price: IMoney; - price: IMoney; - discount: IPercentage; - total: IMoney; - }[];*/ -} +interface QuoteDataForm extends IUpdateQuote_Request_DTO {} // eslint-disable-next-line @typescript-eslint/no-unused-vars export const QuoteEdit = () => { - const [loading, setLoading] = useState(false); const quoteId = useUrlId(); + const [quoteCurrency, setQuoteCurrency] = useState( + CurrencyData.createDefaultCode().object + ); - //const { data: userIdentity } = useGetIdentity(); + /*const { data: userIdentity } = useGetIdentity(); + console.log(userIdentity); + + const { flag } = useLocalization({ + locale: userIdentity?.language ?? "es-es", + }); + + console.log(flag);*/ const { useOne, useUpdate } = useQuotes(); - const { data, status } = useOne(quoteId); + const { data, status, error: queryError } = useOne(quoteId); + const { mutate } = useUpdate(String(quoteId)); const form = useForm({ @@ -63,7 +51,11 @@ export const QuoteEdit = () => { payment_method: "", notes: "", validity: "", - subtotal_price: "", + subtotal_price: { + amount: "", + precision: "", + currency_code: "", + }, items: [], }, }); @@ -72,32 +64,32 @@ export const QuoteEdit = () => { const { isSubmitting } = formState; const onSubmit: SubmitHandler = async (data) => { - console.debug(JSON.stringify(data)); - - try { - setLoading(true); - // Transformación del form -> typo de request - mutate(data, { - onError: (error) => { - alert(error); - }, - //onSettled: () => {}, - onSuccess: () => { - alert("guardado"); - }, - }); - } finally { - setLoading(false); - } + // Transformación del form -> typo de request + mutate(data, { + onError: (error) => { + alert(error.message); + }, + //onSettled: () => {}, + onSuccess: () => { + alert("guardado"); + }, + }); }; useEffect(() => { const { unsubscribe } = watch((_, { name, type }) => { const value = getValues(); - console.debug({ name, type }); + //console.debug({ name, type }); if (name) { + if (name === "currency_code") { + setQuoteCurrency( + CurrencyData.createFromCode(value.lang_code ?? CurrencyData.DEFAULT_CURRENCY_CODE) + .object + ); + } + if (name === "items") { const { items } = value; let quoteSubtotal = MoneyValue.create().object; @@ -111,18 +103,17 @@ export const QuoteEdit = () => { setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject()); }); - console.log(quoteSubtotal.toFormat()); - // Recálculo completo - setValue("subtotal", quoteSubtotal.toObject()); + setValue("subtotal_price", quoteSubtotal.toObject()); } if ( endsWith(name, "quantity") || - endsWith(name, "retail_price") || + endsWith(name, "unit_price") || endsWith(name, "discount") ) { const { items } = value; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [, indexString, fieldName] = String(name).split("."); const index = parseInt(indexString); @@ -150,6 +141,8 @@ export const QuoteEdit = () => { return ; } + console.log(quoteCurrency); + return (
@@ -175,7 +168,7 @@ export const QuoteEdit = () => { - { - + diff --git a/client/src/app/quotes/hooks/useDetailColumns.tsx b/client/src/app/quotes/hooks/useDetailColumns.tsx index 3024a83..7000d97 100644 --- a/client/src/app/quotes/hooks/useDetailColumns.tsx +++ b/client/src/app/quotes/hooks/useDetailColumns.tsx @@ -5,7 +5,7 @@ import { } from "@/components"; import { Checkbox } from "@/ui"; import { ColumnDef, Row, Table } from "@tanstack/react-table"; -import { MoreHorizontalIcon, UnfoldVertical } from "lucide-react"; +import { MoreHorizontalIcon } from "lucide-react"; import { useMemo } from "react"; @@ -41,7 +41,7 @@ export function useDetailColumns( if (enableSelectionColumn) { columns.unshift({ id: "select", - header: ({ table }) => ( + /*header: ({ table }) => ( ( } onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} aria-label='Seleccionar todo' - className='translate-y-[2px]' + className='translate-y-[0px]' /> - ), + ),*/ + header: () => null, // eslint-disable-next-line @typescript-eslint/no-unused-vars cell: ({ row, table }: { row: Row; table: Table }) => ( ( 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; - }}*/ + className='mt-2' /> ), enableSorting: false, @@ -91,10 +74,11 @@ export function useDetailColumns( if (enableDragHandleColumn) { columns.unshift({ id: "row_drag_handle", - header: () => ( + /*header: () => ( - ), - cell: ({ row }: { row: Row }) => , + ),*/ + header: () => null, + cell: (info) => , size: 16, enableSorting: false, enableHiding: false, diff --git a/client/src/components/DataTable/DataTableRowDragHandleCell.tsx b/client/src/components/DataTable/DataTableRowDragHandleCell.tsx index ed4409d..6cb6477 100644 --- a/client/src/components/DataTable/DataTableRowDragHandleCell.tsx +++ b/client/src/components/DataTable/DataTableRowDragHandleCell.tsx @@ -3,7 +3,15 @@ import { Button } from "@/ui"; import { useSortable } from "@dnd-kit/sortable"; import { GripVerticalIcon } from "lucide-react"; -export const DataTableRowDragHandleCell = ({ rowId }: { rowId: string }) => { +export interface DataTableRowDragHandleCellProps { + rowId: string; + className?: string; +} + +export const DataTableRowDragHandleCell = ({ + rowId, + className, +}: DataTableRowDragHandleCellProps) => { const { attributes, listeners, isDragging } = useSortable({ id: rowId, }); @@ -14,17 +22,18 @@ export const DataTableRowDragHandleCell = ({ rowId }: { rowId: string }) => { event.preventDefault(); return; }} - size="icon" - variant="link" + size='icon' + variant='link' className={cn( isDragging ? "cursor-grabbing" : "cursor-grab", - "w-4 h-4 translate-y-[2px]" + "w-4 h-4 mt-2 text-ring hover:text-muted-foreground", + className )} {...attributes} {...listeners} > - - Mover fila + + Mover fila ); }; diff --git a/client/src/components/Forms/FormCurrencyField.tsx b/client/src/components/Forms/FormCurrencyField.tsx new file mode 100644 index 0000000..bb8ad1f --- /dev/null +++ b/client/src/components/Forms/FormCurrencyField.tsx @@ -0,0 +1,122 @@ +import { cn } from "@/lib/utils"; +import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@/ui"; +import { CurrencyData, MoneyValue } from "@shared/contexts"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; +import CurrencyInput from "react-currency-input-field"; +import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form"; +import { FormErrorMessage } from "./FormErrorMessage"; +import { FormLabel, FormLabelProps } from "./FormLabel"; +import { FormInputProps, FormInputWithIconProps } from "./FormProps"; + +export const formCurrencyFieldVariants = cva( + "flex h-10 w-full rounded-md bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", + { + variants: { + variant: { + default: + "border border-input ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ", + outline: + "ring-offset-background focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 ", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export type FormCurrencyFieldProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + button?: (props?: React.PropsWithChildren) => React.ReactNode; +} & InputProps & + FormInputProps & + Partial & + FormInputWithIconProps & + UseControllerProps & + VariantProps & { + currency: CurrencyData; + precision: number; + }; + +export const FormCurrencyField = React.forwardRef( + (props, ref) => { + const { + name, + label, + hint, + description, + className, + disabled, + defaultValue, + rules, + precision, + currency, + variant, + } = props; + + const { control } = useFormContext(); + + const transformToInput = (value: any) => { + if (typeof value !== "object") { + return value; + } + + const moneyOrError = MoneyValue.create(value); + if (moneyOrError.isFailure) { + throw moneyOrError.error; + } + + return moneyOrError.object + .convertPrecision(precision ?? value.precision) + .toUnit() + .toString(); + }; + + return ( + { + return ( + + {label && } + + { + // "value" ya viene con los "0" de la precisión + field.onChange(value ?? ""); + }} + /> + + {description && {description}} + + + ); + }} + /> + ); + } +); + +FormCurrencyField.displayName = "FormCurrencyField"; diff --git a/client/src/components/Forms/FormTextField.tsx b/client/src/components/Forms/FormTextField.tsx index 5e2e7ad..4804279 100644 --- a/client/src/components/Forms/FormTextField.tsx +++ b/client/src/components/Forms/FormTextField.tsx @@ -36,6 +36,8 @@ export const FormTextField = React.forwardRef< trailIcon, button, + defaultValue, + type, } = props; @@ -43,6 +45,7 @@ export const FormTextField = React.forwardRef< return ( { - console.trace("check"); try { return Promise.resolve(authActions.check?.()); } catch (error) { diff --git a/client/src/lib/hooks/useDataTable/useDataTable.tsx b/client/src/lib/hooks/useDataTable/useDataTable.tsx index 066f36f..ca6c349 100644 --- a/client/src/lib/hooks/useDataTable/useDataTable.tsx +++ b/client/src/lib/hooks/useDataTable/useDataTable.tsx @@ -187,9 +187,9 @@ export function useDataTable({ getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), - debugTable: true, - debugHeaders: true, - debugColumns: true, + debugTable: false, + debugHeaders: false, + debugColumns: false, }); return { table }; diff --git a/shared/lib/contexts/sales/application/dto/Quote/CreateQuote.dto/ICreateQuote_Request.dto.ts b/shared/lib/contexts/sales/application/dto/Quote/CreateQuote.dto/ICreateQuote_Request.dto.ts index 75a9e77..58aa800 100644 --- a/shared/lib/contexts/sales/application/dto/Quote/CreateQuote.dto/ICreateQuote_Request.dto.ts +++ b/shared/lib/contexts/sales/application/dto/Quote/CreateQuote.dto/ICreateQuote_Request.dto.ts @@ -19,9 +19,9 @@ export interface ICreateQuote_Request_DTO { notes: string; validity: string; - subtotal: IMoney_Response_DTO; + subtotal_price: IMoney_Response_DTO; discount: IPercentage_Response_DTO; - total: IMoney_Response_DTO; + total_price: IMoney_Response_DTO; items: ICreateQuoteItem_Request_DTO[]; diff --git a/shared/lib/contexts/sales/application/dto/Quote/UpdateQuote.dto/IUpdateQuote_Request.dto.ts b/shared/lib/contexts/sales/application/dto/Quote/UpdateQuote.dto/IUpdateQuote_Request.dto.ts index 38108df..64362ed 100644 --- a/shared/lib/contexts/sales/application/dto/Quote/UpdateQuote.dto/IUpdateQuote_Request.dto.ts +++ b/shared/lib/contexts/sales/application/dto/Quote/UpdateQuote.dto/IUpdateQuote_Request.dto.ts @@ -1,8 +1,6 @@ import Joi from "joi"; import { - IMoney_Request_DTO, IMoney_Response_DTO, - IPercentage_Request_DTO, IPercentage_Response_DTO, IQuantity_Response_DTO, Result, @@ -20,9 +18,13 @@ export interface IUpdateQuote_Request_DTO { notes: string; validity: string; - subtotal: IMoney_Request_DTO; - discount: IPercentage_Request_DTO; + subtotal_price: IMoney_Response_DTO; + discount: IPercentage_Response_DTO; + total_price: IMoney_Response_DTO; + items: IUpdateQuoteItem_Request_DTO[]; + + dealer_id: string; } export interface IUpdateQuoteItem_Request_DTO {