Facturas de cliente

This commit is contained in:
David Arranz 2025-07-02 10:57:09 +02:00
parent b4c330458d
commit 81fffc4a0e
17 changed files with 130 additions and 45 deletions

View File

@ -4,6 +4,7 @@ import dotenv from "dotenv";
import express, { Application } from "express"; import express, { Application } from "express";
import helmet from "helmet"; import helmet from "helmet";
import responseTime from "response-time"; import responseTime from "response-time";
import { ENV } from "./config";
import { logger } from "./lib/logger"; import { logger } from "./lib/logger";
dotenv.config(); dotenv.config();
@ -25,7 +26,7 @@ export function createApp(): Application {
// enable CORS - Cross Origin Resource Sharing // enable CORS - Cross Origin Resource Sharing
app.use( app.use(
cors({ cors({
origin: process.env.FRONTEND_URL || "http://localhost:5173", origin: ENV.NODE_ENV === "development" ? "*" : process.env.FRONTEND_URL,
methods: "GET,POST,PUT,DELETE,OPTIONS", methods: "GET,POST,PUT,DELETE,OPTIONS",
credentials: true, credentials: true,

View File

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --clearScreen false", "dev": "vite --host --clearScreen false",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"clean": "rm -rf dist && rm -rf node_modules && rm -rf .turbo", "clean": "rm -rf dist && rm -rf node_modules && rm -rf .turbo",
@ -19,6 +19,7 @@
"@tailwindcss/postcss": "^4.1.5", "@tailwindcss/postcss": "^4.1.5",
"@tailwindcss/vite": "^4.1.6", "@tailwindcss/vite": "^4.1.6",
"@tanstack/react-query-devtools": "^5.74.11", "@tanstack/react-query-devtools": "^5.74.11",
"@types/dinero.js": "^1.9.4",
"@types/node": "^22.15.12", "@types/node": "^22.15.12",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3", "@types/react-dom": "^19.1.3",
@ -37,6 +38,7 @@
"@repo/shadcn-ui": "workspace:*", "@repo/shadcn-ui": "workspace:*",
"@tanstack/react-query": "^5.74.11", "@tanstack/react-query": "^5.74.11",
"axios": "^1.9.0", "axios": "^1.9.0",
"dinero.js": "^1.9.1",
"i18next": "^25.0.2", "i18next": "^25.0.2",
"i18next-browser-languagedetector": "^8.1.0", "i18next-browser-languagedetector": "^8.1.0",
"react": "^19.1.0", "react": "^19.1.0",

View File

@ -9,11 +9,14 @@ import { i18n } from "@/locales";
import { AuthProvider, createAuthService } from "@erp/auth/client"; import { AuthProvider, createAuthService } from "@erp/auth/client";
import { DataSourceProvider, createAxiosDataSource, createAxiosInstance } from "@erp/core/client"; import { DataSourceProvider, createAxiosDataSource, createAxiosInstance } from "@erp/core/client";
import DineroFactory from "dinero.js";
import "./app.css"; import "./app.css";
import { clearAccessToken, getAccessToken, setAccessToken } from "./lib"; import { clearAccessToken, getAccessToken, setAccessToken } from "./lib";
import { AppRoutes } from "./routes"; import { AppRoutes } from "./routes";
export const App = () => { export const App = () => {
DineroFactory.globalLocale = "es-ES";
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {

View File

@ -6,10 +6,14 @@
"./api": "./src/api/index.ts", "./api": "./src/api/index.ts",
"./client": "./src/web/index.ts" "./client": "./src/web/index.ts"
}, },
"peerDependencies": {
"dinero.js": "^1.9.1"
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@types/axios": "^0.14.4", "@types/axios": "^0.14.4",
"@types/dinero.js": "^1.9.4",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",

View File

@ -0,0 +1 @@
export * from "./module-metadata.d";

View File

@ -0,0 +1,3 @@
export const formatDate = (value: string) => {
return new Date(value).toLocaleDateString();
};

View File

@ -0,0 +1,2 @@
export * from "./date-func";
export * from "./money-funcs";

View File

@ -0,0 +1,12 @@
import DineroFactory, { Currency } from "dinero.js";
import { MoneyDTO } from "../../../common";
export const formatMoney = (value: MoneyDTO) => {
const money = DineroFactory({
amount: Number(value.amount),
currency: value.currency_code as Currency,
precision: Number(value.scale),
});
return money.toFormat();
};

View File

@ -1,2 +1,3 @@
export * from "./data-source"; export * from "./data-source";
export * from "./helpers";
export * from "./modules"; export * from "./modules";

View File

@ -8,8 +8,12 @@
"./api": "./src/api/index.ts", "./api": "./src/api/index.ts",
"./client": "./src/web/manifest.ts" "./client": "./src/web/manifest.ts"
}, },
"peerDependencies": {
"dinero.js": "^1.9.1"
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@types/dinero.js": "^1.9.4",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3", "@types/react-dom": "^19.1.3",
@ -17,6 +21,7 @@
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"dependencies": { "dependencies": {
"@ag-grid-community/locale": "34.0.0",
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@repo/rdx-criteria": "workspace:*", "@repo/rdx-criteria": "workspace:*",

View File

@ -10,17 +10,19 @@ export enum INVOICE_STATUS {
DRAFT = "draft", DRAFT = "draft",
EMITTED = "emitted", EMITTED = "emitted",
SENT = "sent", SENT = "sent",
RECEIVED = "received",
REJECTED = "rejected", REJECTED = "rejected",
} }
export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> { export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> {
private static readonly ALLOWED_STATUSES = ["draft", "emitted", "sent", "rejected"]; private static readonly ALLOWED_STATUSES = ["draft", "emitted", "sent", "received", "rejected"];
private static readonly FIELD = "invoiceStatus"; private static readonly FIELD = "invoiceStatus";
private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS"; private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS";
private static readonly TRANSITIONS: Record<string, string[]> = { private static readonly TRANSITIONS: Record<string, string[]> = {
draft: [INVOICE_STATUS.EMITTED], draft: [INVOICE_STATUS.EMITTED],
emitted: [INVOICE_STATUS.SENT, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT], emitted: [INVOICE_STATUS.SENT, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT],
sent: [INVOICE_STATUS.REJECTED], sent: [INVOICE_STATUS.RECEIVED, INVOICE_STATUS.REJECTED],
received: [],
rejected: [], rejected: [],
}; };
@ -43,7 +45,9 @@ export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusPro
? CustomerInvoiceStatus.createSent() ? CustomerInvoiceStatus.createSent()
: value === "emitted" : value === "emitted"
? CustomerInvoiceStatus.createSent() ? CustomerInvoiceStatus.createSent()
: CustomerInvoiceStatus.createDraft() : value === ""
? CustomerInvoiceStatus.createReceived()
: CustomerInvoiceStatus.createDraft()
); );
} }
@ -59,6 +63,10 @@ export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusPro
return new CustomerInvoiceStatus({ value: INVOICE_STATUS.SENT }); return new CustomerInvoiceStatus({ value: INVOICE_STATUS.SENT });
} }
public static createReceived(): CustomerInvoiceStatus {
return new CustomerInvoiceStatus({ value: INVOICE_STATUS.RECEIVED });
}
public static createRejected(): CustomerInvoiceStatus { public static createRejected(): CustomerInvoiceStatus {
return new CustomerInvoiceStatus({ value: INVOICE_STATUS.REJECTED }); return new CustomerInvoiceStatus({ value: INVOICE_STATUS.REJECTED });
} }

View File

@ -17,6 +17,7 @@ export const ListCustomerInvoicesResultSchema = createListViewSchema(
scale: z.number(), scale: z.number(),
currency_code: z.string(), currency_code: z.string(),
}), }),
total_price: z.object({ total_price: z.object({
amount: z.number(), amount: z.number(),
scale: z.number(), scale: z.number(),

View File

@ -1,12 +1,14 @@
// React Grid Logic import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
// Theme import { AG_GRID_LOCALE_ES } from "@ag-grid-community/locale";
import type { ColDef, ValueFormatterParams } from "ag-grid-community"; // Grid
import type { ColDef, GridOptions, ValueFormatterParams } from "ag-grid-community";
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community"; import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
ModuleRegistry.registerModules([AllCommunityModule]); ModuleRegistry.registerModules([AllCommunityModule]);
import { MoneyDTO } from "@erp/core";
import { formatDate, formatMoney } from "@erp/core/client";
// Core CSS // Core CSS
import { AgGridReact } from "ag-grid-react"; import { AgGridReact } from "ag-grid-react";
import { useCustomerInvoicesQuery } from "../hooks"; import { useCustomerInvoicesQuery } from "../hooks";
@ -54,38 +56,62 @@ export const CustomerInvoicesGrid = () => {
// Column Definitions: Defines & controls grid columns. // Column Definitions: Defines & controls grid columns.
const [colDefs] = useState<ColDef[]>([ const [colDefs] = useState<ColDef[]>([
{ field: "invoice_number" }, { field: "invoice_number", headerName: "Num. factura" },
{ field: "invoice_series" }, { field: "invoice_series", headerName: "Serie" },
{ {
field: "status", field: "invoice_status",
filter: true,
headerName: "Estado",
}, },
{ field: "issue_date" },
{ field: "operation_date" },
{ {
field: "subtotal", field: "issue_date",
headerName: "Fecha fact.",
valueFormatter: (params: ValueFormatterParams) => { valueFormatter: (params: ValueFormatterParams) => {
return "0 €"; return formatDate(params.value);
//return `£${params.value.toLocaleString()}`;
}, },
}, },
{ {
field: "total", field: "subtotal_price",
valueFormatter: (params: ValueFormatterParams) => { valueFormatter: (params: ValueFormatterParams) => {
return "0 €"; const rawValue: MoneyDTO = params.value;
//return `£${params.value.toLocaleString()}`; return formatMoney(rawValue);
},
},
{
field: "total_price",
valueFormatter: (params: ValueFormatterParams) => {
const rawValue: MoneyDTO = params.value;
return formatMoney(rawValue);
}, },
}, },
]); ]);
// Apply settings across all columns const gridOptions: GridOptions = {
const defaultColDef = useMemo<ColDef>(() => { columnDefs: colDefs,
return { defaultColDef: {
filter: true, editable: true,
flex: 1,
minWidth: 100,
filter: false,
sortable: false, sortable: false,
resizable: false, resizable: true,
}; },
}, []); sideBar: true,
statusBar: {
statusPanels: [
{ statusPanel: "agTotalAndFilteredRowCountComponent", align: "left" },
{ statusPanel: "agAggregationComponent" },
],
},
rowGroupPanelShow: "always",
pagination: true,
paginationPageSize: 10,
paginationPageSizeSelector: [10, 20, 30, 50],
enableCharts: true,
localeText: AG_GRID_LOCALE_ES,
rowSelection: { mode: "multiRow" },
};
// Container: Defines the grid's theme & dimensions. // Container: Defines the grid's theme & dimensions.
return ( return (
@ -96,13 +122,7 @@ export const CustomerInvoicesGrid = () => {
width: "100%", width: "100%",
}} }}
> >
<AgGridReact <AgGridReact rowData={data?.items ?? []} loading={isLoading || isPending} {...gridOptions} />
rowData={data?.items ?? []}
loading={isLoading || isPending}
columnDefs={colDefs}
defaultColDef={defaultColDef}
pagination={true}
/>
</div> </div>
); );
}; };

View File

@ -1,25 +1,21 @@
import { IListResponseDTO } from "@erp/core";
import { useDataSource, useQueryKey } from "@erp/core/client"; import { useDataSource, useQueryKey } from "@erp/core/client";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IListCustomerInvoicesResponseDTO } from "../../common/dto"; import { ListCustomerInvoicesResultDTO } from "../../common/dto";
// Obtener todas las facturas // Obtener todas las facturas
export const useCustomerInvoicesQuery = (params: any) => { export const useCustomerInvoicesQuery = (params: any) => {
const dataSource = useDataSource(); const dataSource = useDataSource();
const keys = useQueryKey(); const keys = useQueryKey();
return useQuery({ return useQuery<ListCustomerInvoicesResultDTO>({
queryKey: keys().data().resource("customer-invoices").action("list").params(params).get(), queryKey: keys().data().resource("customer-invoices").action("list").params(params).get(),
queryFn: (context) => { queryFn: (context) => {
console.log(dataSource.getBaseUrl()); console.log(dataSource.getBaseUrl());
const { signal } = context; const { signal } = context;
return dataSource.getList<IListResponseDTO<IListCustomerInvoicesResponseDTO>>( return dataSource.getList<ListCustomerInvoicesResultDTO>("customer-invoices", {
"customer-invoices", signal,
{ ...params,
signal, });
...params,
}
);
}, },
}); });
}; };

View File

@ -3,6 +3,6 @@
"compilerOptions": { "compilerOptions": {
"composite": true "composite": true
}, },
"include": ["src"], "include": ["src", "../../modules/core/src/web/lib/helpers/money-funcs.ts"],
"exclude": ["src/**/__tests__/*"] "exclude": ["src/**/__tests__/*"]
} }

View File

@ -213,6 +213,9 @@ importers:
axios: axios:
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.10.0 version: 1.10.0
dinero.js:
specifier: ^1.9.1
version: 1.9.1
i18next: i18next:
specifier: ^25.0.2 specifier: ^25.0.2
version: 25.2.1(typescript@5.8.3) version: 25.2.1(typescript@5.8.3)
@ -274,6 +277,9 @@ importers:
'@tanstack/react-query-devtools': '@tanstack/react-query-devtools':
specifier: ^5.74.11 specifier: ^5.74.11
version: 5.81.2(@tanstack/react-query@5.81.2(react@19.1.0))(react@19.1.0) version: 5.81.2(@tanstack/react-query@5.81.2(react@19.1.0))(react@19.1.0)
'@types/dinero.js':
specifier: ^1.9.4
version: 1.9.4
'@types/node': '@types/node':
specifier: ^22.15.12 specifier: ^22.15.12
version: 22.15.32 version: 22.15.32
@ -365,6 +371,9 @@ importers:
axios: axios:
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.10.0 version: 1.10.0
dinero.js:
specifier: ^1.9.1
version: 1.9.1
http-status: http-status:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
@ -393,6 +402,9 @@ importers:
'@types/axios': '@types/axios':
specifier: ^0.14.4 specifier: ^0.14.4
version: 0.14.4 version: 0.14.4
'@types/dinero.js':
specifier: ^1.9.4
version: 1.9.4
'@types/express': '@types/express':
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.23 version: 4.17.23
@ -417,6 +429,9 @@ importers:
modules/customer-invoices: modules/customer-invoices:
dependencies: dependencies:
'@ag-grid-community/locale':
specifier: 34.0.0
version: 34.0.0
'@erp/core': '@erp/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
@ -450,6 +465,9 @@ importers:
date-fns: date-fns:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
dinero.js:
specifier: ^1.9.1
version: 1.9.1
express: express:
specifier: ^4.18.2 specifier: ^4.18.2
version: 4.21.2 version: 4.21.2
@ -487,6 +505,9 @@ importers:
'@biomejs/biome': '@biomejs/biome':
specifier: 1.9.4 specifier: 1.9.4
version: 1.9.4 version: 1.9.4
'@types/dinero.js':
specifier: ^1.9.4
version: 1.9.4
'@types/express': '@types/express':
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.23 version: 4.17.23
@ -860,6 +881,9 @@ packages:
'@adobe/css-tools@4.3.3': '@adobe/css-tools@4.3.3':
resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==}
'@ag-grid-community/locale@34.0.0':
resolution: {integrity: sha512-F3Bs6wpT4CCOgN5wmzhimQz6GK5VUznsl8+SZXv0A7bA5aw8qLuVc/yEIpqNRykQwwt0vytcHhZzVDDMuZSy1g==}
'@alloc/quick-lru@5.2.0': '@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -6245,6 +6269,8 @@ snapshots:
'@adobe/css-tools@4.3.3': {} '@adobe/css-tools@4.3.3': {}
'@ag-grid-community/locale@34.0.0': {}
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@ampproject/remapping@2.3.0': '@ampproject/remapping@2.3.0':