This commit is contained in:
David Arranz 2024-07-11 18:40:46 +02:00
parent 8e80bfe31e
commit 3e73eac05a
17 changed files with 394 additions and 310 deletions

4
.vscode/launch.json vendored
View File

@ -12,7 +12,7 @@
{
"name": "Launch Chrome localhost",
"type": "pwa-chrome",
"type": "chrome",
"request": "launch",
"reAttach": true,
"url": "http://localhost:5173",
@ -26,6 +26,7 @@
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/client"
},
{
"type": "node",
"request": "attach",
@ -34,6 +35,7 @@
"restart": true,
"cwd": "${workspaceRoot}"
},
{
"name": "Launch via YARN",
"request": "launch",

View File

@ -271,7 +271,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
{
quantity: { amount: "123" },
description: "aaaa",
retail_price: {
unit_price: {
amount: "10000",
precision: 4,
currency: "EUR",
@ -336,9 +336,8 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className='hover:bg-transparent'>
{headerGroup.headers.map((header) => {
console.log(header.getSize());
return (
<TableHead key={header.id} className={`px-1 w-${header.getSize()}`}>
<TableHead key={header.id} className={`px-2 py-1 w-${header.getSize()}`}>
{header.isPlaceholder ? null : (
<DataTableColumnHeader table={table} header={header} />
)}

View File

@ -1,4 +1,9 @@
import { FormQuantityField, FormTextField } from "@/components";
import {
FormCurrencyField,
FormPercentageField,
FormQuantityField,
FormTextField,
} from "@/components";
import { DataTableProvider } from "@/lib/hooks";
import { cn } from "@/lib/utils";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui";
@ -11,7 +16,7 @@ import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
import { SortableDataTable } from "../SortableDataTable";
export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData }) => {
const { control, register } = useFormContext();
const { control, register, getValues } = useFormContext();
const { fields, ...fieldActions } = useFieldArray({
control,
@ -42,7 +47,7 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
return (
<FormQuantityField
variant='outline'
precision={2}
precision={Quantity.DEFAULT_PRECISION}
{...register(`items.${index}.quantity`)}
/>
);
@ -65,7 +70,7 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
),
cell: ({ row: { index }, column: { id } }) => {
return (
<FormTextField
<FormCurrencyField
variant='outline'
currency={currency}
precision={4}
@ -82,7 +87,16 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
<div className='text-right'>{t("quotes.form_fields.items.subtotal_price.label")}</div>
),
cell: ({ row: { index }, column: { id } }) => {
return <FormTextField {...register(`items.${index}.subtotal_price`)} />;
return (
<FormCurrencyField
variant='outline'
disabled
currency={currency}
precision={4}
className='text-right'
{...register(`items.${index}.subtotal_price`)}
/>
);
},
},
{
@ -93,7 +107,16 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
<div className='text-right'>{t("quotes.form_fields.items.discount.label")}</div>
),
cell: ({ row: { index }, column: { id } }) => {
return <FormTextField className='text-right' {...register(`items.${index}.discount`)} />;
return (
<>
<FormPercentageField
variant='outline'
precision={0}
className='text-right'
{...register(`items.${index}.discount`)}
/>
</>
);
},
},
{
@ -103,7 +126,16 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
<div className='text-right'>{t("quotes.form_fields.items.total_price.label")}</div>
),
cell: ({ row: { index }, column: { id } }) => {
return <FormTextField {...register(`items.${index}.total_price`)} />;
return (
<FormCurrencyField
variant='outline'
disabled
currency={currency}
precision={4}
className='text-right'
{...register(`items.${index}.total_price`)}
/>
);
},
},
],
@ -147,7 +179,7 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
fieldActions.append({
...newArticle,
quantity: {
amount: 1,
amount: 12,
precision: Quantity.DEFAULT_PRECISION,
},
unit_price: newArticle.retail_price,
@ -161,8 +193,6 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
const defaultLayout = [265, 440, 655];
const navCollapsedSize = 4;
return <SortableDataTable actions={fieldActions} columns={columns} data={fields} />;
return (
<ResizablePanelGroup
direction='horizontal'

View File

@ -17,30 +17,30 @@ export const QuoteGeneralCardEditor = () => {
<FormTextAreaField
className='row-span-2'
required
label={t("quotes.create.form_fields.customer_information.label")}
description={t("quotes.create.form_fields.customer_information.desc")}
label={t("quotes.form_fields.customer_information.label")}
description={t("quotes.form_fields.customer_information.desc")}
disabled={formState.disabled}
placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
placeholder={t("quotes.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")}
label={t("quotes.form_fields.reference.label")}
description={t("quotes.form_fields.reference.desc")}
disabled={formState.disabled}
placeholder={t("quotes.create.form_fields.reference.placeholder")}
placeholder={t("quotes.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")}
label={t("quotes.form_fields.date.label")}
description={t("quotes.form_fields.date.desc")}
disabled={formState.disabled}
placeholder={t("quotes.create.form_fields.date.placeholder")}
placeholder={t("quotes.form_fields.date.placeholder")}
{...register("date", {
required: true,
})}
@ -48,20 +48,20 @@ export const QuoteGeneralCardEditor = () => {
</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")}
label={t("quotes.form_fields.validity.label")}
description={t("quotes.form_fields.validity.desc")}
disabled={formState.disabled}
placeholder={t("quotes.create.form_fields.validity.placeholder")}
placeholder={t("quotes.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")}
label={t("quotes.form_fields.payment_method.label")}
description={t("quotes.form_fields.payment_method.desc")}
disabled={formState.disabled}
placeholder={t("quotes.create.form_fields.payment_method.placeholder")}
placeholder={t("quotes.form_fields.payment_method.placeholder")}
{...register("payment_method", {
required: false,
})}
@ -69,10 +69,10 @@ export const QuoteGeneralCardEditor = () => {
<FormTextAreaField
className='col-span-2'
label={t("quotes.create.form_fields.notes.label")}
description={t("quotes.create.form_fields.notes.desc")}
label={t("quotes.form_fields.notes.label")}
description={t("quotes.form_fields.notes.desc")}
disabled={formState.disabled}
placeholder={t("quotes.create.form_fields.notes.placeholder")}
placeholder={t("quotes.form_fields.notes.placeholder")}
{...register("notes", {
required: false,
})}
@ -100,20 +100,20 @@ export const QuoteGeneralCardEditor = () => {
</div>
<FormTextField
required
label={t("quotes.create.form_fields.lang_code.label")}
description={t("quotes.create.form_fields.lang_code.desc")}
label={t("quotes.form_fields.lang_code.label")}
description={t("quotes.form_fields.lang_code.desc")}
disabled={formState.disabled}
placeholder={t("quotes.create.form_fields.lang_code.placeholder")}
placeholder={t("quotes.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")}
label={t("quotes.form_fields.currency_code.label")}
description={t("quotes.form_fields.currency_code.desc")}
disabled={formState.disabled}
placeholder={t("quotes.create.form_fields.currency_code.placeholder")}
placeholder={t("quotes.form_fields.currency_code.placeholder")}
{...register("currency_code", {
required: true,
})}

View File

@ -90,16 +90,16 @@ export const QuoteCreate = () => {
className='row-span-2'
name='reference'
required
label={t("quotes.create.form_fields.reference.label")}
description={t("quotes.create.form_fields.reference.desc")}
placeholder={t("quotes.create.form_fields.reference.placeholder")}
label={t("quotes.form_fields.reference.label")}
description={t("quotes.form_fields.reference.desc")}
placeholder={t("quotes.form_fields.reference.placeholder")}
/>
<FormDatePickerField
required
label={t("quotes.create.form_fields.date.label")}
description={t("quotes.create.form_fields.date.desc")}
placeholder={t("quotes.create.form_fields.date.placeholder")}
label={t("quotes.form_fields.date.label")}
description={t("quotes.form_fields.date.desc")}
placeholder={t("quotes.form_fields.date.placeholder")}
name='date'
/>
@ -108,9 +108,9 @@ export const QuoteCreate = () => {
className='row-span-2'
name='customer_information'
required
label={t("quotes.create.form_fields.customer_information.label")}
description={t("quotes.create.form_fields.customer_information.desc")}
placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
label={t("quotes.form_fields.customer_information.label")}
description={t("quotes.form_fields.customer_information.desc")}
placeholder={t("quotes.form_fields.customer_information.placeholder")}
/>
<div className='flex items-center justify-around gap-2'>

View File

@ -1,4 +1,4 @@
import { ErrorOverlay, FormTextField, LoadingOverlay, SubmitButton } from "@/components";
import { ErrorOverlay, FormCurrencyField, 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";
@ -10,11 +10,6 @@ import { SubmitHandler, useForm } from "react-hook-form";
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
import { useQuotes } from "./hooks";
// simple typesafe helperfunction
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 {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -51,11 +46,6 @@ export const QuoteEdit = () => {
payment_method: "",
notes: "",
validity: "",
subtotal_price: {
amount: "",
precision: "",
currency_code: "",
},
items: [],
},
});
@ -67,21 +57,21 @@ export const QuoteEdit = () => {
// Transformación del form -> typo de request
mutate(data, {
onError: (error) => {
alert(error.message);
console.debug(error);
//alert(error.message);
},
//onSettled: () => {},
onSuccess: () => {
alert("guardado");
//alert("guardado");
},
});
};
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { unsubscribe } = watch((_, { name, type }) => {
const value = getValues();
//console.debug({ name, type });
if (name) {
if (name === "currency_code") {
setQuoteCurrency(
@ -97,6 +87,11 @@ export const QuoteEdit = () => {
// Recálculo líneas
items.map((item, index) => {
const itemTotals = calculateItemTotals(item);
if (itemTotals === null) {
return;
}
quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice);
setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject());
@ -107,17 +102,16 @@ export const QuoteEdit = () => {
setValue("subtotal_price", quoteSubtotal.toObject());
}
if (
endsWith(name, "quantity") ||
endsWith(name, "unit_price") ||
endsWith(name, "discount")
) {
if (name.endsWith("quantity") || name.endsWith("unit_price") || name.endsWith("discount")) {
const { items } = value;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [, indexString, fieldName] = String(name).split(".");
const index = parseInt(indexString);
const itemTotals = calculateItemTotals(items[index]);
if (itemTotals === null) {
return;
}
setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject());
setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject());
@ -141,8 +135,6 @@ export const QuoteEdit = () => {
return <LoadingOverlay />;
}
console.log(quoteCurrency);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
@ -168,7 +160,10 @@ export const QuoteEdit = () => {
</div>
</div>
<FormTextField
<FormCurrencyField
currency={quoteCurrency}
precision={4}
className='text-right'
label={"subtotal_price"}
disabled={form.formState.disabled}
{...form.register("subtotal_price")}

View File

@ -31,7 +31,7 @@ export function useDetailColumns<TData, TValue = unknown>(
if (enableSelectionColumn) {
columns.unshift({
id: "select",
/*header: ({ table }) => (
header: ({ table }) => (
<Checkbox
id='select-all'
checked={
@ -42,8 +42,8 @@ export function useDetailColumns<TData, TValue = unknown>(
aria-label='Seleccionar todo'
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

View File

@ -61,7 +61,9 @@ export const useQuotes = () => {
return dataSource.updateOne({
resource: "quotes",
id,
data,
data: {
...data,
},
});
},
}),

View File

@ -53,6 +53,7 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
disabled,
defaultValue,
rules,
readOnly,
precision,
currency,
variant,
@ -60,20 +61,35 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
const { control } = useFormContext();
const transformToInput = (value: any) => {
if (typeof value !== "object") {
return value;
}
const transform = {
input: (value: any) => {
if (typeof value !== "object") {
return value;
}
const moneyOrError = MoneyValue.create(value);
if (moneyOrError.isFailure) {
throw moneyOrError.error;
}
const moneyOrError = MoneyValue.create(value);
if (moneyOrError.isFailure) {
throw moneyOrError.error;
}
return moneyOrError.object
.convertPrecision(precision ?? value.precision)
.toUnit()
.toString();
return moneyOrError.object
.convertPrecision(precision ?? value.precision)
.toUnit()
.toString();
},
output: (value: string | undefined) => {
const moneyOrError = MoneyValue.create({
amount: value?.replace(",", "") ?? null,
precision,
currencyCode: currency.code,
});
if (moneyOrError.isFailure) {
throw moneyOrError.error;
}
return moneyOrError.object.toObject();
},
};
return (
@ -87,13 +103,16 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
render={({ field, fieldState, formState }) => {
return (
<FormItem ref={ref} className={cn(className, "space-y-3")}>
{label && <FormLabel label={label} hint={hint} required={rules?.required ?? false} />}
{label && (
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<FormControl>
<CurrencyInput
name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras
onBlur={field.onBlur}
disabled={field.disabled}
readOnly={readOnly}
className={cn(formCurrencyFieldVariants({ variant, className }))}
suffix={` ${currency?.symbol}`}
groupSeparator='.'
@ -102,11 +121,8 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
//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 ?? "");
}}
value={transform.input(field.value)}
onValueChange={(e) => field.onChange(transform.output(e))}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}

View File

@ -1,19 +1,32 @@
import { cn } from "@/lib/utils";
import { FormControl, FormDescription, FormItem, InputProps } from "@/ui";
import * as React from "react";
import { Percentage, PercentageObject } from "@shared/contexts";
import { createElement, forwardRef, useState } from "react";
import {
Controller,
FieldPath,
FieldValues,
UseControllerProps,
useFormContext,
} from "react-hook-form";
import { cn } from "@/lib/utils";
import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@/ui";
import { Percentage } from "@shared/contexts";
import { cva, type VariantProps } from "class-variance-authority";
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";
const formPercentageFieldVariants = 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: "focus-visible:border focus-visible:border-input",
},
},
defaultVariants: {
variant: "default",
},
}
);
export type FormPercentageFieldProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
@ -24,9 +37,12 @@ export type FormPercentageFieldProps<
FormInputProps &
Partial<FormLabelProps> &
FormInputWithIconProps &
UseControllerProps<TFieldValues, TName>;
UseControllerProps<TFieldValues, TName> &
VariantProps<typeof formPercentageFieldVariants> & {
precision: number;
};
export const FormPercentageField = forwardRef<
export const FormPercentageField = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormPercentageFieldProps
>((props, ref) => {
@ -34,42 +50,42 @@ export const FormPercentageField = forwardRef<
name,
label,
hint,
placeholder,
description,
required,
placeholder,
className,
leadIcon,
trailIcon,
button,
disabled,
defaultValue,
rules,
readOnly,
precision,
variant,
} = props;
const { control } = useFormContext();
const [precision, setPrecision] = useState<number>(Percentage.DEFAULT_PRECISION);
const transform = {
input: (value: PercentageObject) => {
const percentageOrError = Percentage.create(value);
input: (value: any) => {
if (typeof value !== "object") {
return value;
}
const percentageOrError = Percentage.create(value);
if (percentageOrError.isFailure) {
throw percentageOrError.error;
}
const percentageValue = percentageOrError.object;
setPrecision(percentageValue.getPrecision());
return percentageValue.toString();
return (
percentageOrError.object
.toNumber()
//.toPrecision(precision ?? value.precision)
.toString()
);
},
output: (event: React.ChangeEvent<HTMLInputElement>): PercentageObject => {
const value = parseFloat(event.target.value);
const output = !isNaN(value) ? value : 0;
output: (value: string | undefined) => {
const percentageOrError = Percentage.create({
amount: output * Math.pow(10, precision),
amount: value?.replace(",", "") ?? null,
precision,
});
if (percentageOrError.isFailure) {
throw percentageOrError.error;
}
@ -79,81 +95,39 @@ export const FormPercentageField = forwardRef<
};
return (
<Controller
<FormField
defaultValue={defaultValue}
control={control}
name={name}
disabled={disabled}
rules={{
required,
min: Percentage.MIN_VALUE,
max: Percentage.MAX_VALUE,
max: 100,
min: 0,
...rules,
}}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => {
return (
<input
type='number'
{...field}
className='text-right'
placeholder='number'
onChange={(e) => field.onChange(transform.output(e))}
value={transform.input(field.value)}
/>
);
render={({ field }) => {
return (
<FormItem ref={ref} className={cn(className, "space-y-3")}>
{label && <FormLabel label={label} hint={hint} required={required} />}
<div className={cn(button ? "flex" : null)}>
<div
className={cn(
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
)}
>
{leadIcon && (
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
{createElement(
leadIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
<FormControl
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
>
<input
type='number'
placeholder={placeholder}
className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
)}
{...field}
onInput={(e) => field.onChange(transform.output(e))}
value={transform.input(field.value)}
/>
</FormControl>
{trailIcon && (
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
{createElement(
trailIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
</div>
{button && <>{createElement(button)}</>}
</div>
{label && (
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<FormControl>
<CurrencyInput
name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras
onBlur={field.onBlur}
disabled={field.disabled}
readOnly={readOnly}
className={cn(formPercentageFieldVariants({ variant, className }))}
groupSeparator='.'
decimalSeparator=','
placeholder={placeholder}
decimalsLimit={precision}
decimalScale={precision}
value={transform.input(field.value)}
onValueChange={(e) => field.onChange(transform.output(e))}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
<FormErrorMessage />
</FormItem>

View File

@ -1,22 +1,23 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { FormControl, FormDescription, FormField, FormItem, Input, InputProps } from "@/ui";
import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@/ui";
import { Quantity } from "@shared/contexts";
import { cva, type VariantProps } from "class-variance-authority";
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";
const formQuantityFieldVariants = cva(
"text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
"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: "",
outline:
"border-0 focus-visible:border focus-visible:border-input focus-visible:ring-0 focus-visible:ring-offset-0 ",
default:
"border border-input ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ",
outline: "focus-visible:border focus-visible:border-input",
},
},
defaultVariants: {
@ -52,28 +53,42 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
disabled,
defaultValue,
rules,
readOnly,
precision,
variant,
} = props;
const { control } = useFormContext();
const transformToInput = (value: any) => {
if (typeof value !== "object") {
return value;
}
const transform = {
input: (value: any) => {
if (typeof value !== "object") {
return value;
}
const quantityOrError = Quantity.create(value);
if (quantityOrError.isFailure) {
throw quantityOrError.error;
}
const quantityOrError = Quantity.create(value);
if (quantityOrError.isFailure) {
throw quantityOrError.error;
}
return (
quantityOrError.object
.toNumber()
//.toPrecision(precision ?? value.precision)
.toString()
);
return (
quantityOrError.object
.toNumber()
//.toPrecision(precision ?? value.precision)
.toString()
);
},
output: (value: string | undefined) => {
const quantityOrError = Quantity.create({
amount: value?.replace(",", "") ?? null,
precision,
});
if (quantityOrError.isFailure) {
throw quantityOrError.error;
}
return quantityOrError.object.toObject();
},
};
return (
@ -83,26 +98,27 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
name={name}
disabled={disabled}
rules={rules}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field }) => {
return (
<FormItem ref={ref} className={cn(className, "space-y-3")}>
{label && <FormLabel label={label} hint={hint} required={rules?.required ?? false} />}
{label && (
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<FormControl>
<Input
type='number'
<CurrencyInput
name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras
onBlur={field.onBlur}
disabled={field.disabled}
readOnly={readOnly}
className={cn(formQuantityFieldVariants({ variant, className }))}
groupSeparator='.'
decimalSeparator=','
placeholder={placeholder}
value={transformToInput(field.value)}
onChange={(value) => {
// "value" ya viene con los "0" de la precisión
console.log(value);
field.onChange(value ?? "");
}}
decimalsLimit={precision}
decimalScale={precision}
value={transform.input(field.value)}
onValueChange={(e) => field.onChange(transform.output(e))}
/>
</FormControl>
{description && <FormDescription>{description}</FormDescription>}

View File

@ -1,6 +1,7 @@
import { cn } from "@/lib/utils";
import { FormControl, FormDescription, FormField, FormItem, Input, InputProps } from "@/ui";
import { cva } from "class-variance-authority";
import * as React from "react";
import { createElement } from "react";
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
@ -8,6 +9,19 @@ import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel";
import { FormInputProps, FormInputWithIconProps } from "./FormProps";
const FormTextFieldVariants = cva("", {
variants: {
variant: {
default: "",
outline:
"border-0 focus-visible:border focus-visible:border-input focus-visible:ring-0 focus-visible:ring-offset-0 ",
},
},
defaultVariants: {
variant: "default",
},
});
export type FormTextFieldProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
@ -19,94 +33,96 @@ export type FormTextFieldProps<
FormInputWithIconProps &
UseControllerProps<TFieldValues, TName>;
export const FormTextField = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormTextFieldProps
>((props, ref) => {
const {
name,
label,
hint,
placeholder,
description,
export const FormTextField = React.forwardRef<HTMLInputElement, FormTextFieldProps>(
(props, ref) => {
const {
name,
label,
hint,
description,
placeholder,
className,
disabled,
defaultValue,
rules,
type,
variant,
required,
className,
leadIcon,
trailIcon,
button,
button,
leadIcon,
trailIcon,
} = props;
defaultValue,
const { control } = useFormContext();
type,
} = props;
const { control } = useFormContext();
return (
<FormField
defaultValue={defaultValue}
control={control}
name={name}
rules={{ required }}
// 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={required} />}
<div className={cn(button ? "flex" : null)}>
<div
className={cn(
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
)}
>
{leadIcon && (
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
{React.createElement(
leadIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
<FormControl
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
return (
<FormField
defaultValue={defaultValue}
control={control}
name={name}
disabled={disabled}
rules={rules}
render={({ field, fieldState }) => {
return (
<FormItem ref={ref} className={cn(className, "space-y-3")}>
{label && (
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<div className={cn(button ? "flex" : null)}>
<div
className={cn(
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
)}
>
<Input
type={type}
placeholder={placeholder}
className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
)}
{...field}
/>
</FormControl>
{leadIcon && (
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
{React.createElement(
leadIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
{trailIcon && (
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
{createElement(
trailIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
<FormControl
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
>
<Input
type={type}
placeholder={placeholder}
className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : "",
FormTextFieldVariants({ variant, className })
)}
{...field}
/>
</FormControl>
{trailIcon && (
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
{createElement(
trailIcon,
{
className: "h-5 w-5 text-muted-foreground",
"aria-hidden": true,
},
null
)}
</div>
)}
</div>
{button && <>{createElement(button)}</>}
</div>
{button && <>{createElement(button)}</>}
</div>
{description && <FormDescription>{description}</FormDescription>}
<FormErrorMessage />
</FormItem>
);
}}
/>
);
});
{description && <FormDescription>{description}</FormDescription>}
<FormErrorMessage />
</FormItem>
);
}}
/>
);
}
);

View File

@ -11,22 +11,22 @@ export const calculateItemTotals = (item: {
subtotalPrice: MoneyValue;
discount: Percentage;
totalPrice: MoneyValue;
} => {
const { quantity: quantity_value, unit_price: unit_price_value, discount: discount_value } = item;
} | null => {
const { quantity: quantity_dto, unit_price: unit_price_dto, discount: discount_dto } = item;
const quantityOrError = Quantity.create(quantity_value);
const quantityOrError = Quantity.create(quantity_dto);
if (quantityOrError.isFailure) {
throw quantityOrError.error;
}
const quantity = quantityOrError.object;
const unitPriceOrError = MoneyValue.create(unit_price_value);
const unitPriceOrError = MoneyValue.create(unit_price_dto);
if (unitPriceOrError.isFailure) {
throw unitPriceOrError.error;
}
const unitPrice = unitPriceOrError.object;
const discountOrError = Percentage.create(discount_value);
const discountOrError = Percentage.create(discount_dto);
if (discountOrError.isFailure) {
throw discountOrError.error;
}

View File

@ -16,6 +16,7 @@ export * from "./useCustomDialog";
export * from "./useDataSource";
export * from "./useDataTable";
export * from "./useLocalization";
export * from "./useMediaQuery";
export * from "./usePagination";
export * from "./useTheme";
export * from "./useUnsavedChangesNotifier";

View File

@ -0,0 +1 @@
export * from "./useMediaQuery";

View File

@ -0,0 +1,19 @@
import * as React from "react";
export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false);
React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches);
}
const result = matchMedia(query);
result.addEventListener("change", onChange);
setValue(result.matches);
return () => result.removeEventListener("change", onChange);
}, [query]);
return value;
}

View File

@ -124,6 +124,9 @@
"title": "Cotización"
}
},
"edit": {
"title": "Cotización"
},
"status": {
"draft": "Borrador"
},
@ -138,6 +141,16 @@
"desc": "Referencia para esta cotización",
"placeholder": ""
},
"lang_code": {
"label": "Idioma",
"desc": "Idioma de la cotización",
"placeholder": ""
},
"currency_code": {
"label": "Moneda",
"desc": "Moneda de la cotización",
"placeholder": ""
},
"customer_information": {
"label": "Datos del cliente",
"desc": "Escriba el nombre del cliente en la primera línea, la direccion en la segunda y el código postal y ciudad en la tercera.",