.
This commit is contained in:
parent
8e80bfe31e
commit
3e73eac05a
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -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",
|
||||
|
||||
@ -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} />
|
||||
)}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
})}
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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")}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -61,7 +61,9 @@ export const useQuotes = () => {
|
||||
return dataSource.updateOne({
|
||||
resource: "quotes",
|
||||
id,
|
||||
data,
|
||||
data: {
|
||||
...data,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@ -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>}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
1
client/src/lib/hooks/useMediaQuery/index.ts
Normal file
1
client/src/lib/hooks/useMediaQuery/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./useMediaQuery";
|
||||
19
client/src/lib/hooks/useMediaQuery/useMediaQuery.tsx
Normal file
19
client/src/lib/hooks/useMediaQuery/useMediaQuery.tsx
Normal 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;
|
||||
}
|
||||
@ -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.",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user