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 { Toaster, TooltipProvider } from "@repo/shadcn-ui/components";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import { i18n } from "@/locales"; import { i18n } from "@/locales";

View File

@ -27,6 +27,7 @@
"@repo/shadcn-ui": "workspace:*", "@repo/shadcn-ui": "workspace:*",
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@tanstack/react-query": "^5.75.4", "@tanstack/react-query": "^5.75.4",
"ag-grid-community": "^33.3.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"express": "^4.18.2", "express": "^4.18.2",
"http-status": "^2.1.0", "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 "./form";
export * from "./taxes-multi-select"; export * from "./taxes-multi-select";

View File

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

View File

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

View File

@ -1,9 +1,24 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
export const UpdateCustomerInvoiceByIdParamsRequestSchema = z.object({ 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", "series": "Serie",
"status": "Status", "status": "Status",
"invoice_date": "Date", "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" "total_amount": "Total price"
} }
}, },

View File

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

View File

@ -1,20 +1,43 @@
import { useDataSource, useQueryKey } from "@erp/core/hooks"; import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
import { CreateCustomerInvoiceRequestDTO } from "../../common/dto"; import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
import { CreateCustomerInvoiceRequestSchema } from "../../common";
import { CustomerInvoiceData, CustomerInvoiceFormData } from "../schemas";
type CreateCustomerInvoicePayload = {
data: CustomerInvoiceFormData;
};
export const useCreateCustomerInvoiceMutation = () => { export const useCreateCustomerInvoiceMutation = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const dataSource = useDataSource(); const dataSource = useDataSource();
const keys = useQueryKey(); const schema = CreateCustomerInvoiceRequestSchema;
return useMutation< return useMutation<CustomerInvoiceData, DefaultError, CreateCustomerInvoicePayload>({
CreateCustomerInvoiceRequestDTO, mutationKey: ["customer-invoice:create"],
Error,
Partial<CreateCustomerInvoiceRequestDTO> mutationFn: async (payload) => {
>({ const { data } = payload;
mutationFn: (data) => { const invoiceId = UniqueID.generateNewID();
console.log(data);
return dataSource.createOne("customer-invoices", data); 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: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["customer-invoices"] }); 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 /> <AppBreadcrumb />
<AppContent> <AppContent>
<div className='flex items-center justify-between space-y-2'> <div className='flex items-center justify-between space-y-6'>
<div> <div>
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.list.title")}</h2> <h2 className='text-2xl font-bold tracking-tight'>{t("pages.list.title")}</h2>
<p className='text-muted-foreground'>{t("pages.list.description")}</p> <p className='text-muted-foreground'>{t("pages.list.description")}</p>
@ -28,7 +28,7 @@ export const CustomerInvoicesList = () => {
</Button> </Button>
</div> </div>
</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 /> <CustomerInvoicesListGrid />
</div> </div>
</AppContent> </AppContent>

View File

@ -6,4 +6,10 @@ export const CustomerInvoiceFormSchema = z.object({
series: z.string().optional(), 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": { "peerDependencies": {
"@tanstack/react-query": "^5.74.11", "@tanstack/react-query": "^5.74.11",
"ag-grid-community": "^33.3.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"express": "^4.18.2", "express": "^4.18.2",
"i18next": "^25.1.1", "i18next": "^25.1.1",
@ -38,7 +39,6 @@
"@repo/rdx-ui": "workspace:*", "@repo/rdx-ui": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*", "@repo/shadcn-ui": "workspace:*",
"ag-grid-community": "^33.3.0",
"ag-grid-react": "^33.3.0", "ag-grid-react": "^33.3.0",
"lucide-react": "^0.503.0", "lucide-react": "^0.503.0",
"react": "^19.1.0", "react": "^19.1.0",

View File

@ -1,16 +1,15 @@
import { AG_GRID_LOCALE_ES } from "@ag-grid-community/locale"; import { AG_GRID_LOCALE_ES } from "@ag-grid-community/locale";
import type { ValueFormatterParams } from "ag-grid-community"; import type { ValueFormatterParams } from "ag-grid-community";
import { import {
AllCommunityModule,
ColDef, ColDef,
GridOptions, GridOptions,
ModuleRegistry,
SizeColumnsToContentStrategy, SizeColumnsToContentStrategy,
SizeColumnsToFitGridStrategy, SizeColumnsToFitGridStrategy,
SizeColumnsToFitProvidedWidthStrategy, SizeColumnsToFitProvidedWidthStrategy,
} from "ag-grid-community"; } from "ag-grid-community";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { ErrorOverlay } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { AgGridReact } from "ag-grid-react"; import { AgGridReact } from "ag-grid-react";
import { ChevronRightIcon } from "lucide-react"; import { ChevronRightIcon } from "lucide-react";
@ -19,8 +18,6 @@ import { useCustomersQuery } from "../hooks";
import { useTranslation } from "../i18n"; import { useTranslation } from "../i18n";
import { CustomerStatusBadge } from "./customer-status-badge"; import { CustomerStatusBadge } from "./customer-status-badge";
ModuleRegistry.registerModules([AllCommunityModule]);
// Create new GridExample component // Create new GridExample component
export const CustomersListGrid = () => { export const CustomersListGrid = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -124,6 +121,19 @@ export const CustomersListGrid = () => {
[autoSizeStrategy, colDefs] [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. // Container: Defines the grid's theme & dimensions.
return ( return (
<div <div

View File

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

View File

@ -21,11 +21,11 @@ export const CustomersList = () => {
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
<Button onClick={() => navigate("/customers/create")} className='cursor-pointer'> <Button onClick={() => navigate("/customers/create")} className='cursor-pointer'>
<PlusIcon className='w-4 h-4 mr-2' /> <PlusIcon className='w-4 h-4 mr-2' />
{t("pages.list.title")} {t("pages.create.title")}
</Button> </Button>
</div> </div>
</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 /> <CustomersListGrid />
</div> </div>
<Outlet /> <Outlet />

View File

@ -31,5 +31,5 @@
"engines": { "engines": {
"node": ">=18" "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)) 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: ts-jest:
specifier: ^29.2.5 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: tsconfig-paths:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0 version: 4.2.0
@ -395,6 +395,9 @@ importers:
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.75.4 specifier: ^5.75.4
version: 5.81.2(react@19.1.0) version: 5.81.2(react@19.1.0)
ag-grid-community:
specifier: ^33.3.0
version: 33.3.2
axios: axios:
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.10.0 version: 1.10.0
@ -572,8 +575,6 @@ importers:
specifier: ^5.8.3 specifier: ^5.8.3
version: 5.8.3 version: 5.8.3
modules/customer-payments: {}
modules/customers: modules/customers:
dependencies: dependencies:
'@ag-grid-community/locale': '@ag-grid-community/locale':
@ -705,40 +706,6 @@ importers:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.23 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: modules/verifactu:
dependencies: dependencies:
'@erp/auth': '@erp/auth':
@ -12871,7 +12838,7 @@ snapshots:
ts-interface-checker@0.1.13: {} 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: dependencies:
bs-logger: 0.2.6 bs-logger: 0.2.6
ejs: 3.1.10 ejs: 3.1.10
@ -12889,6 +12856,7 @@ snapshots:
'@jest/transform': 29.7.0 '@jest/transform': 29.7.0
'@jest/types': 29.6.3 '@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.27.4) babel-jest: 29.7.0(@babel/core@7.27.4)
esbuild: 0.25.5
jest-util: 29.7.0 jest-util: 29.7.0
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3): ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):