This commit is contained in:
David Arranz 2024-07-10 20:54:33 +02:00
parent 95882ee10d
commit 0b5f180b6f
14 changed files with 229 additions and 120 deletions

View File

@ -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",

View File

@ -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);
}

View File

@ -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<SortableProps>) {
export function SortableTableRow({ id, children }: PropsWithChildren<SortableProps>) {
const {
attributes,
isDragging,
@ -58,7 +50,7 @@ export function SortableTableRow({
listeners,
ref: setActivatorNodeRef,
}),
[attributes, listeners, setActivatorNodeRef],
[attributes, listeners, setActivatorNodeRef]
);
return (
@ -66,10 +58,7 @@ export function SortableTableRow({
<TableRow
key={id}
id={String(id)}
className={cn(
isDragging ? "opacity-40" : "opacity-100",
"hover:bg-muted/30 m-0",
)}
className={cn(isDragging ? "opacity-40" : "opacity-100", "hover:bg-muted/30 m-0")}
ref={setNodeRef}
style={style}
>

View File

@ -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 <FormMoneyField {...register(`items.${index}.unit_price`)} />;
return (
<FormCurrencyField
variant='outline'
currency={currency}
precision={4}
{...register(`items.${index}.unit_price`)}
/>
);
},
},
{
@ -68,7 +75,7 @@ export const QuoteDetailsCardEditor = () => {
header: "subtotal_price",
size: 10,
cell: ({ row: { index }, column: { id } }) => {
return <FormMoneyField {...register(`items.${index}.subtotal_price`)} />;
return <FormTextField {...register(`items.${index}.subtotal_price`)} />;
},
},
{
@ -77,7 +84,7 @@ export const QuoteDetailsCardEditor = () => {
header: "discount",
size: 5,
cell: ({ row: { index }, column: { id } }) => {
return <FormPercentageField {...register(`items.${index}.discount`)} />;
return <FormTextField {...register(`items.${index}.discount`)} />;
},
},
{
@ -86,7 +93,7 @@ export const QuoteDetailsCardEditor = () => {
header: "total_price",
size: 10,
cell: ({ row: { index }, column: { id } }) => {
return <FormMoneyField {...register(`items.${index}.total_price`)} />;
return <FormTextField {...register(`items.${index}.total_price`)} />;
},
},
],

View File

@ -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, b extends string> = T extends `${infer f}${b}` ? T : never;
const endsWith = <T extends string, b extends string>(str: T, prefix: b): str is EndsWith<T, b> =>
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>(
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<QuoteDataForm>({
@ -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<QuoteDataForm> = 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 <LoadingOverlay />;
}
console.log(quoteCurrency);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
@ -175,7 +168,7 @@ export const QuoteEdit = () => {
</div>
</div>
<FormMoneyField
<FormTextField
label={"subtotal_price"}
disabled={form.formState.disabled}
{...form.register("subtotal_price")}
@ -191,7 +184,7 @@ export const QuoteEdit = () => {
<QuoteGeneralCardEditor />
</TabsContent>
<TabsContent value='items'>
<QuoteDetailsCardEditor />
<QuoteDetailsCardEditor currency={quoteCurrency} />
</TabsContent>
<TabsContent value='history'></TabsContent>

View File

@ -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<TData, TValue>(
if (enableSelectionColumn) {
columns.unshift({
id: "select",
header: ({ table }) => (
/*header: ({ table }) => (
<Checkbox
id='select-all'
checked={
@ -50,9 +50,10 @@ export function useDetailColumns<TData, TValue>(
}
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<TData>; table: Table<TData> }) => (
<Checkbox
@ -61,25 +62,7 @@ export function useDetailColumns<TData, TValue>(
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<TData, TValue>(
if (enableDragHandleColumn) {
columns.unshift({
id: "row_drag_handle",
header: () => (
/*header: () => (
<UnfoldVertical aria-label='Mover fila' className='items-center justify-center w-4 h-4' />
),
cell: ({ row }: { row: Row<TData> }) => <DataTableRowDragHandleCell rowId={row.id} />,
),*/
header: () => null,
cell: (info) => <DataTableRowDragHandleCell rowId={info.row.id} />,
size: 16,
enableSorting: false,
enableHiding: false,

View File

@ -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}
>
<GripVerticalIcon className="w-4 h-4" />
<span className="sr-only">Mover fila</span>
<GripVerticalIcon className='w-4 h-4' />
<span className='sr-only'>Mover fila</span>
</Button>
);
};

View File

@ -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<TFieldValues> = FieldPath<TFieldValues>
> = {
button?: (props?: React.PropsWithChildren) => React.ReactNode;
} & InputProps &
FormInputProps &
Partial<FormLabelProps> &
FormInputWithIconProps &
UseControllerProps<TFieldValues, TName> &
VariantProps<typeof formCurrencyFieldVariants> & {
currency: CurrencyData;
precision: number;
};
export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrencyFieldProps>(
(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 (
<FormField
defaultValue={defaultValue}
control={control}
name={name}
disabled={disabled}
rules={rules}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => {
return (
<FormItem ref={ref} className={cn(className, "space-y-3")}>
{label && <FormLabel label={label} hint={hint} required={rules?.required ?? false} />}
<FormControl>
<CurrencyInput
name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras
onBlur={field.onBlur}
disabled={field.disabled}
className={cn(formCurrencyFieldVariants({ variant, className }))}
suffix={` ${currency?.symbol}`}
groupSeparator='.'
decimalSeparator=','
//placeholder={`0 ${fieldCurrenty.value?.symbol}`}
//fixedDecimalLength={precision} <- no activar para que sea más cómodo escribir las cantidades
decimalsLimit={precision}
decimalScale={precision}
value={transformToInput(field.value)}
onValueChange={(value) => {
// "value" ya viene con los "0" de la precisión
field.onChange(value ?? "");
}}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
<FormErrorMessage />
</FormItem>
);
}}
/>
);
}
);
FormCurrencyField.displayName = "FormCurrencyField";

View File

@ -36,6 +36,8 @@ export const FormTextField = React.forwardRef<
trailIcon,
button,
defaultValue,
type,
} = props;
@ -43,6 +45,7 @@ export const FormTextField = React.forwardRef<
return (
<FormField
defaultValue={defaultValue}
control={control}
name={name}
rules={{ required }}

View File

@ -1,3 +1,4 @@
export * from "./FormCurrencyField";
export * from "./FormDatePickerField";
export * from "./FormErrorMessage";
export * from "./FormGroup";

View File

@ -28,7 +28,6 @@ export const AuthProvider = ({
};
const handleCheck = async () => {
console.trace("check");
try {
return Promise.resolve(authActions.check?.());
} catch (error) {

View File

@ -187,9 +187,9 @@ export function useDataTable<TData, TValue>({
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
debugTable: true,
debugHeaders: true,
debugColumns: true,
debugTable: false,
debugHeaders: false,
debugColumns: false,
});
return { table };

View File

@ -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[];

View File

@ -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 {