Clientes y facturas de cliente

This commit is contained in:
David Arranz 2025-09-24 17:09:37 +02:00
parent 7e700bdf22
commit 327756413d
32 changed files with 109 additions and 114 deletions

View File

@ -8,5 +8,5 @@ export const registerModules = () => {
//registerModule(authAPIModule);
registerModule(customersAPIModule);
registerModule(customerInvoicesAPIModule);
// registerModule(verifactuAPIModule);
//registerModule(verifactuAPIModule);
};

View File

@ -1,5 +1,6 @@
import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { ModuleParams, validateRequest } from "@erp/core/api";
import { ILogger } from "@repo/rdx-logger";
import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize";
import {
@ -115,5 +116,5 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
}
);
app.use(`${baseRoutePath}/proforma-invoices`, router);
app.use(`${baseRoutePath}/customer-invoices`, router);
};

View File

@ -10,6 +10,7 @@ import { useMemo, useState } from "react";
import { MoneyDTO } from "@erp/core";
import { formatDate, formatMoney } from "@erp/core/client";
import { ErrorOverlay } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { AgGridReact } from "ag-grid-react";
import { ChevronRightIcon } from "lucide-react";
@ -23,7 +24,6 @@ ModuleRegistry.registerModules([AllCommunityModule]);
// Create new GridExample component
export const CustomerInvoicesListGrid = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const {
@ -141,6 +141,19 @@ export const CustomerInvoicesListGrid = () => {
[autoSizeStrategy, colDefs]
);
if (isLoadError) {
return (
<>
<ErrorOverlay
errorMessage={
(loadError as Error)?.message ??
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
}
/>
</>
);
}
// Container: Defines the grid's theme & dimensions.
return (
<div

View File

@ -15,25 +15,6 @@ const CustomerInvoiceAdd = lazy(() =>
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
);
//const LogoutPage = lazy(() => import("./app").then((m) => ({ default: m.LogoutPage })));
/*const DealerLayout = lazy(() => import("./app").then((m) => ({ default: m.DealerLayout })));
const DealersList = lazy(() => import("./app").then((m) => ({ default: m.DealersList })));
const LoginPageWithLanguageSelector = lazy(() =>
import("./app").then((m) => ({ default: m.LoginPageWithLanguageSelector }))
);
const CustomerInvoiceEdit = lazy(() => import("./app").then((m) => ({ default: m.CustomerInvoiceEdit })));
const SettingsEditor = lazy(() => import("./app").then((m) => ({ default: m.SettingsEditor })));
const SettingsLayout = lazy(() => import("./app").then((m) => ({ default: m.SettingsLayout })));
const CatalogLayout = lazy(() => import("./app").then((m) => ({ default: m.CatalogLayout })));
const CatalogList = lazy(() => import("./app").then((m) => ({ default: m.CatalogList })));
const DashboardPage = lazy(() => import("./app").then((m) => ({ default: m.DashboardPage })));
const CustomerInvoicesLayout = lazy(() => import("./app").then((m) => ({ default: m.CustomerInvoicesLayout })));
const CustomerInvoicesList = lazy(() => import("./app").then((m) => ({ default: m.CustomerInvoicesList })));*/
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
return [
{

View File

@ -1,7 +1,6 @@
import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { CustomerInvoicesListGrid } from "../components";
import { useTranslation } from "../i18n";
@ -9,17 +8,6 @@ import { useTranslation } from "../i18n";
export const CustomerInvoicesList = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [status, setStatus] = useState("all");
/*const CustomerInvoiceStatuses = [
{ value: "all", label: t("customerInvoices.list.tabs.all") },
{ value: "draft", label: t("customerInvoices.list.tabs.draft") },
{ value: "ready", label: t("customerInvoices.list.tabs.ready") },
{ value: "delivered", label: t("customerInvoices.list.tabs.delivered") },
{ value: "accepted", label: t("customerInvoices.list.tabs.accepted") },
{ value: "rejected", label: t("customerInvoices.list.tabs.rejected") },
{ value: "archived", label: t("customerInvoices.list.tabs.archived") },
];*/
return (
<>

View File

@ -1,2 +1,2 @@
export * from "./create";
export * from "./list";
export * from "./customer-invoices-list";

View File

@ -0,0 +1,13 @@
//import * as z from "zod/v4";
import { ListCustomerInvoicesResponseDTO } from "@erp/customer-invoices/common";
/*export const CustomerCreateSchema = CreateCustomerRequestSchema;
export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema;
export const CustomerSchema = GetCustomerByIdResponseSchema.omit({
metadata: true,
});
export type CustomerData = z.infer<typeof CustomerSchema>;*/
export type CustomerInvoicesListData = ListCustomerInvoicesResponseDTO;

View File

@ -0,0 +1,2 @@
export * from "./customer-invoices.api.schema";
export * from "./customer-invoices.form.schema";

View File

@ -1,6 +1,7 @@
import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { CustomerListDTO } from "../../infrastructure/mappers";
import { Customer } from "../aggregates";
/**
@ -42,7 +43,7 @@ export interface ICustomerRepository {
companyId: UniqueID,
criteria: Criteria,
transaction?: any
): Promise<Result<Collection<CustomerListDTO>>>;
): Promise<Result<Collection<CustomerListDTO>, Error>>;
/**
* Elimina un Customer por su ID, dentro de una empresa.

View File

@ -14,10 +14,10 @@ export class CustomerAddressType extends ValueObject<ICustomerAddressTypeProps>
private static readonly ALLOWED_TYPES = ["shipping", "billing"];
static create(value: string): Result<CustomerAddressType, Error> {
if (!this.ALLOWED_TYPES.includes(value)) {
if (!CustomerAddressType.ALLOWED_TYPES.includes(value)) {
return Result.fail(
new Error(
`Invalid address type: ${value}. Allowed types are: ${this.ALLOWED_TYPES.join(", ")}`
`Invalid address type: ${value}. Allowed types are: ${CustomerAddressType.ALLOWED_TYPES.join(", ")}`
)
);
}

View File

@ -1,5 +1,4 @@
import { DomainValidationError } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import * as z from "zod/v4";

View File

@ -1,5 +1,4 @@
import { DomainValidationError } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import * as z from "zod/v4";

View File

@ -1,5 +1,4 @@
import { DomainValidationError } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
interface ICustomerStatusProps {

View File

@ -5,6 +5,12 @@
"cancel": "Cancel",
"save": "Save"
},
"catalog": {
"status": {
"active": "active",
"inactive": "inactive"
}
},
"pages": {
"title": "Customers",
"description": "Manage your customers",

View File

@ -5,6 +5,12 @@
"cancel": "Cancelar",
"save": "Guardar"
},
"catalog": {
"status": {
"active": "activo",
"inactive": "inactivo"
}
},
"pages": {
"title": "Clientes",
"description": "Gestiona tus clientes",
@ -96,6 +102,7 @@
"placeholder": "Ingrese el correo electrónico",
"description": "La dirección de correo electrónico principal del cliente"
},
"email_secondary": {
"label": "Email secundario",
"placeholder": "Ingrese el correo electrónico",
@ -107,6 +114,7 @@
"placeholder": "Ingrese el número de teléfono",
"description": "El número de teléfono del cliente"
},
"phone_secondary": {
"label": "Teléfono secundario",
"placeholder": "Ingrese el número de teléfono secundario",
@ -118,6 +126,7 @@
"placeholder": "Ingrese el número de teléfono",
"description": "El número de teléfono del cliente"
},
"mobile_secondary": {
"label": "Teléfono secundario",
"placeholder": "Ingrese el número de teléfono secundario",
@ -129,21 +138,25 @@
"placeholder": "Ingrese el número de fax",
"description": "El número de fax del cliente"
},
"website": {
"label": "Sitio web",
"placeholder": "Ingrese la URL del sitio web",
"description": "El sitio web del cliente"
},
"default_taxes": {
"label": "Impuesto por defecto",
"placeholder": "Seleccione el impuesto por defecto",
"description": "La tasa de impuesto por defecto para el cliente"
},
"language_code": {
"label": "Idioma",
"placeholder": "Seleccione el idioma",
"description": "El idioma preferido del cliente"
},
"currency_code": {
"label": "Moneda",
"placeholder": "Seleccione la moneda",

View File

@ -42,7 +42,7 @@ export const CustomerStatusBadge = forwardRef<HTMLDivElement, CustomerStatusBadg
return (
<Badge className={cn(commonClassName, config.badge, className)} {...props}>
<div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />
{t(`status.${status}`)}
{t(`catalog.status.${status}`)}
</Badge>
);
}

View File

@ -67,7 +67,7 @@ export const CustomersListGrid = () => {
{
field: "status",
headerName: t("pages.list.grid_columns.status"),
maxWidth: 125,
maxWidth: 135,
cellRenderer: (params: ValueFormatterParams) => {
return <CustomerStatusBadge status={params.value} />;
},

View File

@ -33,8 +33,8 @@ export const CustomerBasicInfoFields = () => {
return (
<Fieldset>
<Legend>Identificación</Legend>
<Description>descripción</Description>
<Legend>{t("form_groups.basic_info.title")}</Legend>
<Description>{t("form_groups.basic_info.description")}</Description>
<FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
<Field className='lg:col-span-2'>
<TextField

View File

@ -12,25 +12,6 @@ const CustomersList = lazy(() => import("./pages").then((m) => ({ default: m.Cus
const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreate })));
//const LogoutPage = lazy(() => import("./app").then((m) => ({ default: m.LogoutPage })));
/*const DealerLayout = lazy(() => import("./app").then((m) => ({ default: m.DealerLayout })));
const DealersList = lazy(() => import("./app").then((m) => ({ default: m.DealersList })));
const LoginPageWithLanguageSelector = lazy(() =>
import("./app").then((m) => ({ default: m.LoginPageWithLanguageSelector }))
);
const CustomerEdit = lazy(() => import("./app").then((m) => ({ default: m.CustomerEdit })));
const SettingsEditor = lazy(() => import("./app").then((m) => ({ default: m.SettingsEditor })));
const SettingsLayout = lazy(() => import("./app").then((m) => ({ default: m.SettingsLayout })));
const CatalogLayout = lazy(() => import("./app").then((m) => ({ default: m.CatalogLayout })));
const CatalogList = lazy(() => import("./app").then((m) => ({ default: m.CatalogList })));
const DashboardPage = lazy(() => import("./app").then((m) => ({ default: m.DashboardPage })));
const CustomersLayout = lazy(() => import("./app").then((m) => ({ default: m.CustomersLayout })));
const CustomersList = lazy(() => import("./app").then((m) => ({ default: m.CustomersList })));*/
export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
return [
{

View File

@ -1,13 +1,13 @@
import { useDataSource, useQueryKey } from "@erp/core/hooks";
import { ListCustomersResponseDTO } from "@erp/customer-invoices/common";
import { useQuery } from "@tanstack/react-query";
import { CustomersListData } from "../schemas";
// Obtener todas las facturas
export const useCustomersQuery = (params?: any) => {
const dataSource = useDataSource();
const keys = useQueryKey();
return useQuery<ListCustomersResponseDTO>({
return useQuery<CustomersListData>({
queryKey: keys().data().resource("customers").action("list").params(params).get(),
queryFn: async (context) => {
const { signal } = context;
@ -16,7 +16,7 @@ export const useCustomersQuery = (params?: any) => {
...params,
});
return customers as ListCustomersResponseDTO;
return customers as CustomersListData;
},
});
};

View File

@ -63,7 +63,7 @@ export const CustomerCreate = () => {
<AppBreadcrumb />
<AppContent>
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
<div className='flex items-center justify-between space-y-4 px-6'>
<div className='flex items-center justify-between space-y-6'>
<div className='space-y-2'>
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
{t("pages.create.title")}
@ -97,15 +97,13 @@ export const CustomerCreate = () => {
/>
)}
<div className='flex flex-1 flex-col gap-4 p-4'>
<FormProvider {...form}>
<CustomerEditForm
formId='customer-create-form'
onSubmit={handleSubmit}
onError={handleError}
/>
</FormProvider>
</div>
<FormProvider {...form}>
<CustomerEditForm
formId='customer-create-form'
onSubmit={handleSubmit}
onError={handleError}
/>
</FormProvider>
</UnsavedChangesProvider>
</AppContent>
</>

View File

@ -13,7 +13,7 @@ export const CustomersList = () => {
<>
<AppBreadcrumb />
<AppContent>
<div className='flex items-center justify-between space-y-2'>
<div className='flex items-center justify-between space-y-6'>
<div>
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.list.title")}</h2>
<p className='text-muted-foreground'>{t("pages.list.description")}</p>
@ -21,7 +21,7 @@ export const CustomersList = () => {
<div className='flex items-center space-x-2'>
<Button onClick={() => navigate("/customers/create")} className='cursor-pointer'>
<PlusIcon className='w-4 h-4 mr-2' />
{t("pages.create.title")}
{t("pages.list.title")}
</Button>
</div>
</div>

View File

@ -1,2 +1,2 @@
export * from "./create";
export * from "./list";
export * from "./customer-list";

View File

@ -121,7 +121,7 @@ export const CustomerUpdate = () => {
<AppBreadcrumb />
<AppContent>
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
<div className='flex items-center justify-between space-y-4 px-6'>
<div className='flex items-center justify-between space-y-6'>
<div className='space-y-2'>
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
{t("pages.update.title")}
@ -156,15 +156,13 @@ export const CustomerUpdate = () => {
/>
)}
<div className='flex flex-1 flex-col gap-4 p-4'>
<FormProvider {...form}>
<CustomerEditForm
formId={"customer-update-form"} // para que el botón del header pueda hacer submit
onSubmit={handleSubmit}
onError={handleError}
/>
</FormProvider>
</div>
<FormProvider {...form}>
<CustomerEditForm
formId={"customer-update-form"} // para que el botón del header pueda hacer submit
onSubmit={handleSubmit}
onError={handleError}
/>
</FormProvider>
</UnsavedChangesProvider>
</AppContent>
</>

View File

@ -1,15 +0,0 @@
import * as z from "zod/v4";
import { CustomerFormSchema } from "./customer.form.schema";
export const UpdateCustomerFormSchema = CustomerFormSchema.extend({
is_company: CustomerFormSchema.shape.is_company.optional(),
name: CustomerFormSchema.shape.name.optional(),
default_taxes: z.array(z.string()).optional(),
country: CustomerFormSchema.shape.country.optional(),
language_code: CustomerFormSchema.shape.language_code.optional(),
currency_code: CustomerFormSchema.shape.currency_code.optional(),
});
export type UpdateCustomerFormData = z.infer<typeof UpdateCustomerFormSchema>;

View File

@ -3,6 +3,7 @@ import * as z from "zod/v4";
import {
CreateCustomerRequestSchema,
GetCustomerByIdResponseSchema,
ListCustomersResponseDTO,
UpdateCustomerByIdRequestSchema,
} from "@erp/customers";
@ -13,3 +14,5 @@ export const CustomerSchema = GetCustomerByIdResponseSchema.omit({
});
export type CustomerData = z.infer<typeof CustomerSchema>;
export type CustomersListData = ListCustomersResponseDTO;

View File

@ -3,12 +3,14 @@
"version": "0.0.1",
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {},
"exports": { "./api": "./src/api/index.ts" },
"peerDependencies": {
"sequelize": "^6.37.5"
"sequelize": "^6.37.5",
"express": "^4.18.2"
},
"devDependencies": {},
"devDependencies": { "@types/express": "^4.17.21" },
"dependencies": {
"@erp/auth": "workspace:*",
"@erp/core": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-utils": "workspace:*",

View File

@ -8,6 +8,7 @@ export const verifactuAPIModule: IModuleServer = {
async init(params: ModuleParams) {
// const contacts = getService<ContactsService>("contacts");
console.log("111111111111111111111111111A>>>>>>>>>>>>>>>>>>>");
const { logger } = params;
verifactuRouter(params);
logger.info("🚀 Verifactu module initialized", { label: this.name });

View File

@ -14,6 +14,7 @@ export const verifactuRouter = (params: ModuleParams) => {
logger: ILogger;
};
console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>>>>>>>>>>>>>>>>>>>");
const deps = buildVerifactuDependencies(params);
const router: Router = Router({ mergeParams: true });

View File

@ -20,7 +20,7 @@ interface ErrorOverlayProps {
export const ErrorOverlay = ({
title = "Se ha producido un error",
subtitle = undefined,
subtitle = "Inténtalo de nuevo más tarde",
description = undefined,
errorMessage = undefined,
}: //errorStatusCode = undefined,
@ -32,7 +32,7 @@ ErrorOverlayProps): JSX.Element => {
: _DrawByStatusCode['0'];*/
return (
<div className='grid h-screen place-items-center '>
<div className='h-fit place-items-center-safe mt-10 '>
<div className='text-center'>
<h2 className='mt-2 text-xl font-semibold text-center text-slate-900'>{title}</h2>
<p className='mt-1 font-medium text-slate-500'>

View File

@ -662,6 +662,9 @@ importers:
modules/verifactu:
dependencies:
'@erp/auth':
specifier: workspace:*
version: link:../auth
'@erp/core':
specifier: workspace:*
version: link:../core
@ -674,9 +677,16 @@ importers:
'@repo/rdx-utils':
specifier: workspace:*
version: link:../../packages/rdx-utils
express:
specifier: ^4.18.2
version: 4.21.2
sequelize:
specifier: ^6.37.5
version: 6.37.7(mysql2@3.14.1)
devDependencies:
'@types/express':
specifier: ^4.17.21
version: 4.17.23
packages/rdx-criteria:
dependencies: