Clientes
This commit is contained in:
parent
01ca0bd5dc
commit
c8e3191d05
@ -9,11 +9,11 @@ import {
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerFormData } from "../../schemas";
|
||||
import { CreateCustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerAdditionalConfigFields = () => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<CustomerFormData>();
|
||||
const { control } = useFormContext<CreateCustomerFormData>();
|
||||
|
||||
return (
|
||||
<Card className='border-0 shadow-none'>
|
||||
|
||||
@ -9,11 +9,11 @@ import {
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { COUNTRY_OPTIONS } from "../../constants";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerFormData } from "../../schemas";
|
||||
import { CreateCustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerAddressFields = () => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<CustomerFormData>();
|
||||
const { control } = useFormContext<CreateCustomerFormData>();
|
||||
|
||||
return (
|
||||
<Card className='border-0 shadow-none'>
|
||||
|
||||
@ -15,11 +15,11 @@ import {
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerFormData } from "../../schemas";
|
||||
import { CreateCustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerBasicInfoFields = () => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<CustomerFormData>();
|
||||
const { control } = useFormContext<CreateCustomerFormData>();
|
||||
|
||||
const isCompany = useWatch({
|
||||
control,
|
||||
|
||||
@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FieldErrors, FormProvider, useForm } from "react-hook-form";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { CreateCustomerFormSchema, CustomerFormData } from "../../schemas";
|
||||
import { CreateCustomerFormData, CreateCustomerFormSchema } from "../../schemas";
|
||||
import { FormDebug } from "../form-debug";
|
||||
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
|
||||
import { CustomerAddressFields } from "./customer-address-fields";
|
||||
@ -11,9 +11,9 @@ import { CustomerContactFields } from "./customer-contact-fields";
|
||||
|
||||
interface CustomerFormProps {
|
||||
formId: string;
|
||||
initialValues: CustomerFormData;
|
||||
onSubmit: (data: CustomerFormData) => void;
|
||||
onError: (errors: FieldErrors<CustomerFormData>) => void;
|
||||
initialValues: CreateCustomerFormData;
|
||||
onSubmit: (data: CreateCustomerFormData) => void;
|
||||
onError: (errors: FieldErrors<CreateCustomerFormData>) => void;
|
||||
disabled?: boolean;
|
||||
onDirtyChange: (isDirty: boolean) => void;
|
||||
}
|
||||
@ -26,7 +26,7 @@ export function CustomerEditForm({
|
||||
disabled,
|
||||
onDirtyChange,
|
||||
}: CustomerFormProps) {
|
||||
const form = useForm<CustomerFormData>({
|
||||
const form = useForm<CreateCustomerFormData>({
|
||||
resolver: zodResolver(CreateCustomerFormSchema),
|
||||
defaultValues: initialValues,
|
||||
disabled,
|
||||
|
||||
@ -2,11 +2,11 @@ import { useDataSource } from "@erp/core/hooks";
|
||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { CreateCustomerRequestSchema, CustomerCreationResponseDTO } from "../../common";
|
||||
import { CustomerFormData } from "../schemas";
|
||||
import { CreateCustomerFormData } from "../schemas";
|
||||
import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation";
|
||||
|
||||
type CreateCustomerPayload = {
|
||||
data: CustomerFormData;
|
||||
data: CreateCustomerFormData;
|
||||
};
|
||||
|
||||
export function useCreateCustomerMutation() {
|
||||
@ -34,8 +34,6 @@ export function useCreateCustomerMutation() {
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
console.debug(validationErrors);
|
||||
|
||||
throw new ValidationErrorCollection("Validation failed", validationErrors);
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { CustomerData, CustomerUpdateData } from "../schemas";
|
||||
import { UpdateCustomerByIdRequestDTO, UpdateCustomerByIdRequestSchema } from "../../common";
|
||||
import { CreateCustomerFormData } from "../schemas";
|
||||
import { CUSTOMER_QUERY_KEY } from "./use-customer-query";
|
||||
|
||||
export const CUSTOMERS_LIST_KEY = ["customers"] as const;
|
||||
|
||||
type UpdateCustomerPayload = {
|
||||
id: string;
|
||||
data: CustomerUpdateData;
|
||||
data: CreateCustomerFormData;
|
||||
};
|
||||
|
||||
export function useUpdateCustomerMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const dataSource = useDataSource();
|
||||
const schema = UpdateCustomerByIdRequestSchema;
|
||||
|
||||
return useMutation<CustomerData, Error, UpdateCustomerPayload>({
|
||||
return useMutation<UpdateCustomerByIdRequestDTO, Error, UpdateCustomerPayload>({
|
||||
mutationKey: ["customer:update"], //, customerId],
|
||||
|
||||
mutationFn: async (payload) => {
|
||||
@ -23,14 +26,28 @@ export function useUpdateCustomerMutation() {
|
||||
throw new Error("customerId is required");
|
||||
}
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
if (!result.success) {
|
||||
// Construye errores detallados
|
||||
const validationErrors = result.error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
throw new ValidationErrorCollection("Validation failed", validationErrors);
|
||||
}
|
||||
|
||||
const updated = await dataSource.updateOne("customers", customerId, data);
|
||||
return updated as CustomerData;
|
||||
return updated as UpdateCustomerByIdRequestDTO;
|
||||
},
|
||||
onSuccess: (updated, variables) => {
|
||||
const { id: customerId } = variables;
|
||||
|
||||
// Refresca inmediatamente el detalle
|
||||
queryClient.setQueryData<CustomerData>(CUSTOMER_QUERY_KEY(customerId), updated);
|
||||
queryClient.setQueryData<UpdateCustomerByIdRequestDTO>(
|
||||
CUSTOMER_QUERY_KEY(customerId),
|
||||
updated
|
||||
);
|
||||
|
||||
// Otra opción es invalidar el detalle para forzar refetch:
|
||||
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
|
||||
|
||||
@ -8,14 +8,14 @@ import { FieldErrors } from "react-hook-form";
|
||||
import { CustomerEditForm, ErrorAlert } from "../../components";
|
||||
import { useCreateCustomerMutation } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerFormData, defaultCustomerFormData } from "../../schemas";
|
||||
import { CreateCustomerFormData, defaultCustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerCreate = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
|
||||
// 2) Estado de creación (mutación)
|
||||
// 1) Estado de creación (mutación)
|
||||
const {
|
||||
mutate,
|
||||
isPending: isCreating,
|
||||
@ -23,8 +23,8 @@ export const CustomerCreate = () => {
|
||||
error: createError,
|
||||
} = useCreateCustomerMutation();
|
||||
|
||||
// 3) Submit con navegación condicionada por éxito
|
||||
const handleSubmit = (formData: CustomerFormData) => {
|
||||
// 2) Submit con navegación condicionada por éxito
|
||||
const handleSubmit = (formData: CreateCustomerFormData) => {
|
||||
mutate(
|
||||
{ data: formData },
|
||||
{
|
||||
@ -48,7 +48,7 @@ export const CustomerCreate = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<CustomerFormData>) => {
|
||||
const handleError = (errors: FieldErrors<CreateCustomerFormData>) => {
|
||||
console.error("Errores en el formulario:", errors);
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import { CustomersListGrid } from "../components";
|
||||
import { useTranslation } from "../i18n";
|
||||
|
||||
@ -28,6 +28,7 @@ export const CustomersList = () => {
|
||||
<div className='flex flex-col w-full h-full py-4'>
|
||||
<CustomersListGrid />
|
||||
</div>
|
||||
<Outlet />
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { AppBreadcrumb, AppContent, BackHistoryButton, ButtonGroup } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { AppBreadcrumb, AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useUrlParamId } from "@erp/core/hooks";
|
||||
import { FormCommitButtonGroup, UnsavedChangesProvider, useUrlParamId } from "@erp/core/hooks";
|
||||
import { showErrorToast, showSuccessToast } from "@repo/shadcn-ui/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { FieldErrors } from "react-hook-form";
|
||||
import {
|
||||
CustomerEditForm,
|
||||
@ -13,12 +13,13 @@ import {
|
||||
} from "../../components";
|
||||
import { useCustomerQuery, useUpdateCustomerMutation } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerFormData } from "../../schemas";
|
||||
import { CreateCustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerUpdate = () => {
|
||||
const customerId = useUrlParamId();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
|
||||
// 1) Estado de carga del cliente (query)
|
||||
const {
|
||||
@ -30,30 +31,38 @@ export const CustomerUpdate = () => {
|
||||
|
||||
// 2) Estado de actualización (mutación)
|
||||
const {
|
||||
mutateAsync,
|
||||
mutate,
|
||||
isPending: isUpdating,
|
||||
isError: isUpdateError,
|
||||
error: updateError,
|
||||
} = useUpdateCustomerMutation();
|
||||
|
||||
// 3) Submit con navegación condicionada por éxito
|
||||
const handleSubmit = async (formData: CustomerFormData) => {
|
||||
console.log(formData);
|
||||
try {
|
||||
const result = await mutateAsync({ id: customerId!, data: formData });
|
||||
console.log(result);
|
||||
const handleSubmit = (formData: CreateCustomerFormData) => {
|
||||
mutate(
|
||||
{ id: customerId!, data: formData },
|
||||
{
|
||||
onSuccess(data) {
|
||||
setIsDirty(false);
|
||||
showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg"));
|
||||
|
||||
if (result) {
|
||||
showSuccessToast(t("pages.update.successTitle"), t("pages.update.successMsg"));
|
||||
navigate("/customers/list", { relative: "path" });
|
||||
// El timeout es para que a React le dé tiempo a procesar
|
||||
// el cambio de estado de isDirty / setIsDirty.
|
||||
setTimeout(() => {
|
||||
navigate("/customers/list", {
|
||||
state: { customerId: data.id, isNew: true },
|
||||
replace: true,
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
onError(error) {
|
||||
showErrorToast(t("pages.update.errorTitle"), error.message);
|
||||
},
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorToast(t("pages.update.errorTitle"), (e as Error).message);
|
||||
} finally {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<CustomerFormData>) => {
|
||||
const handleError = (errors: FieldErrors<CreateCustomerFormData>) => {
|
||||
console.error("Errores en el formulario:", errors);
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
@ -100,60 +109,48 @@ export const CustomerUpdate = () => {
|
||||
<>
|
||||
<AppBreadcrumb />
|
||||
<AppContent>
|
||||
<div className='flex items-center justify-between space-y-4 px-6'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||
{t("pages.update.title")}
|
||||
</h2>
|
||||
<p className='text-muted-foreground scroll-m-20 tracking-tight text-balance'>
|
||||
{t("pages.update.description")}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className='cursor-pointer'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/customers/list");
|
||||
<UnsavedChangesProvider isDirty={isDirty}>
|
||||
<div className='flex items-center justify-between space-y-4 px-6'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||
{t("pages.update.title")}
|
||||
</h2>
|
||||
<p className='text-muted-foreground scroll-m-20 tracking-tight text-balance'>
|
||||
{t("pages.update.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FormCommitButtonGroup
|
||||
cancel={{
|
||||
to: "/customers/list",
|
||||
}}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
submit={{
|
||||
formId: "customer-create-form",
|
||||
disabled: isUpdating,
|
||||
isLoading: isUpdating,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
{isUpdateError && (
|
||||
<ErrorAlert
|
||||
title={t("pages.update.errorTitle", "No se pudo guardar los cambios")}
|
||||
message={
|
||||
(updateError as Error)?.message ??
|
||||
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
form='customer-update-form'
|
||||
className='cursor-pointer'
|
||||
disabled={isUpdating || isLoadingCustomer}
|
||||
aria-busy={isUpdating}
|
||||
aria-disabled={isUpdating || isLoadingCustomer}
|
||||
data-state={isUpdating ? "loading" : "idle"}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
{isUpdateError && (
|
||||
<ErrorAlert
|
||||
title={t("pages.update.errorTitle", "No se pudo guardar los cambios")}
|
||||
message={
|
||||
(updateError as Error)?.message ??
|
||||
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
<CustomerEditForm
|
||||
formId={"customer-update-form"} // para que el botón del header pueda hacer submit
|
||||
initialValues={customerData}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
disabled={isLoadingCustomer}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
<CustomerEditForm
|
||||
formId={"customer-update-form"} // para que el botón del header pueda hacer submit
|
||||
initialValues={customerData}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
onDirtyChange={setIsDirty}
|
||||
/>
|
||||
</div>
|
||||
</UnsavedChangesProvider>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -55,9 +55,9 @@ export const CreateCustomerFormSchema = z.object({
|
||||
.default("EUR"),
|
||||
});
|
||||
|
||||
export type CustomerFormData = z.infer<typeof CreateCustomerFormSchema>;
|
||||
export type CreateCustomerFormData = z.infer<typeof CreateCustomerFormSchema>;
|
||||
|
||||
export const defaultCustomerFormData: CustomerFormData = {
|
||||
export const defaultCustomerFormData: CreateCustomerFormData = {
|
||||
reference: "",
|
||||
|
||||
is_company: "true",
|
||||
@ -0,0 +1,15 @@
|
||||
import * as z from "zod/v4";
|
||||
import { CreateCustomerFormSchema } from "./customer-create.form.schema";
|
||||
|
||||
export const UpdateCustomerFormSchema = CreateCustomerFormSchema.extend({
|
||||
is_company: CreateCustomerFormSchema.shape.is_company.optional(),
|
||||
name: CreateCustomerFormSchema.shape.name.optional(),
|
||||
default_taxes: z.array(z.string()).optional(),
|
||||
|
||||
country: CreateCustomerFormSchema.shape.country.optional(),
|
||||
|
||||
language_code: CreateCustomerFormSchema.shape.language_code.optional(),
|
||||
currency_code: CreateCustomerFormSchema.shape.currency_code.optional(),
|
||||
});
|
||||
|
||||
export type UpdateCustomerFormData = z.infer<typeof UpdateCustomerFormSchema>;
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./customer-create.form.schema";
|
||||
export * from "./customer.api.schema";
|
||||
export * from "./customer.form.schema";
|
||||
|
||||
47
packages/rdx-ui/src/components/full-screen-modal.tsx
Normal file
47
packages/rdx-ui/src/components/full-screen-modal.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import type React from "react";
|
||||
|
||||
import { Button, Dialog, DialogContent } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
interface FullscreenModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FullscreenModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
}: FullscreenModalProps) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"max-w-none max-h-none w-screen h-screen",
|
||||
"bg-background border-0 rounded-none p-0",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Header fijo */}
|
||||
<div className='flex items-center justify-between p-6 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60'>
|
||||
<h2 className='text-2xl font-semibold tracking-tight'>{title}</h2>
|
||||
<Button variant='ghost' size='sm' onClick={onClose} className='h-8 w-8 p-0'>
|
||||
<X className='h-4 w-4' />
|
||||
<span className='sr-only'>Cerrar</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Contenido scrolleable */}
|
||||
<div className='flex-1 overflow-auto p-6'>{children}</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -4,6 +4,7 @@ export * from "./datatable/index.tsx";
|
||||
export * from "./dynamics-tabs.tsx";
|
||||
export * from "./error-overlay.tsx";
|
||||
export * from "./form/index.tsx";
|
||||
export * from "./full-screen-modal.tsx";
|
||||
export * from "./grid/index.ts";
|
||||
export * from "./layout/index.tsx";
|
||||
export * from "./loading-overlay/index.tsx";
|
||||
|
||||
@ -2,7 +2,7 @@ import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components";
|
||||
import { Outlet } from "react-router";
|
||||
import { AppSidebar } from "./app-sidebar.tsx";
|
||||
|
||||
export const AppLayout: React.FC = () => {
|
||||
export const AppLayout = () => {
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={
|
||||
|
||||
Loading…
Reference in New Issue
Block a user