Clientes y facturas de cliente
This commit is contained in:
parent
0367c51a97
commit
633f7cf6bd
@ -1,2 +1,4 @@
|
||||
export * from "./use-unsaved-changes-notifier";
|
||||
export * from "./use-warn-about-change";
|
||||
export * from "./warn-about-change-context";
|
||||
export * from "./warn-about-change-provider";
|
||||
|
||||
@ -51,6 +51,7 @@ export const useUnsavedChangesNotifier = ({
|
||||
onConfirm: () => {
|
||||
resolve(true);
|
||||
onConfirm?.();
|
||||
window.onbeforeunload = null; // limpirar de cambios
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false);
|
||||
@ -71,7 +72,12 @@ export const useUnsavedChangesNotifier = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
window.onbeforeunload = () => texts.subtitle;
|
||||
window.onbeforeunload = (e) => {
|
||||
e.preventDefault();
|
||||
return texts.subtitle;
|
||||
};
|
||||
} else {
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
@ -79,7 +85,5 @@ export const useUnsavedChangesNotifier = ({
|
||||
};
|
||||
}, [isDirty, texts.subtitle]);
|
||||
|
||||
return {
|
||||
confirm,
|
||||
};
|
||||
return { confirm };
|
||||
};
|
||||
|
||||
@ -3,8 +3,8 @@ import { UnsavedWarnContext } from "./warn-about-change-context";
|
||||
|
||||
export const useWarnAboutChange = () => {
|
||||
const context = useContext(UnsavedWarnContext);
|
||||
if (context === null)
|
||||
throw new Error("useWarnAboutChange must be used within a UnsavedWarnProvider");
|
||||
|
||||
if (context === null) {
|
||||
throw new Error("useWarnAboutChange must be used within an UnsavedWarnProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier
|
||||
|
||||
export interface IUnsavedWarnContextState {
|
||||
show: (options: NullOr<UnsavedChangesNotifierProps>) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
export const UnsavedWarnContext = createContext<NullOr<IUnsavedWarnContextState>>(null);
|
||||
|
||||
@ -4,38 +4,43 @@ import { type PropsWithChildren, useCallback, useMemo, useState } from "react";
|
||||
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
||||
import { UnsavedWarnContext } from "./warn-about-change-context";
|
||||
|
||||
export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => {
|
||||
const [confirm, setConfirm] = useState<NullOr<UnsavedChangesNotifierProps>>(null);
|
||||
// Aseguramos que las props mínimas para el diálogo siempre estén presentes
|
||||
type ConfirmOptions = Required<
|
||||
Pick<UnsavedChangesNotifierProps, "title" | "subtitle" | "confirmText" | "cancelText">
|
||||
> &
|
||||
Omit<UnsavedChangesNotifierProps, "title" | "subtitle" | "confirmText" | "cancelText">;
|
||||
|
||||
const [open, toggle] = useState(false);
|
||||
export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => {
|
||||
const [confirm, setConfirm] = useState<NullOr<ConfirmOptions>>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const show = useCallback((confirmOptions: NullOr<UnsavedChangesNotifierProps>) => {
|
||||
setConfirm(confirmOptions);
|
||||
toggle(true);
|
||||
if (confirmOptions) {
|
||||
setConfirm(confirmOptions as ConfirmOptions);
|
||||
setOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onConfirm = () => {
|
||||
const hide = useCallback(() => setOpen(false), []);
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
confirm?.onConfirm?.();
|
||||
toggle(false);
|
||||
};
|
||||
hide();
|
||||
}, [confirm, hide]);
|
||||
|
||||
const onCancel = () => {
|
||||
const onCancel = useCallback(() => {
|
||||
confirm?.onCancel?.();
|
||||
toggle(false);
|
||||
};
|
||||
hide();
|
||||
}, [confirm, hide]);
|
||||
|
||||
const value = useMemo(() => ({ show }), [show]);
|
||||
const value = useMemo(() => ({ show, hide }), [show, hide]);
|
||||
|
||||
return (
|
||||
<UnsavedWarnContext.Provider value={value}>
|
||||
{children}
|
||||
<CustomDialog
|
||||
//type='warning'
|
||||
onCancel={() => {
|
||||
//console.log("onCancel");
|
||||
onCancel();
|
||||
}}
|
||||
onConfirm={() => onConfirm()}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
title={confirm?.title}
|
||||
description={confirm?.subtitle}
|
||||
confirmLabel={confirm?.confirmText}
|
||||
|
||||
@ -32,6 +32,13 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
|
||||
|
||||
taxes: z.string(),
|
||||
|
||||
payment_method: z
|
||||
.object({
|
||||
payment_id: z.string(),
|
||||
payment_description: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
subtotal_amount: MoneySchema,
|
||||
discount_percentage: PercentageSchema,
|
||||
discount_amount: MoneySchema,
|
||||
|
||||
@ -100,7 +100,7 @@ export const CustomerInvoicesListGrid = () => {
|
||||
size='icon'
|
||||
className='size-8'
|
||||
onClick={() => {
|
||||
navigate(`${data.id}/edit`);
|
||||
navigate(`/customer-invoices/${data.id}/edit`);
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
|
||||
@ -1,38 +1,35 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const CreateCustomerRequestSchema = z.object({
|
||||
id: z.uuid(),
|
||||
company_id: z.uuid(),
|
||||
reference: z.string().default(""),
|
||||
reference: z.string().optional(),
|
||||
|
||||
is_company: z.string().toLowerCase().default("false"),
|
||||
name: z.string().default(""),
|
||||
trade_name: z.string().default(""),
|
||||
tin: z.string().default(""),
|
||||
is_company: z.string().toLowerCase().default("false").optional(),
|
||||
name: z.string(),
|
||||
trade_name: z.string().optional(),
|
||||
tin: z.string().optional(),
|
||||
default_taxes: z.array(z.string()).default([]).optional(),
|
||||
|
||||
street: z.string().default(""),
|
||||
street2: z.string().default(""),
|
||||
city: z.string().default(""),
|
||||
province: z.string().default(""),
|
||||
postal_code: z.string().default(""),
|
||||
country: z.string().default("es"),
|
||||
street: z.string().optional(),
|
||||
street2: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
province: z.string().optional(),
|
||||
postal_code: z.string().optional(),
|
||||
country: z.string().default("es").optional(),
|
||||
|
||||
email_primary: z.string().default(""),
|
||||
email_secondary: z.string().default(""),
|
||||
phone_primary: z.string().default(""),
|
||||
phone_secondary: z.string().default(""),
|
||||
mobile_primary: z.string().default(""),
|
||||
mobile_secondary: z.string().default(""),
|
||||
email_primary: z.string().optional(),
|
||||
email_secondary: z.string().optional(),
|
||||
phone_primary: z.string().optional(),
|
||||
phone_secondary: z.string().optional(),
|
||||
mobile_primary: z.string().optional(),
|
||||
mobile_secondary: z.string().optional(),
|
||||
|
||||
fax: z.string().default(""),
|
||||
website: z.string().default(""),
|
||||
fax: z.string().optional(),
|
||||
website: z.string().optional(),
|
||||
|
||||
legal_record: z.string().default(""),
|
||||
legal_record: z.string().optional(),
|
||||
|
||||
default_taxes: z.array(z.string()).default([]),
|
||||
status: z.string().toLowerCase().default("active"),
|
||||
language_code: z.string().toLowerCase().default("es"),
|
||||
currency_code: z.string().toUpperCase().default("EUR"),
|
||||
language_code: z.string().toLowerCase().default("es").optional(),
|
||||
currency_code: z.string().toUpperCase().default("EUR").optional(),
|
||||
});
|
||||
|
||||
export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>;
|
||||
|
||||
@ -33,10 +33,10 @@
|
||||
},
|
||||
"form_fields": {
|
||||
"customer_type": {
|
||||
"label": "Customer type",
|
||||
"label": "This contact is...",
|
||||
"description": "Select the type of customer",
|
||||
"company": "Company",
|
||||
"individual": "Individual"
|
||||
"company": "a company",
|
||||
"individual": "a person"
|
||||
},
|
||||
"name": {
|
||||
"label": "Name",
|
||||
|
||||
@ -35,10 +35,10 @@
|
||||
},
|
||||
"form_fields": {
|
||||
"customer_type": {
|
||||
"label": "Tipo de cliente",
|
||||
"label": "Este cliente es...",
|
||||
"description": "Seleccione el tipo de cliente",
|
||||
"company": "Empresa",
|
||||
"individual": "Persona física"
|
||||
"company": "una empresa",
|
||||
"individual": "una persona"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nombre",
|
||||
|
||||
@ -79,7 +79,7 @@ export const CustomersListGrid = () => {
|
||||
size='icon'
|
||||
className='size-8'
|
||||
onClick={() => {
|
||||
navigate(`${data.id}/edit`, { relative: "route" });
|
||||
navigate(`/customers/${data.id}/edit`);
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
|
||||
@ -6,15 +6,14 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Control } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerUpdateData } from "../../schemas";
|
||||
import { CustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerAdditionalConfigFields = ({
|
||||
control,
|
||||
}: { control: Control<CustomerUpdateData> }) => {
|
||||
export const CustomerAdditionalConfigFields = () => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<CustomerFormData>();
|
||||
|
||||
return (
|
||||
<Card className='border-0 shadow-none'>
|
||||
@ -6,13 +6,14 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Control } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { COUNTRY_OPTIONS } from "../../constants";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerUpdateData } from "../../schemas";
|
||||
import { CustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerAddressFields = ({ control }: { control: Control<CustomerUpdateData> }) => {
|
||||
export const CustomerAddressFields = () => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<CustomerFormData>();
|
||||
|
||||
return (
|
||||
<Card className='border-0 shadow-none'>
|
||||
@ -0,0 +1,131 @@
|
||||
import { TaxesMultiSelectField } from "@erp/core/components";
|
||||
import { TextAreaField, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerBasicInfoFields = () => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<CustomerFormData>();
|
||||
|
||||
const isCompany = useWatch({
|
||||
name: "is_company",
|
||||
defaultValue: "true",
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className='border-0 shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>Identificación</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-12 '>
|
||||
<div className='lg:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='name'
|
||||
required
|
||||
label={t("form_fields.name.label")}
|
||||
placeholder={t("form_fields.name.placeholder")}
|
||||
description={t("form_fields.name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='lg:col-span-2'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='is_company'
|
||||
render={({ field }) => (
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ? "true" : "false"}
|
||||
className='flex items-center gap-6'
|
||||
>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem id='rgi-company' value='true' />
|
||||
</FormControl>
|
||||
<FormLabel className='cursor-pointer' htmlFor='rgi-company'>
|
||||
{t("form_fields.customer_type.company")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem id='rgi-individual' value='false' />
|
||||
</FormControl>
|
||||
<FormLabel className='cursor-pointer' htmlFor='rgi-individual'>
|
||||
{t("form_fields.customer_type.individual")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isCompany === "false" ? (
|
||||
<div className='lg:col-span-full'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='trade_name'
|
||||
label={t("form_fields.trade_name.label")}
|
||||
placeholder={t("form_fields.trade_name.placeholder")}
|
||||
description={t("form_fields.trade_name.description")}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<div className='lg:col-span-2 lg:col-start-1'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='reference'
|
||||
label={t("form_fields.reference.label")}
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='lg:col-span-2'>
|
||||
<TaxesMultiSelectField
|
||||
control={control}
|
||||
name='default_taxes'
|
||||
required
|
||||
label={t("form_fields.default_taxes.label")}
|
||||
placeholder={t("form_fields.default_taxes.placeholder")}
|
||||
description={t("form_fields.default_taxes.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextAreaField
|
||||
className='lg:col-span-full'
|
||||
control={control}
|
||||
name='legal_record'
|
||||
label={t("form_fields.legal_record.label")}
|
||||
placeholder={t("form_fields.legal_record.placeholder")}
|
||||
description={t("form_fields.legal_record.description")}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@ -12,13 +12,13 @@ import {
|
||||
import { TextField } from "@repo/rdx-ui/components";
|
||||
import { ChevronDown, MailIcon, PhoneIcon, SmartphoneIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Control } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerUpdateData } from "../../schemas";
|
||||
|
||||
export const CustomerContactFields = ({ control }: { control: Control<CustomerUpdateData> }) => {
|
||||
export const CustomerContactFields = () => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(true);
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<Card className='border-0 shadow-none'>
|
||||
@ -124,7 +124,7 @@ export const CustomerContactFields = ({ control }: { control: Control<CustomerUp
|
||||
description={t("form_fields.website.description")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className='sm:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='fax'
|
||||
@ -0,0 +1,63 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FieldErrors, FormProvider, useForm } from "react-hook-form";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { CustomerFormData, CustomerFormSchema } from "../../schemas";
|
||||
import { FormDebug } from "../form-debug";
|
||||
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
|
||||
import { CustomerAddressFields } from "./customer-address-fields";
|
||||
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
||||
import { CustomerContactFields } from "./customer-contact-fields";
|
||||
|
||||
interface CustomerFormProps {
|
||||
formId: string;
|
||||
initialValues: CustomerFormData;
|
||||
onSubmit: (data: CustomerFormData) => void;
|
||||
onError: (errors: FieldErrors<CustomerFormData>) => void;
|
||||
disabled?: boolean;
|
||||
onDirtyChange: (isDirty: boolean) => void;
|
||||
}
|
||||
|
||||
export function CustomerEditForm({
|
||||
formId,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
onError,
|
||||
disabled,
|
||||
onDirtyChange,
|
||||
}: CustomerFormProps) {
|
||||
const form = useForm<CustomerFormData>({
|
||||
resolver: zodResolver(CustomerFormSchema),
|
||||
defaultValues: initialValues,
|
||||
disabled,
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
} = form;
|
||||
|
||||
useEffect(() => {
|
||||
if (onDirtyChange) {
|
||||
onDirtyChange(isDirty);
|
||||
}
|
||||
}, [isDirty, onDirtyChange]);
|
||||
|
||||
// Resetear el form si cambian los valores iniciales
|
||||
useEffect(() => {
|
||||
form.reset(initialValues);
|
||||
}, [initialValues, form]);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<FormDebug />
|
||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
||||
<div className='w-full xl:w-2/3'>
|
||||
<CustomerBasicInfoFields />
|
||||
<CustomerContactFields />
|
||||
<CustomerAddressFields />
|
||||
<CustomerAdditionalConfigFields />
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
1
modules/customers/src/web/components/editor/index.ts
Normal file
1
modules/customers/src/web/components/editor/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./customer-edit-form";
|
||||
@ -1,6 +1,8 @@
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
export const FormDebug = () => {
|
||||
const form = useFormContext(); // ✅ mantiene el tipo de T
|
||||
|
||||
export const FormDebug = ({ form }: { form: UseFormReturn }) => {
|
||||
const {
|
||||
watch,
|
||||
formState: { isDirty, dirtyFields, defaultValues },
|
||||
|
||||
@ -2,6 +2,7 @@ export * from "./client-selector";
|
||||
export * from "./customer-editor-skeleton";
|
||||
export * from "./customers-layout";
|
||||
export * from "./customers-list-grid";
|
||||
export * from "./editor";
|
||||
export * from "./error-alert";
|
||||
export * from "./form-debug";
|
||||
export * from "./not-found-card";
|
||||
|
||||
@ -1,20 +1,33 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { CustomerCreateData, CustomerData } from "../schemas";
|
||||
import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation";
|
||||
|
||||
type CreateCustomerPayload = {
|
||||
data: CustomerCreateData;
|
||||
};
|
||||
|
||||
export function useCreateCustomerMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const dataSource = useDataSource();
|
||||
|
||||
return useMutation<CustomerData, Error, CustomerCreateData>({
|
||||
return useMutation<CustomerData, Error, CreateCustomerPayload>({
|
||||
mutationKey: ["customer:create"],
|
||||
mutationFn: async (data: CustomerCreateData) => {
|
||||
const created = await dataSource.createOne("customers", data);
|
||||
|
||||
mutationFn: async (payload) => {
|
||||
const { data } = payload;
|
||||
const customerId = UniqueID.generateNewID();
|
||||
|
||||
const created = await dataSource.createOne("customers", {
|
||||
...data,
|
||||
id: customerId.toString(),
|
||||
});
|
||||
return created as CustomerData;
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
// Invalida el listado de clientes para incluir el nuevo
|
||||
// Invalida el listado para refrescar desde servidor
|
||||
queryClient.invalidateQueries({ queryKey: CUSTOMERS_LIST_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import { AppBreadcrumb, AppContent, ButtonGroup } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useCreateCustomerMutation } from "../../hooks/use-create-customer-mutation";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerEditForm } from "./customer-edit-form";
|
||||
|
||||
export const CustomerCreate = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { mutate, isPending, isError, error } = useCreateCustomerMutation();
|
||||
|
||||
const handleSubmit = (data: any) => {
|
||||
// Handle form submission logic here
|
||||
console.log("Form submitted with data:", data);
|
||||
mutate(data);
|
||||
|
||||
// Navigate to the list page after submission
|
||||
navigate("/customers/list");
|
||||
};
|
||||
|
||||
if (isError) {
|
||||
console.error("Error creating customer:", error);
|
||||
// Optionally, you can show an error message to the user
|
||||
}
|
||||
|
||||
// Render the component
|
||||
// You can also handle loading state if needed
|
||||
// For example, you can disable the submit button while the mutation is in progress
|
||||
// const isLoading = useCreateCustomerMutation().isLoading;
|
||||
|
||||
// Return the JSX for the component
|
||||
// You can customize the form and its fields as needed
|
||||
// For example, you can use a form library like react-hook-form or Formik to handle form state and validation
|
||||
// Here, we are using a simple form with a submit button
|
||||
|
||||
// Note: Make sure to replace the form fields with your actual invoice fields
|
||||
// and handle validation as needed.
|
||||
// This is just a basic example to demonstrate the structure of the component.
|
||||
|
||||
// If you are using a form library, you can pass the handleSubmit function to the form's onSubmit prop
|
||||
// and use the form library's methods to handle form state and validation.
|
||||
|
||||
// Example of a simple form submission handler
|
||||
// You can replace this with your actual form handling logic
|
||||
// const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
// event.preventDefault();
|
||||
// const formData = new FormData(event.currentTarget);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBreadcrumb />
|
||||
<AppContent>
|
||||
<div className='flex items-center justify-between space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||
{t("pages.create.title")}
|
||||
</h2>
|
||||
<p className='text-muted-foreground scroll-m-20 tracking-tight text-balance'>
|
||||
{t("pages.create.description")}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button className='cursor-pointer' onClick={handleCancel}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button type='submit' className='cursor-pointer'>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
<CustomerEditForm onSubmit={handleSubmit} isPending={isPending} />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
124
modules/customers/src/web/pages/create/customer-create.tsx
Normal file
124
modules/customers/src/web/pages/create/customer-create.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { AppBreadcrumb, AppContent, ButtonGroup } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useUnsavedChangesNotifier } 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, ErrorAlert } from "../../components";
|
||||
import { useCreateCustomerMutation } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerFormData, defaultCustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerCreate = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
|
||||
// 2) Estado de creación (mutación)
|
||||
const {
|
||||
mutateAsync,
|
||||
isPending: isCreating,
|
||||
isError: isCreateError,
|
||||
error: createError,
|
||||
} = useCreateCustomerMutation();
|
||||
|
||||
const { confirm: confirmCancel } = useUnsavedChangesNotifier({
|
||||
isDirty,
|
||||
});
|
||||
|
||||
// 3) Submit con navegación condicionada por éxito
|
||||
const handleSubmit = async (formData: CustomerFormData) => {
|
||||
/*const changedData: Record<string, string> = {};
|
||||
|
||||
Object.keys(dirtyFields).forEach((field) => {
|
||||
const value = String(currentValues[field as keyof CustomerFormData]);
|
||||
changedData[field] = value;
|
||||
});*/
|
||||
|
||||
try {
|
||||
const result = await mutateAsync({ data: formData });
|
||||
console.log(result);
|
||||
|
||||
if (result) {
|
||||
showSuccessToast(t("pages.create.successTitle"), t("pages.create.successMsg"));
|
||||
navigate("/customers/list");
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorToast(t("pages.create.errorTitle"), (e as Error).message);
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<CustomerFormData>) => {
|
||||
console.error("Errores en el formulario:", errors);
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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.create.title")}
|
||||
</h2>
|
||||
<p className='text-muted-foreground scroll-m-20 tracking-tight text-balance'>
|
||||
{t("pages.create.description")}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className='cursor-pointer'
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const ok = await confirmCancel();
|
||||
if (ok) {
|
||||
console.log("Cambios descartados");
|
||||
navigate("/customers/list");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
form='customer-create-form'
|
||||
className='cursor-pointer'
|
||||
disabled={isCreating}
|
||||
aria-busy={isCreating}
|
||||
aria-disabled={isCreating}
|
||||
data-state={isCreating ? "loading" : "idle"}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
{isCreateError && (
|
||||
<ErrorAlert
|
||||
title={t("pages.create.errorTitle", "No se pudo guardar los cambios")}
|
||||
message={
|
||||
(createError as Error)?.message ??
|
||||
t("pages.create.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
<CustomerEditForm
|
||||
formId={"customer-create-form"} // para que el botón del header pueda hacer submit
|
||||
initialValues={defaultCustomerFormData}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
onDirtyChange={setIsDirty}
|
||||
/>
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,350 +0,0 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { TaxesMultiSelectField } from "@erp/core/components";
|
||||
import { SelectField, TextAreaField, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerData, CustomerUpdateSchema } from "../../schemas";
|
||||
|
||||
const defaultCustomerData = {
|
||||
id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
|
||||
is_company: true,
|
||||
status: "active",
|
||||
tin: "B12345678",
|
||||
name: "Pepe",
|
||||
trade_name: "Pepe's Shop",
|
||||
email: "pepe@example.com",
|
||||
phone: "+34 123 456 789",
|
||||
website: "https://pepe.com",
|
||||
fax: "+34 123 456 789",
|
||||
street: "Calle Falsa 123",
|
||||
city: "Madrid",
|
||||
country: "ES",
|
||||
postal_code: "28080",
|
||||
province: "Madrid",
|
||||
language_code: "es",
|
||||
currency_code: "EUR",
|
||||
legal_record: "Registro Mercantil de Madrid, Tomo 12345, Folio 67, Hoja M-123456",
|
||||
default_taxes: ["iva_21", "rec_5_2"],
|
||||
};
|
||||
|
||||
interface CustomerFormProps {
|
||||
initialData?: CustomerData;
|
||||
isPending?: boolean;
|
||||
/**
|
||||
* Callback function to handle form submission.
|
||||
* @param data - The customer data submitted by the form.
|
||||
*/
|
||||
onSubmit?: (data: CustomerData) => void;
|
||||
}
|
||||
|
||||
export const CustomerEditForm = ({
|
||||
initialData = defaultCustomerData,
|
||||
onSubmit,
|
||||
isPending,
|
||||
}: CustomerFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<CustomerData>({
|
||||
resolver: zodResolver(CustomerUpdateSchema),
|
||||
defaultValues: initialData,
|
||||
disabled: isPending,
|
||||
});
|
||||
|
||||
useUnsavedChangesNotifier({
|
||||
isDirty: form.formState.isDirty,
|
||||
});
|
||||
|
||||
const handleSubmit = (data: CustomerData) => {
|
||||
console.log("Datos del formulario:", data);
|
||||
onSubmit?.(data);
|
||||
};
|
||||
|
||||
const handleError = (errors: any) => {
|
||||
console.error("Errores en el formulario:", errors);
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.reset(initialData);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||
<div className='w-full grid grid-cols-1 space-y-8 space-x-8 xl:grid-cols-2'>
|
||||
{/* Información básica */}
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='is_company'
|
||||
render={({ field }) => (
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ? "1" : "0"}
|
||||
className='flex gap-6'
|
||||
>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value={"1"} />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.company")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value={"0"} />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.individual")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='name'
|
||||
required
|
||||
label={t("form_fields.name.label")}
|
||||
placeholder={t("form_fields.name.placeholder")}
|
||||
description={t("form_fields.name.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='trade_name'
|
||||
required
|
||||
label={t("form_fields.trade_name.label")}
|
||||
placeholder={t("form_fields.trade_name.placeholder")}
|
||||
description={t("form_fields.trade_name.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='tin'
|
||||
required
|
||||
label={t("form_fields.tin.label")}
|
||||
placeholder={t("form_fields.tin.placeholder")}
|
||||
description={t("form_fields.tin.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='reference'
|
||||
label={t("form_fields.reference.label")}
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dirección */}
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("form_groups.address.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.address.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||
<TextField
|
||||
className='xl:col-span-2'
|
||||
control={form.control}
|
||||
name='street'
|
||||
required
|
||||
label={t("form_fields.street.label")}
|
||||
placeholder={t("form_fields.street.placeholder")}
|
||||
description={t("form_fields.street.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='city'
|
||||
required
|
||||
label={t("form_fields.city.label")}
|
||||
placeholder={t("form_fields.city.placeholder")}
|
||||
description={t("form_fields.city.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='postal_code'
|
||||
required
|
||||
label={t("form_fields.postal_code.label")}
|
||||
placeholder={t("form_fields.postal_code.placeholder")}
|
||||
description={t("form_fields.postal_code.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='province'
|
||||
required
|
||||
label={t("form_fields.province.label")}
|
||||
placeholder={t("form_fields.province.placeholder")}
|
||||
description={t("form_fields.province.description")}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
control={form.control}
|
||||
name='country'
|
||||
required
|
||||
label={t("form_fields.country.label")}
|
||||
placeholder={t("form_fields.country.placeholder")}
|
||||
description={t("form_fields.country.description")}
|
||||
items={[
|
||||
{ value: "ES", label: "España" },
|
||||
{ value: "FR", label: "Francia" },
|
||||
{ value: "DE", label: "Alemania" },
|
||||
{ value: "IT", label: "Italia" },
|
||||
{ value: "PT", label: "Portugal" },
|
||||
{ value: "US", label: "Estados Unidos" },
|
||||
{ value: "MX", label: "México" },
|
||||
{ value: "AR", label: "Argentina" },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contacto */}
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("form_groups.contact_info.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.contact_info.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='email'
|
||||
required
|
||||
label={t("form_fields.email.label")}
|
||||
placeholder={t("form_fields.email.placeholder")}
|
||||
description={t("form_fields.email.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='phone'
|
||||
required
|
||||
label={t("form_fields.phone.label")}
|
||||
placeholder={t("form_fields.phone.placeholder")}
|
||||
description={t("form_fields.phone.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='fax'
|
||||
required
|
||||
label={t("form_fields.fax.label")}
|
||||
placeholder={t("form_fields.fax.placeholder")}
|
||||
description={t("form_fields.fax.description")}
|
||||
/>
|
||||
<TextField
|
||||
className='xl:col-span-2'
|
||||
control={form.control}
|
||||
name='website'
|
||||
required
|
||||
label={t("form_fields.website.label")}
|
||||
placeholder={t("form_fields.website.placeholder")}
|
||||
description={t("form_fields.website.description")}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Configuraciones Adicionales */}
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("form_groups.additional_config.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.additional_config.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||
<TaxesMultiSelectField
|
||||
control={form.control}
|
||||
name='default_taxes'
|
||||
required
|
||||
label={t("form_fields.default_taxes.label")}
|
||||
placeholder={t("form_fields.default_taxes.placeholder")}
|
||||
description={t("form_fields.default_taxes.description")}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
control={form.control}
|
||||
name='language_code'
|
||||
required
|
||||
label={t("form_fields.language_code.label")}
|
||||
placeholder={t("form_fields.language_code.placeholder")}
|
||||
description={t("form_fields.language_code.description")}
|
||||
items={[
|
||||
{ value: "es", label: "Español" },
|
||||
{ value: "en", label: "Inglés" },
|
||||
{ value: "fr", label: "Francés" },
|
||||
{ value: "de", label: "Alemán" },
|
||||
{ value: "it", label: "Italiano" },
|
||||
{ value: "pt", label: "Portugués" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
control={form.control}
|
||||
name='currency_code'
|
||||
required
|
||||
label={t("form_fields.currency_code.label")}
|
||||
placeholder={t("form_fields.currency_code.placeholder")}
|
||||
description={t("form_fields.currency_code.description")}
|
||||
items={[
|
||||
{ value: "EUR", label: "Euro" },
|
||||
{ value: "USD", label: "Dólar estadounidense" },
|
||||
{ value: "GBP", label: "Libra esterlina" },
|
||||
{ value: "ARS", label: "Peso argentino" },
|
||||
{ value: "MXN", label: "Peso mexicano" },
|
||||
{ value: "JPY", label: "Yen japonés" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<TextAreaField
|
||||
className=''
|
||||
control={form.control}
|
||||
name='legal_record'
|
||||
required
|
||||
label={t("form_fields.legal_record.label")}
|
||||
placeholder={t("form_fields.legal_record.placeholder")}
|
||||
description={t("form_fields.legal_record.description")}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Button type='submit'>Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@ -1 +1 @@
|
||||
export * from "./create";
|
||||
export * from "./customer-create";
|
||||
|
||||
@ -1,272 +0,0 @@
|
||||
import { TaxesMultiSelectField } from "@erp/core/components";
|
||||
import { TextAreaField, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Control } from "react-hook-form";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerUpdateData } from "../../schemas";
|
||||
|
||||
export const CustomerBasicInfoFields = ({ control }: { control: Control<CustomerUpdateData> }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className='border-0 shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>Identificación</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-12 '>
|
||||
<div className='lg:col-span-full'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='is_company'
|
||||
render={({ field }) => (
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ? "true" : "false"}
|
||||
className='flex items-center gap-6'
|
||||
>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem id='rgi-company' value='true' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal' htmlFor='rgi-company'>
|
||||
{t("form_fields.customer_type.company")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem id='rgi-individual' value='false' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal' htmlFor='rgi-individual'>
|
||||
{t("form_fields.customer_type.individual")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-12 '>
|
||||
<div className='lg:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='name'
|
||||
required
|
||||
label={t("form_fields.name.label")}
|
||||
placeholder={t("form_fields.name.placeholder")}
|
||||
description={t("form_fields.name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='lg:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='trade_name'
|
||||
label={t("form_fields.trade_name.label")}
|
||||
placeholder={t("form_fields.trade_name.placeholder")}
|
||||
description={t("form_fields.trade_name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='lg:col-span-2'>
|
||||
<TaxesMultiSelectField
|
||||
control={control}
|
||||
name='default_taxes'
|
||||
required
|
||||
label={t("form_fields.default_taxes.label")}
|
||||
placeholder={t("form_fields.default_taxes.placeholder")}
|
||||
description={t("form_fields.default_taxes.description")}
|
||||
/>
|
||||
</div>
|
||||
<div className='lg:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='reference'
|
||||
label={t("form_fields.reference.label")}
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
</div>
|
||||
<TextAreaField
|
||||
className='lg:col-span-full'
|
||||
control={control}
|
||||
name='legal_record'
|
||||
label={t("form_fields.legal_record.label")}
|
||||
placeholder={t("form_fields.legal_record.placeholder")}
|
||||
description={t("form_fields.legal_record.description")}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='space-y-12'>
|
||||
<div className='border-b border-gray-900/10 pb-12 dark:border-white/10'>
|
||||
<h2 className='text-base/7 font-semibold text-gray-900 dark:text-white'>
|
||||
{t("form_groups.basic_info.title")}
|
||||
</h2>
|
||||
<p className='mt-1 text-sm/6 text-gray-600 dark:text-gray-400'>
|
||||
{t("form_groups.basic_info.description")}
|
||||
</p>
|
||||
|
||||
<div className='mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6'>
|
||||
<div className='sm:col-span-6'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='is_company'
|
||||
render={({ field }) => (
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ? "1" : "0"}
|
||||
className='flex gap-6'
|
||||
>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='1' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.company")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='0' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.individual")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='name'
|
||||
required
|
||||
label={t("form_fields.name.label")}
|
||||
placeholder={t("form_fields.name.placeholder")}
|
||||
description={t("form_fields.name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='trade_name'
|
||||
label={t("form_fields.trade_name.label")}
|
||||
placeholder={t("form_fields.trade_name.placeholder")}
|
||||
description={t("form_fields.trade_name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='col-span-full'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='reference'
|
||||
label={t("form_fields.reference.label")}
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className='shadow-sm bg-gray-50/50'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='is_company'
|
||||
render={({ field }) => (
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ? "1" : "0"}
|
||||
className='flex gap-6'
|
||||
>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='1' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.company")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='0' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.individual")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={control}
|
||||
name='name'
|
||||
required
|
||||
label={t("form_fields.name.label")}
|
||||
placeholder={t("form_fields.name.placeholder")}
|
||||
description={t("form_fields.name.description")}
|
||||
/>
|
||||
<TextField
|
||||
control={control}
|
||||
name='trade_name'
|
||||
label={t("form_fields.trade_name.label")}
|
||||
placeholder={t("form_fields.trade_name.placeholder")}
|
||||
description={t("form_fields.trade_name.description")}
|
||||
/>
|
||||
<TextField
|
||||
control={control}
|
||||
name='reference'
|
||||
label={t("form_fields.reference.label")}
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@ -1,80 +0,0 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FieldErrors, useForm } from "react-hook-form";
|
||||
|
||||
import { Form } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
|
||||
import { FormDebug } from "../../components/form-debug";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerData, CustomerUpdateData, CustomerUpdateSchema } from "../../schemas";
|
||||
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
|
||||
import { CustomerAddressFields } from "./customer-address-fields";
|
||||
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
||||
import { CustomerContactFields } from "./customer-contact-fields";
|
||||
|
||||
interface CustomerFormProps {
|
||||
defaultValues: CustomerData; // ✅ ya no recibe DTO
|
||||
isPending?: boolean;
|
||||
onSubmit: (data: CustomerUpdateData) => void;
|
||||
onError: (errors: FieldErrors<CustomerUpdateData>) => void;
|
||||
}
|
||||
|
||||
export const CustomerEditForm = ({ defaultValues, onSubmit, isPending }: CustomerFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<CustomerUpdateData>({
|
||||
resolver: zodResolver(CustomerUpdateSchema),
|
||||
defaultValues,
|
||||
disabled: isPending,
|
||||
});
|
||||
|
||||
const {
|
||||
watch,
|
||||
formState: { isDirty, dirtyFields },
|
||||
} = form;
|
||||
|
||||
useUnsavedChangesNotifier({
|
||||
isDirty,
|
||||
});
|
||||
|
||||
const currentValues = watch();
|
||||
|
||||
const handleSubmit = (data: CustomerUpdateData) => {
|
||||
console.log("Datos del formulario:", data);
|
||||
const changedData: Record<string, string> = {};
|
||||
|
||||
Object.keys(dirtyFields).forEach((field) => {
|
||||
const value = String(currentValues[field as keyof CustomerUpdateData]);
|
||||
changedData[field] = value;
|
||||
});
|
||||
|
||||
console.log(changedData);
|
||||
|
||||
onSubmit(changedData);
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<CustomerUpdateData>) => {
|
||||
console.error("Errores en el formulario:", errors);
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.reset(defaultValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormDebug form={form} />
|
||||
<form id='customer-edit-form' onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||
<div className='flex gap-6'>
|
||||
<div className='w-full xl:w-2/3 space-y-12'>
|
||||
<CustomerBasicInfoFields control={form.control} />
|
||||
<CustomerContactFields control={form.control} />
|
||||
<CustomerAddressFields control={form.control} />
|
||||
<CustomerAdditionalConfigFields control={form.control} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@ -5,15 +5,19 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useUrlParamId } from "@erp/core/hooks";
|
||||
import { showErrorToast, showSuccessToast } from "@repo/shadcn-ui/lib/utils";
|
||||
import { FieldErrors } from "react-hook-form";
|
||||
import { CustomerEditorSkeleton, ErrorAlert, NotFoundCard } from "../../components";
|
||||
import {
|
||||
CustomerEditForm,
|
||||
CustomerEditorSkeleton,
|
||||
ErrorAlert,
|
||||
NotFoundCard,
|
||||
} from "../../components";
|
||||
import { useCustomerQuery, useUpdateCustomerMutation } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerUpdateData } from "../../schemas";
|
||||
import { CustomerEditForm } from "./customer-edit-form";
|
||||
import { CustomerFormData } from "../../schemas";
|
||||
|
||||
export const CustomerUpdate = () => {
|
||||
const { t } = useTranslation();
|
||||
const customerId = useUrlParamId();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 1) Estado de carga del cliente (query)
|
||||
@ -33,7 +37,8 @@ export const CustomerUpdate = () => {
|
||||
} = useUpdateCustomerMutation();
|
||||
|
||||
// 3) Submit con navegación condicionada por éxito
|
||||
const handleSubmit = async (formData: CustomerUpdateData) => {
|
||||
const handleSubmit = async (formData: CustomerFormData) => {
|
||||
console.log(formData);
|
||||
try {
|
||||
const result = await mutateAsync({ id: customerId!, data: formData });
|
||||
console.log(result);
|
||||
@ -48,7 +53,7 @@ export const CustomerUpdate = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<CustomerUpdateData>) => {
|
||||
const handleError = (errors: FieldErrors<CustomerFormData>) => {
|
||||
console.error("Errores en el formulario:", errors);
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
@ -110,6 +115,7 @@ export const CustomerUpdate = () => {
|
||||
className='cursor-pointer'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/customers/list");
|
||||
}}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
@ -117,7 +123,7 @@ export const CustomerUpdate = () => {
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
form='customer-edit-form'
|
||||
form='customer-update-form'
|
||||
className='cursor-pointer'
|
||||
disabled={isUpdating || isLoadingCustomer}
|
||||
aria-busy={isUpdating}
|
||||
@ -140,12 +146,12 @@ export const CustomerUpdate = () => {
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
{/* Importante: proveemos un formId para que el botón del header pueda hacer submit */}
|
||||
<CustomerEditForm
|
||||
defaultValues={customerData}
|
||||
formId={"customer-update-form"} // para que el botón del header pueda hacer submit
|
||||
initialValues={customerData}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
isPending={isUpdating}
|
||||
disabled={isLoadingCustomer}
|
||||
/>
|
||||
</div>
|
||||
</AppContent>
|
||||
@ -1 +1 @@
|
||||
export * from "./update";
|
||||
export * from "./customer-update";
|
||||
|
||||
4
modules/customers/src/web/schemas/customer.api.schema.ts
Normal file
4
modules/customers/src/web/schemas/customer.api.schema.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { CreateCustomerRequestSchema, UpdateCustomerByIdRequestSchema } from "@erp/customers";
|
||||
|
||||
export const CustomerCreateSchema = CreateCustomerRequestSchema;
|
||||
export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema;
|
||||
66
modules/customers/src/web/schemas/customer.form.schema.ts
Normal file
66
modules/customers/src/web/schemas/customer.form.schema.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const CustomerFormSchema = z.object({
|
||||
reference: z.string().optional(),
|
||||
|
||||
is_company: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
trade_name: z.string().optional(),
|
||||
tin: z.string().optional(),
|
||||
default_taxes: z.array(z.string()).optional(), // completo (sustituye), o null => vaciar
|
||||
|
||||
street: z.string().optional(),
|
||||
street2: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
province: z.string().optional(),
|
||||
postal_code: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
|
||||
email_primary: z.string().optional(),
|
||||
email_secondary: z.string().optional(),
|
||||
phone_primary: z.string().optional(),
|
||||
phone_secondary: z.string().optional(),
|
||||
mobile_primary: z.string().optional(),
|
||||
mobile_secondary: z.string().optional(),
|
||||
|
||||
fax: z.string().optional(),
|
||||
website: z.string().optional(),
|
||||
|
||||
legal_record: z.string().optional(),
|
||||
|
||||
language_code: z.string().optional(),
|
||||
currency_code: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CustomerFormData = z.infer<typeof CustomerFormSchema>;
|
||||
|
||||
export const defaultCustomerFormData: CustomerFormData = {
|
||||
reference: "",
|
||||
|
||||
is_company: "false",
|
||||
name: "",
|
||||
trade_name: "",
|
||||
tin: "",
|
||||
default_taxes: [],
|
||||
|
||||
street: "",
|
||||
street2: "",
|
||||
city: "",
|
||||
province: "",
|
||||
postal_code: "",
|
||||
country: "es",
|
||||
|
||||
email_primary: "",
|
||||
email_secondary: "",
|
||||
phone_primary: "",
|
||||
phone_secondary: "",
|
||||
mobile_primary: "",
|
||||
mobile_secondary: "",
|
||||
|
||||
fax: "",
|
||||
website: "",
|
||||
|
||||
legal_record: "",
|
||||
language_code: "es",
|
||||
currency_code: "EUR",
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
import {
|
||||
CreateCustomerRequestDTO,
|
||||
CreateCustomerRequestSchema,
|
||||
GetCustomerByIdResponseDTO,
|
||||
UpdateCustomerByIdRequestDTO,
|
||||
UpdateCustomerByIdRequestSchema,
|
||||
} from "@erp/customers";
|
||||
|
||||
export type CustomerData = GetCustomerByIdResponseDTO;
|
||||
|
||||
export const CustomerCreateSchema = CreateCustomerRequestSchema;
|
||||
export type CustomerCreateData = CreateCustomerRequestDTO;
|
||||
|
||||
export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema;
|
||||
export type CustomerUpdateData = UpdateCustomerByIdRequestDTO;
|
||||
@ -1 +1,2 @@
|
||||
export * from "./customer.schema";
|
||||
export * from "./customer.api.schema";
|
||||
export * from "./customer.form.schema";
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@ -8,13 +8,11 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { SyntheticEvent } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface CustomDialogProps {
|
||||
isOpen: boolean;
|
||||
onCancel: (event: SyntheticEvent) => void;
|
||||
onConfirm: (event: SyntheticEvent) => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
cancelLabel: React.ReactNode;
|
||||
@ -31,23 +29,15 @@ export const CustomDialog = ({
|
||||
confirmLabel,
|
||||
}: CustomDialogProps) => {
|
||||
return (
|
||||
<AlertDialog open={isOpen}>
|
||||
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Link to='#' onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
</Link>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction>
|
||||
<Link to='#' onClick={onConfirm}>
|
||||
{confirmLabel}
|
||||
</Link>
|
||||
</AlertDialogAction>
|
||||
<AlertDialogCancel onClick={onCancel}>{cancelLabel}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm}>{confirmLabel}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
104
packages/rdx-ui/src/components/dynamics-tabs.tsx
Normal file
104
packages/rdx-ui/src/components/dynamics-tabs.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
const DynamicTabsListVariants = cva("inline-flex items-center justify-center", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
underline: "border-b rounded-none bg-background gap-2 p-0 justify-start",
|
||||
},
|
||||
width: {
|
||||
full: "w-full",
|
||||
fit: "w-fit",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
const DynamicTabsTriggerVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap text-sm font-normal transition-all disabled:pointer-events-none data-[state=active]:text-foreground px-3",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
underline:
|
||||
"bg-background border-b-2 border-background focus:border-primary ring-0 outline-none shadow-none data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary disabled:opacity-100 data-[state=active]:shadow-none rounded-none m-0 pt-1.5 pb-2 hover:bg-background-muted",
|
||||
},
|
||||
width: {
|
||||
full: "w-full",
|
||||
fit: "w-fit",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function DynamicTabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot='tabs'
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DynamicTabsList({
|
||||
className,
|
||||
variant,
|
||||
width,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot='tabs-list'
|
||||
className={cn(DynamicTabsListVariants({ variant, width, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DynamicTabsTrigger({
|
||||
className,
|
||||
variant,
|
||||
width,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot='tabs-trigger'
|
||||
className={cn(DynamicTabsTriggerVariants({ variant, width, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DynamicTabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot='tabs-content'
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { DynamicTabs, DynamicTabsContent, DynamicTabsList, DynamicTabsTrigger };
|
||||
@ -1,6 +1,7 @@
|
||||
export * from "./buttons/index.tsx";
|
||||
export * from "./custom-dialog.tsx";
|
||||
export * from "./datatable/index.tsx";
|
||||
export * from "./dynamics-tabs.tsx";
|
||||
export * from "./error-overlay.tsx";
|
||||
export * from "./form/index.tsx";
|
||||
export * from "./grid/index.ts";
|
||||
|
||||
@ -179,7 +179,7 @@ importers:
|
||||
version: 29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3))
|
||||
ts-jest:
|
||||
specifier: ^29.2.5
|
||||
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@ -703,6 +703,9 @@ importers:
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@19.1.0)
|
||||
'@radix-ui/react-tabs':
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@repo/shadcn-ui':
|
||||
specifier: workspace:*
|
||||
version: link:../shadcn-ui
|
||||
@ -12292,7 +12295,7 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
ejs: 3.1.10
|
||||
@ -12310,7 +12313,6 @@ snapshots:
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.27.4)
|
||||
esbuild: 0.25.5
|
||||
jest-util: 29.7.0
|
||||
|
||||
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user