This commit is contained in:
David Arranz 2024-07-03 17:15:52 +02:00
parent a32ba80bb6
commit 1ac0533de5
15 changed files with 244 additions and 209 deletions

View File

@ -1,11 +1,17 @@
import { CancelButton, FormDatePickerField, FormTextAreaField, FormTextField } from "@/components";
import {
BackHistoryButton,
FormDatePickerField,
FormTextAreaField,
FormTextField,
} from "@/components";
import { t } from "i18next";
import { ChevronLeft } from "lucide-react";
import { SubmitButton } from "@/components";
import { Button, Form } from "@/ui";
import { SubmitHandler, useForm } from "react-hook-form";
import { FieldErrors, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useQuotes } from "./hooks";
type QuoteDataForm = {
@ -32,14 +38,15 @@ export const QuoteCreate = () => {
//const { data: userIdentity } = useGetIdentity();
//console.log(userIdentity);
const navigate = useNavigate();
const { useMutation } = useQuotes();
const { mutate } = useMutation;
const { mutate } = useMutation();
const form = useForm<QuoteDataForm>({
defaultValues: {
reference: "",
date: Date.now().toLocaleString(),
date: new Date(Date.now()).toUTCString(),
customer_information: "",
reference: "",
},
});
@ -48,15 +55,25 @@ export const QuoteCreate = () => {
try {
//setLoading(true);
mutate(formData);
mutate(formData, {
onSuccess: (data) => {
navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true });
},
});
} finally {
//setLoading(false);
}
};
const onErrors: SubmitErrorHandler<QuoteDataForm> = async (
errors: FieldErrors<QuoteDataForm>
) => {
console.log(errors);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<form onSubmit={form.handleSubmit(onSubmit, onErrors)}>
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
<div className='flex items-center gap-4'>
<Button variant='outline' size='icon' className='h-7 w-7'>
@ -70,47 +87,35 @@ export const QuoteCreate = () => {
<div className='grid max-w-lg gap-6'>
<FormTextField
className='row-span-2'
name='reference'
required
label={t("quotes.create.form_fields.reference.label")}
description={t("quotes.create.form_fields.reference.desc")}
disabled={form.formState.disabled}
placeholder={t("quotes.create.form_fields.reference.placeholder")}
{...form.register("reference", {
required: false,
})}
/>
<FormDatePickerField
required
label={t("quotes.create.form_fields.date.label")}
description={t("quotes.create.form_fields.date.desc")}
disabled={form.formState.disabled}
placeholder={t("quotes.create.form_fields.date.placeholder")}
{...form.register("date", {
required: true,
})}
name='date'
/>
<div className='grid grid-cols-1 grid-rows-2 gap-6'>
<FormTextAreaField
className='row-span-2'
required
label={t("quotes.create.form_fields.customer_information.label")}
description={t("quotes.create.form_fields.customer_information.desc")}
disabled={form.formState.disabled}
placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
{...form.register("customer_information", {
required: true,
})}
errors={form.formState.errors}
/>
</div>
<FormTextAreaField
rows={4}
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")}
/>
</div>
<div className='flex items-center justify-center gap-2'>
<CancelButton
variant='outline'
size='sm'
label={t("quotes.create.buttons.discard")}
></CancelButton>
<div className='flex items-center justify-start gap-2'>
<BackHistoryButton size='sm' label={t("quotes.create.buttons.discard")} url='/quotes' />
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton>
</div>

View File

@ -19,32 +19,36 @@ export const useQuotes = (params?: UseQuotesGetParamsType) => {
const keys = useQueryKey();
return {
useQuery: useOne<IGetQuote_Response_DTO>({
queryKey: keys().data().resource("quotes").action("one").id("").params().get(),
queryFn: () =>
dataSource.getOne({
resource: "quotes",
id: "",
}),
...params,
}),
useMutation: useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({
mutationKey: keys().data().resource("quotes").action("one").id("").params().get(),
mutationFn: (data) => {
let { id } = data;
useQuery: () =>
useOne<IGetQuote_Response_DTO>({
queryKey: keys().data().resource("quotes").action("one").id("").params().get(),
queryFn: () =>
dataSource.getOne({
resource: "quotes",
id: "",
}),
...params,
}),
useMutation: () =>
useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({
mutationKey: keys().data().resource("quotes").action("one").id("").params().get(),
mutationFn: (data) => {
let { id, status } = data;
if (!id) {
id = UniqueID.generateNewID().object.toString();
}
if (!id) {
id = UniqueID.generateNewID().object.toString();
status = "draft";
}
return dataSource.createOne({
resource: "quotes",
data: {
...data,
id,
},
});
},
}),
return dataSource.createOne({
resource: "quotes",
data: {
...data,
status,
id,
},
});
},
}),
};
};

View File

@ -4,12 +4,9 @@ export interface CancelButtonProps extends ButtonProps {
label?: string;
}
export const CancelButton = ({
label = "Cancelar",
...props
}: CancelButtonProps): JSX.Element => {
export const CancelButton = ({ label = "Cancelar", ...props }: CancelButtonProps): JSX.Element => {
return (
<Button type="button" variant="secondary" {...props}>
<Button type='button' variant='secondary' {...props}>
{label}
</Button>
);

View File

@ -18,9 +18,7 @@ const customButtonVariants = cva("", {
},
});
export interface CustomButtonProps
extends ButtonProps,
VariantProps<typeof customButtonVariants> {
export interface CustomButtonProps extends ButtonProps, VariantProps<typeof customButtonVariants> {
icon: LucideIcon; // Propiedad para proporcionar el icono personalizado
label?: string;
}

View File

@ -1,6 +1,7 @@
import { Button, ButtonProps } from "@/ui";
import { ChevronLeft } from "lucide-react";
import { To, useNavigate } from "react-router-dom";
import { CustomButton } from "../Buttons/CustomButton";
export interface BackHistoryButtonProps extends ButtonProps {
label?: string;
@ -15,16 +16,29 @@ export const BackHistoryButton = ({
}: BackHistoryButtonProps): JSX.Element => {
const navigate = useNavigate();
return (
<CustomButton
type='button'
label={label}
icon={ChevronLeft}
variant='ghost'
onClick={() => {
url ? navigate(url) : navigate(-1);
}}
{...props}
/>
);
return (
<Button
variant="ghost"
variant='ghost'
onClick={() => {
url ? navigate(url) : navigate(-1);
}}
size={size}
{...props}
>
<ChevronLeft className="w-4 h-4" />
<ChevronLeft className='w-4 h-4' />
<span className={size === "icon" ? "sr-only" : "ml-2"}>{label}</span>
</Button>
);

View File

@ -12,7 +12,6 @@ import {
FormDescription,
FormField,
FormItem,
FormMessage,
InputProps,
Popover,
PopoverContent,
@ -28,14 +27,12 @@ import { CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { t } from "i18next";
import { FormErrorMessage } from "./FormErrorMessage";
type FormDatePickerFieldProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = InputProps &
FormInputProps &
Partial<FormLabelProps> &
UseControllerProps<TFieldValues, TName> & {};
> = InputProps & FormInputProps & Partial<FormLabelProps> & UseControllerProps<TFieldValues, TName>;
/*const loadDateFnsLocale = async (locale: Locale) => {
return await import(`date-fns/locale/${locale.code}/index.js`);
@ -45,19 +42,7 @@ export const FormDatePickerField = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormDatePickerFieldProps
>((props: FormDatePickerFieldProps, ref) => {
const {
label,
placeholder,
hint,
description,
required,
className,
disabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
errors,
name,
type,
} = props;
const { label, placeholder, hint, description, required, className, name } = props;
const { control } = useFormContext();
//const { locale } = loadDateFnsLocale();
@ -72,11 +57,10 @@ export const FormDatePickerField = React.forwardRef<
control={control}
name={name}
rules={{ required }}
disabled={disabled}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => (
<FormItem ref={ref} className={cn(className, "flex flex-col")}>
{label && <FormLabel label={label} hint={hint} />}
{label && <FormLabel label={label} hint={hint} required={required} />}
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
@ -90,6 +74,8 @@ export const FormDatePickerField = React.forwardRef<
>
{field.value ? (
new Date(field.value).toLocaleDateString() //"en-US", DATE_OPTIONS)
) : placeholder ? (
placeholder
) : (
<span>{t("common.pick_date")}</span>
)}
@ -117,8 +103,9 @@ export const FormDatePickerField = React.forwardRef<
/>
</PopoverContent>
</Popover>
{description && <FormDescription>{description}</FormDescription>}
<FormMessage />
<FormErrorMessage />
</FormItem>
)}
/>

View File

@ -0,0 +1,30 @@
import { FormMessage, useFormField } from "@/ui";
import { t } from "i18next";
import * as React from "react";
export const FormErrorMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ children, ...props }, ref) => {
const { error } = useFormField();
let message = children;
if (error) {
if (error.message) {
message = String(error?.message || error.root?.message);
} else {
if (error.type === "required") {
message = t("common.required_field");
}
}
}
console.log(message);
return (
<FormMessage ref={ref} {...props}>
{message}
</FormMessage>
);
});
FormErrorMessage.displayName = "FormErrorMessage";

View File

@ -14,12 +14,14 @@ export const FormLabel = React.forwardRef<
FormLabelProps &
Pick<FormInputProps, "required">
>(({ label, hint, required, ...props }, ref) => {
const { error } = UI.useFormField();
const _hint = hint ? hint : required ? "obligatorio" : undefined;
const _hintClassName = required ? "text-destructive" : "";
const _hintClassName = error ? "text-destructive font-semibold" : "";
return (
<UI.FormLabel ref={ref} className='flex justify-between text-sm' {...props}>
<span className='block font-semibold'>{label}</span>
{_hint && <span className={`text-sm font-medium ${_hintClassName}`}>{_hint}</span>}
<span className={`block font-semibold ${_hintClassName}`}>{label}</span>
{_hint && <span className={`text-sm font-medium ${_hintClassName} `}>{_hint}</span>}
</UI.FormLabel>
);
});

View File

@ -5,7 +5,6 @@ import {
FormDescription,
FormField,
FormItem,
FormMessage,
Textarea,
} from "@/ui";
import * as React from "react";
@ -17,6 +16,7 @@ import {
UseControllerProps,
useFormContext,
} from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel";
export type FormTextAreaFieldProps<
@ -41,12 +41,14 @@ export const FormTextAreaField = React.forwardRef<
name,
label,
hint,
description,
placeholder,
description,
required,
disabled,
autoSize,
className,
autoSize,
...props
},
ref
@ -57,7 +59,6 @@ export const FormTextAreaField = React.forwardRef<
control={control}
name={name}
rules={{ required }}
disabled={disabled}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => (
<FormItem ref={ref} className={cn(className, "flex flex-col space-y-3")}>
@ -65,17 +66,29 @@ export const FormTextAreaField = React.forwardRef<
<FormControl className='grow'>
{autoSize ? (
<AutosizeTextarea
{...field}
placeholder={placeholder}
className='resize-y'
className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : "",
"resize-y"
)}
{...props}
{...field}
/>
) : (
<Textarea {...field} placeholder={placeholder} className='resize-y' {...props} />
<Textarea
placeholder={placeholder}
className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : "",
"resize-y"
)}
{...props}
{...field}
/>
)}
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
<FormMessage />
<FormErrorMessage />
</FormItem>
)}
/>

View File

@ -1,23 +1,10 @@
import { cn } from "@/lib/utils";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
Input,
InputProps,
} from "@/ui";
import { FormControl, FormDescription, FormField, FormItem, Input, InputProps } from "@/ui";
import * as React from "react";
import { createElement } from "react";
import {
FieldErrors,
FieldPath,
FieldValues,
UseControllerProps,
useFormContext,
} from "react-hook-form";
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel";
import { FormInputProps, FormInputWithIconProps } from "./FormProps";
@ -30,28 +17,25 @@ export type FormTextFieldProps<
FormInputProps &
Partial<FormLabelProps> &
FormInputWithIconProps &
UseControllerProps<TFieldValues, TName> & {
errors?: FieldErrors<TFieldValues>;
};
UseControllerProps<TFieldValues, TName>;
export const FormTextField = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormTextFieldProps
>((props, ref) => {
const {
name,
label,
placeholder,
hint,
placeholder,
description,
required,
className,
leadIcon,
trailIcon,
button,
disabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
errors,
name,
type,
} = props;
@ -62,55 +46,64 @@ export const FormTextField = React.forwardRef<
control={control}
name={name}
rules={{ required }}
disabled={disabled}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState, formState }) => (
<FormItem ref={ref} className={cn(className, "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" : "")}
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" : ""
)}
>
<Input type={type} placeholder={placeholder} disabled={disabled} {...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" : ""
)}
{...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>}
<FormMessage />
</FormItem>
)}
{description && <FormDescription>{description}</FormDescription>}
<FormErrorMessage />
</FormItem>
);
}}
/>
);
});

View File

@ -1,4 +1,5 @@
export * from "./FormDatePickerField";
export * from "./FormErrorMessage";
export * from "./FormGroup";
export * from "./FormLabel";
export * from "./FormMoneyField";

View File

@ -21,8 +21,8 @@
--secondary-foreground: 25 18% 30%;
--accent: 25 23% 83%;
--accent-foreground: 25 23% 23%;
--destructive: 13 96% 20%;
--destructive-foreground: 13 96% 80%;
--destructive: 0 72.2% 50.6%; /* 13 96% 20%; */
--destructive-foreground: 0 85.7% 97.3%; /* 13 96% 80%; */
--ring: 25 31% 75%;
--radius: 0.5rem;
}

View File

@ -29,7 +29,8 @@
"open_menu": "Abrir el menú",
"duplicate_rows": "Duplicar",
"duplicate_rows_tooltip": "Duplica las fila(s) seleccionadas(s)",
"pick_date": "Elige una fecha"
"pick_date": "Elige una fecha",
"required_field": "Este campo es obligatorio"
},
"main_menu": {
"home": "Inicio",

View File

@ -22,9 +22,7 @@ type FormFieldContextValue<
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
@ -66,22 +64,19 @@ type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
}
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
@ -105,18 +100,13 @@ const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
@ -146,7 +136,8 @@ const FormMessage = React.forwardRef<
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
const body = error && error.message ? String(error?.message || error.root?.message) : children;
if (!body) {
return null;

View File

@ -1,9 +1,8 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
@ -17,9 +16,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref}
{...props}
/>
)
);
}
)
Input.displayName = "Input"
);
Input.displayName = "Input";
export { Input }
export { Input };