Facturas de cliente

This commit is contained in:
David Arranz 2025-09-29 10:42:46 +02:00
parent 198137d426
commit 5584b6039b
19 changed files with 177 additions and 106 deletions

View File

@ -1,6 +1,7 @@
import { Toaster, TooltipProvider } from "@repo/shadcn-ui/components";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { I18nextProvider } from "react-i18next";
import { i18n } from "@/locales";

View File

@ -27,6 +27,7 @@
"@repo/shadcn-ui": "workspace:*",
"@hookform/resolvers": "^5.0.1",
"@tanstack/react-query": "^5.75.4",
"ag-grid-community": "^33.3.0",
"axios": "^1.9.0",
"express": "^4.18.2",
"http-status": "^2.1.0",

View File

@ -1,30 +0,0 @@
import { MultiSelectField, MultiSelectFieldProps } from "@repo/rdx-ui/components";
import * as React from "react";
import type { FieldValues } from "react-hook-form";
import { TaxesList } from "../../constants";
/**
* Igual que MultiSelect pero con `options` preconfiguradas a TaxesList.
* Puedes sobreescribir `options` si lo necesitas, se mergean (TaxesList primero).
*/
export type TaxesMultiSelectFieldProps<T extends FieldValues> = Omit<
MultiSelectFieldProps<T>,
"options"
>;
const TaxesMultiSelectFieldInner = React.forwardRef(
<T extends FieldValues>(
props: TaxesMultiSelectFieldProps<T>,
ref: React.Ref<HTMLButtonElement>
) => {
return (
<MultiSelectField ref={ref} {...(props as MultiSelectFieldProps<T>)} options={TaxesList} />
);
}
);
TaxesMultiSelectFieldInner.displayName = "TaxesMultiSelectField";
export const TaxesMultiSelectField = TaxesMultiSelectFieldInner as <T extends FieldValues>(
p: TaxesMultiSelectFieldProps<T> & { ref?: React.Ref<HTMLButtonElement> }
) => React.JSX.Element;

View File

@ -1,2 +1,6 @@
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
ModuleRegistry.registerModules([AllCommunityModule]);
export * from "./form";
export * from "./taxes-multi-select";

View File

@ -11,6 +11,7 @@
},
"peerDependencies": {
"@tanstack/react-query": "^5.74.11",
"ag-grid-community": "^33.3.0",
"dinero.js": "^1.9.1",
"express": "^4.18.2",
"handlebars": "^4.7.8",
@ -46,7 +47,6 @@
"@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"@tanstack/react-table": "^8.21.3",
"ag-grid-community": "^33.3.0",
"ag-grid-react": "^33.3.0",
"date-fns": "^4.1.0",
"libphonenumber-js": "^1.12.7",

View File

@ -13,7 +13,6 @@ export const CreateCustomerInvoiceItemRequestSchema = z.object({
export const CreateCustomerInvoiceRequestSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
invoice_number: z.string(),
series: z.string().default(""),

View File

@ -1,9 +1,24 @@
import { z } from "zod/v4";
export const UpdateCustomerInvoiceByIdParamsRequestSchema = z.object({
customer_id: z.string(),
invoice_id: z.string(),
});
export const UpdateCustomerByIdRequestSchema = z.object({});
export const UpdateCustomerInvoiceByIdRequestSchema = z.object({
invoice_number: z.string(),
series: z.string().default(""),
export type UpdateCustomerByIdRequestDTO = z.infer<typeof UpdateCustomerByIdRequestSchema>;
invoice_date: z.string(),
operation_date: z.string().default(""),
customer_id: z.uuid(),
notes: z.string().default(""),
language_code: z.string().toLowerCase().default("es"),
currency_code: z.string().toUpperCase().default("EUR"),
});
export type UpdateCustomerInvoiceByIdRequestDTO = Partial<
z.infer<typeof UpdateCustomerInvoiceByIdRequestSchema>
>;

View File

@ -18,6 +18,11 @@
"series": "Serie",
"status": "Status",
"invoice_date": "Date",
"recipient_tin": "Customer TIN",
"recipient_name": "Customer name",
"recipient_city": "Customer city",
"recipient_province": "Customer province",
"recipient_postal_code": "Customer postal code",
"total_amount": "Total price"
}
},

View File

@ -5,7 +5,7 @@ import type {
SizeColumnsToFitProvidedWidthStrategy,
ValueFormatterParams,
} from "ag-grid-community";
import { AllCommunityModule, ColDef, GridOptions, ModuleRegistry } from "ag-grid-community";
import { ColDef, GridOptions } from "ag-grid-community";
import { useMemo, useState } from "react";
import { MoneyDTO } from "@erp/core";
@ -19,8 +19,6 @@ import { useCustomerInvoicesQuery } from "../hooks";
import { useTranslation } from "../i18n";
import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
ModuleRegistry.registerModules([AllCommunityModule]);
// Create new GridExample component
export const CustomerInvoicesListGrid = () => {
const { t } = useTranslation();
@ -58,12 +56,14 @@ export const CustomerInvoicesListGrid = () => {
return formatDate(params.value);
},
},
{ field: "recipient.tin" },
{ field: "recipient.name" },
{ field: "recipient.city" },
{ field: "recipient.province" },
{ field: "recipient.postal_code" },
{ field: "recipient.tin", headerName: t("pages.list.grid_columns.recipient_tin") },
{ field: "recipient.name", headerName: t("pages.list.grid_columns.recipient_name") },
{ field: "recipient.city", headerName: t("pages.list.grid_columns.recipient_city") },
{ field: "recipient.province", headerName: t("pages.list.grid_columns.recipient_province") },
{
field: "recipient.postal_code",
headerName: t("pages.list.grid_columns.recipient_postal_code"),
},
{
field: "taxable_amount",
headerName: t("pages.list.grid_columns.taxable_amount"),

View File

@ -1,20 +1,43 @@
import { useDataSource, useQueryKey } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { CreateCustomerInvoiceRequestDTO } from "../../common/dto";
import { useDataSource } from "@erp/core/hooks";
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
import { CreateCustomerInvoiceRequestSchema } from "../../common";
import { CustomerInvoiceData, CustomerInvoiceFormData } from "../schemas";
type CreateCustomerInvoicePayload = {
data: CustomerInvoiceFormData;
};
export const useCreateCustomerInvoiceMutation = () => {
const queryClient = useQueryClient();
const dataSource = useDataSource();
const keys = useQueryKey();
const schema = CreateCustomerInvoiceRequestSchema;
return useMutation<
CreateCustomerInvoiceRequestDTO,
Error,
Partial<CreateCustomerInvoiceRequestDTO>
>({
mutationFn: (data) => {
console.log(data);
return dataSource.createOne("customer-invoices", data);
return useMutation<CustomerInvoiceData, DefaultError, CreateCustomerInvoicePayload>({
mutationKey: ["customer-invoice:create"],
mutationFn: async (payload) => {
const { data } = payload;
const invoiceId = UniqueID.generateNewID();
const newInvoiceData = {
...data,
id: invoiceId.toString(),
};
const result = schema.safeParse(newInvoiceData);
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 created = await dataSource.createOne("customer-invoices", newInvoiceData);
return created as CustomerInvoiceData;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["customer-invoices"] });

View File

@ -0,0 +1,69 @@
import { useDataSource } from "@erp/core/hooks";
import { ValidationErrorCollection } from "@repo/rdx-ddd";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
UpdateCustomerInvoiceByIdRequestDTO,
UpdateCustomerInvoiceByIdRequestSchema,
} from "../../common";
import { CustomerInvoiceFormData } from "../schemas";
import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-customer-invoice-query";
export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const;
type UpdateCustomerInvoiceContext = {};
type UpdateCustomerInvoicePayload = {
id: string;
data: Partial<CustomerInvoiceFormData>;
};
export function useUpdateCustomerInvoice() {
const queryClient = useQueryClient();
const dataSource = useDataSource();
const schema = UpdateCustomerInvoiceByIdRequestSchema;
return useMutation<
CustomerInvoiceFormData,
Error,
UpdateCustomerInvoicePayload,
UpdateCustomerInvoiceContext
>({
mutationKey: ["customer-invoice:update"], //, customerId],
mutationFn: async (payload) => {
const { id: invoiceId, data } = payload;
if (!invoiceId) {
throw new Error("customerInvoiceId 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("customer-invoices", invoiceId, data);
return updated as CustomerInvoiceFormData;
},
onSuccess: (updated: CustomerInvoiceFormData, variables) => {
const { id: invoiceId } = variables;
// Refresca inmediatamente el detalle
queryClient.setQueryData<UpdateCustomerInvoiceByIdRequestDTO>(
CUSTOMER_INVOICE_QUERY_KEY(invoiceId),
updated
);
// Otra opción es invalidar el detalle para forzar refetch:
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
// Invalida el listado para refrescar desde servidor
queryClient.invalidateQueries({ queryKey: CUSTOMER_INVOICES_LIST_KEY });
},
});
}

View File

@ -13,7 +13,7 @@ export const CustomerInvoicesList = () => {
<>
<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>
@ -28,7 +28,7 @@ export const CustomerInvoicesList = () => {
</Button>
</div>
</div>
<div className='flex flex-col w-full h-full py-4'>
<div className='flex flex-col w-full h-full py-3'>
<CustomerInvoicesListGrid />
</div>
</AppContent>

View File

@ -6,4 +6,10 @@ export const CustomerInvoiceFormSchema = z.object({
series: z.string().optional(),
});
export const CustomerInvoiceFormData = z.infer<typeof CustomerInvoiceFormSchema>;
export type CustomerInvoiceFormData = z.infer<typeof CustomerInvoiceFormSchema>;
export const defaultCustomerFormData: CustomerInvoiceFormData = {
invoice_number: "",
status: "draft",
series: "",
};

View File

@ -12,6 +12,7 @@
},
"peerDependencies": {
"@tanstack/react-query": "^5.74.11",
"ag-grid-community": "^33.3.0",
"dinero.js": "^1.9.1",
"express": "^4.18.2",
"i18next": "^25.1.1",
@ -38,7 +39,6 @@
"@repo/rdx-ui": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"ag-grid-community": "^33.3.0",
"ag-grid-react": "^33.3.0",
"lucide-react": "^0.503.0",
"react": "^19.1.0",

View File

@ -1,16 +1,15 @@
import { AG_GRID_LOCALE_ES } from "@ag-grid-community/locale";
import type { ValueFormatterParams } from "ag-grid-community";
import {
AllCommunityModule,
ColDef,
GridOptions,
ModuleRegistry,
SizeColumnsToContentStrategy,
SizeColumnsToFitGridStrategy,
SizeColumnsToFitProvidedWidthStrategy,
} from "ag-grid-community";
import { useMemo, useState } from "react";
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";
@ -19,8 +18,6 @@ import { useCustomersQuery } from "../hooks";
import { useTranslation } from "../i18n";
import { CustomerStatusBadge } from "./customer-status-badge";
ModuleRegistry.registerModules([AllCommunityModule]);
// Create new GridExample component
export const CustomersListGrid = () => {
const { t } = useTranslation();
@ -124,6 +121,19 @@ export const CustomersListGrid = () => {
[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

@ -2,7 +2,7 @@ import { useDataSource, useQueryKey } from "@erp/core/hooks";
import { useQuery } from "@tanstack/react-query";
import { CustomersListData } from "../schemas";
// Obtener todas las facturas
// Obtener todos los clientes
export const useCustomersQuery = (params?: any) => {
const dataSource = useDataSource();
const keys = useQueryKey();

View File

@ -21,11 +21,11 @@ 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.list.title")}
{t("pages.create.title")}
</Button>
</div>
</div>
<div className='flex flex-col w-full h-full py-4'>
<div className='flex flex-col w-full h-full py-3'>
<CustomersListGrid />
</div>
<Outlet />

View File

@ -31,5 +31,5 @@
"engines": {
"node": ">=18"
},
"packageManager": "pnpm@10.15.0"
"packageManager": "pnpm@10.17.1"
}

View File

@ -191,7 +191,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))(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))(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)
tsconfig-paths:
specifier: ^4.2.0
version: 4.2.0
@ -395,6 +395,9 @@ importers:
'@tanstack/react-query':
specifier: ^5.75.4
version: 5.81.2(react@19.1.0)
ag-grid-community:
specifier: ^33.3.0
version: 33.3.2
axios:
specifier: ^1.9.0
version: 1.10.0
@ -572,8 +575,6 @@ importers:
specifier: ^5.8.3
version: 5.8.3
modules/customer-payments: {}
modules/customers:
dependencies:
'@ag-grid-community/locale':
@ -705,40 +706,6 @@ importers:
specifier: ^4.17.21
version: 4.17.23
modules/document-numbering:
dependencies:
'@erp/auth':
specifier: workspace:*
version: link:../auth
'@erp/core':
specifier: workspace:*
version: link:../core
'@repo/rdx-criteria':
specifier: workspace:*
version: link:../../packages/rdx-criteria
'@repo/rdx-ddd':
specifier: workspace:*
version: link:../../packages/rdx-ddd
'@repo/rdx-logger':
specifier: workspace:*
version: link:../../packages/rdx-logger
'@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)
zod:
specifier: ^4.1.11
version: 4.1.11
devDependencies:
'@types/express':
specifier: ^4.17.21
version: 4.17.23
modules/verifactu:
dependencies:
'@erp/auth':
@ -12871,7 +12838,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))(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))(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):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
@ -12889,6 +12856,7 @@ 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):