From 220cc4c3a4f665c5fca6df5bfa6ecd7126e13079 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 24 Nov 2025 14:05:10 +0100 Subject: [PATCH] Facturas de cliente --- apps/server/src/app.ts | 39 ++++---- apps/server/src/index.ts | 2 +- .../src/common/locales/en.json | 49 ++++++++-- .../src/common/locales/es.json | 49 ++++++++-- .../hooks/use-issued-invoice-list-query.ts | 1 + .../use-issued-invoices-grid-columns.tsx | 10 +++ .../list/ui/components/index.ts | 1 + .../ui/components/verifactu-status-badge.tsx | 46 ++++++++++ .../ui/pages/issued-invoice-list-page.tsx | 31 ++++--- .../src/web/issued-invoices/types/index.ts | 1 + .../types/verifactu-record-status.ts | 89 +++++++++++++++++++ .../use-proforma-grid-columns.tsx | 5 +- .../templates/acana/issued-invoice.hbs | 62 +++++++------ .../templates/acana/tailwind.min.css | 1 + modules/customers/src/web/customer-routes.tsx | 8 +- .../web/pages/list/customers-list-grid.tsx | 47 +++++----- .../web/pages/list/customers-list-page.tsx | 22 ++--- .../pages/list/use-customers-list-columns.tsx | 66 +++++++------- 18 files changed, 381 insertions(+), 148 deletions(-) create mode 100644 modules/customer-invoices/src/web/issued-invoices/list/ui/components/index.ts create mode 100644 modules/customer-invoices/src/web/issued-invoices/list/ui/components/verifactu-status-badge.tsx create mode 100644 modules/customer-invoices/src/web/issued-invoices/types/verifactu-record-status.ts create mode 100644 modules/customer-invoices/templates/acana/tailwind.min.css diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index fe01b53a..03c1c74c 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,7 +1,8 @@ -import cors, { CorsOptions } from "cors"; -import express, { Application } from "express"; +import cors, { type CorsOptions } from "cors"; +import express, { type Application } from "express"; import helmet from "helmet"; import responseTime from "response-time"; + // ❗️ No cargamos dotenv aquí. Debe hacerse en el entrypoint o en ./config. // dotenv.config(); import { ENV } from "./config"; @@ -9,22 +10,6 @@ import { logger } from "./lib/logger"; export function createApp(): Application { const app = express(); - app.set("port", process.env.SERVER_PORT ?? 3002); - - // Oculta la cabecera x-powered-by - app.disable("x-powered-by"); - - // Desactiva ETag correctamente a nivel de Express - app.set("etag", false); - - // ─────────────────────────────────────────────────────────────────────────── - // Parsers - app.use(express.json()); - app.use(express.text()); - app.use(express.urlencoded({ extended: true })); - - // Métrica de tiempo de respuesta - app.use(responseTime()); // ─────────────────────────────────────────────────────────────────────────── // CORS @@ -60,6 +45,24 @@ export function createApp(): Application { }; app.use(cors(ENV.NODE_ENV === "development" ? devCors : prodCors)); + app.options("*", cors(ENV.NODE_ENV === "development" ? devCors : prodCors)); + + app.set("port", process.env.SERVER_PORT ?? 3002); + + // Oculta la cabecera x-powered-by + app.disable("x-powered-by"); + + // Desactiva ETag correctamente a nivel de Express + app.set("etag", false); + + // ─────────────────────────────────────────────────────────────────────────── + // Parsers + app.use(express.json()); + app.use(express.text()); + app.use(express.urlencoded({ extended: true })); + + // Métrica de tiempo de respuesta + app.use(responseTime()); // ─────────────────────────────────────────────────────────────────────────── // Seguridad HTTP diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 82bbcd73..b7c0aba6 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -248,7 +248,7 @@ process.on("uncaughtException", async (error: Error) => { logger.info("✅ Server is READY (readiness=true)"); logger.info(`startup_duration_ms=${DateTime.now().diff(currentState.launchedAt).toMillis()}`); - server.listen(currentState.port, () => { + server.listen(currentState.port, "0.0.0.0", () => { server.emit("listening"); const networkInterfaces = os.networkInterfaces(); diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index 773ca2d0..04a667f2 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -52,15 +52,46 @@ }, "issued_invoices": { "status": { - "all": "Todos", - "pendiente": "Pendiente", - "aceptado_con_error": "Aceptado con error", - "incorrecto": "Incorrecto", - "duplicado": "Duplicado", - "anulado": "Anulado", - "factura_inexistente": "Factura inexistente", - "rechazado": "Rechazado", - "error": "Error" + "all": { + "label": "All", + "description": "Proforma in preparation" + }, + "pendiente": { + "label": "Pending", + "description": "Queued record not yet processed" + }, + "correcto": { + "label": "Correct", + "description": "Record successfully processed by the AEAT" + }, + "aceptado_con_error": { + "label": "Accepted with errors", + "description": "Record accepted with errors by the AEAT. A correction record or a rectifying invoice is required" + }, + "incorrecto": { + "label": "Incorrect", + "description": "Record considered incorrect by the AEAT. A correction record with rechazo_previo=S or rechazo_previo=X, or a rectifying invoice, is required" + }, + "duplicado": { + "label": "Duplicate", + "description": "Record not accepted by the AEAT because another record with the same (series, number, issue_date) already exists" + }, + "anulado": { + "label": "Cancelled", + "description": "Cancellation record successfully processed by the AEAT" + }, + "factura_inexistente": { + "label": "Non-existent invoice", + "description": "Cancellation record not accepted by the AEAT because the invoice does not exist" + }, + "rechazado": { + "label": "Not registered", + "description": "Record rejected by the AEAT" + }, + "error": { + "label": "Error", + "description": "AEAT server error. The invoice record will be retried" + } } } }, diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index 74957266..c9c12770 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -52,15 +52,46 @@ }, "issued_invoices": { "status": { - "all": "All", - "pendiente": "Pendiente", - "aceptado_con_error": "Aceptado con error", - "incorrecto": "Incorrecto", - "duplicado": "Duplicado", - "anulado": "Anulado", - "factura_inexistente": "Factura inexistente", - "rechazado": "Rechazado", - "error": "Error" + "all": { + "label": "All", + "description": "Proforma en preparación" + }, + "pendiente": { + "label": "Pendiente", + "description": "Registro encolado y no procesado aún" + }, + "correcto": { + "label": "Correcto", + "description": "Registro procesado correctamente por la AEAT" + }, + "aceptado_con_error": { + "label": "Aceptado con errores", + "description": "Registro aceptado con errores por la AEAT. Se requiere enviar un registro de subsanación o emitir una rectificativa" + }, + "incorrecto": { + "label": "Incorrecto", + "description": "Registro considerado incorrecto por la AEAT. Se requiere enviar un registro de subsanación con rechazo_previo=S o rechazo_previo=X o emitir una rectificativa" + }, + "duplicado": { + "label": "Duplicado", + "description": "Registro no aceptado por la AEAT por existir un registro con el mismo (serie, numero, fecha_expedicion)" + }, + "anulado": { + "label": "Anulado", + "description": "Registro de anulación procesado correctamente por la AEAT" + }, + "factura_inexistente": { + "label": "Factura inexistente", + "description": "Registro de anulación no aceptado por la AEAT por no existir la factura" + }, + "rechazado": { + "label": "No registrado", + "description": "Registro rechazado por la AEAT" + }, + "error": { + "label": "Error", + "description": "Error en el servidor de la AEAT. Se intentará reenviar el registro de facturación de nuevo" + } } } }, diff --git a/modules/customer-invoices/src/web/issued-invoices/list/hooks/use-issued-invoice-list-query.ts b/modules/customer-invoices/src/web/issued-invoices/list/hooks/use-issued-invoice-list-query.ts index a7ec9c2f..1e3f34f0 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/hooks/use-issued-invoice-list-query.ts +++ b/modules/customer-invoices/src/web/issued-invoices/list/hooks/use-issued-invoice-list-query.ts @@ -33,6 +33,7 @@ export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesQueryOptions) queryKey: ISSUED_INVOICES_QUERY_KEY(criteria), queryFn: async ({ signal }) => getIssuedInvoiceListApi(dataSource, signal, criteria), enabled, + staleTime: 5000, placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`) }); }; diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx index ebe15950..c6f356e6 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns.tsx @@ -19,6 +19,7 @@ import QrCode from "react-qr-code"; import { useTranslation } from "../../../../../i18n"; import type { IssuedInvoiceSummaryData } from "../../../../types"; +import { VerifactuStatusBadge } from "../../components"; type GridActionHandlers = { onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void; @@ -68,6 +69,15 @@ export function useIssuedInvoicesGridColumns( /> ), accessorFn: (row) => row.verifactu.status, // para ordenar/buscar por nombre + cell: ({ row }) => { + const invoice = row.original; + + return ( +
+ +
+ ); + }, enableHiding: false, enableSorting: false, size: 140, diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/components/index.ts b/modules/customer-invoices/src/web/issued-invoices/list/ui/components/index.ts new file mode 100644 index 00000000..2cf1fb81 --- /dev/null +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/components/index.ts @@ -0,0 +1 @@ +export * from "./verifactu-status-badge"; diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/components/verifactu-status-badge.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/components/verifactu-status-badge.tsx new file mode 100644 index 00000000..8767926c --- /dev/null +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/components/verifactu-status-badge.tsx @@ -0,0 +1,46 @@ +import { Badge, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; + +import { useTranslation } from "../../../../i18n"; +import { + type VerifactuRecordStatus, + getVerifactuRecordStatusButtonVariant, + getVerifactuRecordStatusColor, + getVerifactuRecordStatusIcon, +} from "../../../types"; + +export type VerifactuStatusBadgeProps = { + status: string | VerifactuRecordStatus; // permitir cualquier valor + className?: string; +}; + +export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadgeProps) => { + const { t } = useTranslation(); + const normalizedStatus = status as VerifactuRecordStatus; + const Icon = getVerifactuRecordStatusIcon(normalizedStatus); + + return ( + + + + + {t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, { + defaultValue: status, + })} + + + +

{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.description`)}

+
+
+ ); +}; + +VerifactuStatusBadge.displayName = "VerifactuStatusBadge"; diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/issued-invoice-list-page.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/issued-invoice-list-page.tsx index 4db32e05..240d58b7 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/issued-invoice-list-page.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/issued-invoice-list-page.tsx @@ -96,19 +96,30 @@ export const IssuedInvoiceListPage = () => { - {t("catalog.issuedInvoices.status.all.label")} - - {t("catalog.issuedInvoices.status.draft.label")} + {t("catalog.issued_invoices.status.all.label")} + + {t("catalog.issued_invoices.status.correcto.label")} - {t("catalog.issuedInvoices.status.sent.label")} - - {t("catalog.issuedInvoices.status.approved.label")} + + {t("catalog.issued_invoices.status.pendiente.label")} - - {t("catalog.issuedInvoices.status.rejected.label")} + + {t("catalog.issued_invoices.status.aceptado_con_error.label")} - - {t("catalog.issuedInvoices.status.issued.label")} + + {t("catalog.issued_invoices.status.incorrecto.label")} + + + {t("catalog.issued_invoices.status.duplicado.label")} + + + {t("catalog.issued_invoices.status.anulado.label")} + + + {t("catalog.issued_invoices.status.duplicado.label")} + + + {t("catalog.issued_invoices.status.rechazado.label")} diff --git a/modules/customer-invoices/src/web/issued-invoices/types/index.ts b/modules/customer-invoices/src/web/issued-invoices/types/index.ts index 03bc2fab..00ede27f 100644 --- a/modules/customer-invoices/src/web/issued-invoices/types/index.ts +++ b/modules/customer-invoices/src/web/issued-invoices/types/index.ts @@ -1,2 +1,3 @@ export * from "./issued-invoice.api.schema"; export * from "./issued-invoice-summary.web.schema"; +export * from "./verifactu-record-status"; diff --git a/modules/customer-invoices/src/web/issued-invoices/types/verifactu-record-status.ts b/modules/customer-invoices/src/web/issued-invoices/types/verifactu-record-status.ts new file mode 100644 index 00000000..2f3b6d3e --- /dev/null +++ b/modules/customer-invoices/src/web/issued-invoices/types/verifactu-record-status.ts @@ -0,0 +1,89 @@ +import { + BugIcon, + CheckCheckIcon, + CheckIcon, + CopyIcon, + FileQuestionIcon, + HourglassIcon, + StopCircleIcon, +} from "lucide-react"; + +export enum VERIFACTU_RECORD_STATUS { + PENDIENTE = "Pendiente", // <- Registro encolado y no procesado aún + CORRECTO = "Correcto", // <- Registro procesado correctamente por la AEAT + ACEPTADO_CON_ERROR = "Aceptado con errores", // <- Registro aceptado con errores por la AEAT. Se requiere enviar un registro de subsanación o emitir una rectificativa + INCORRECTO = "Incorrecto", // <- Registro considerado incorrecto por la AEAT. Se requiere enviar un registro de subsanación con rechazo_previo=S o rechazo_previo=X o emitir una rectificativa + DUPLICADO = "Duplicado", // <- Registro no aceptado por la AEAT por existir un registro con el mismo (serie, numero, fecha_expedicion) + ANULADO = "Anulado", // <- Registro de anulación procesado correctamente por la AEAT + FACTURA_INEXISTENTE = "Factura inexistente", // <- Registro de anulación no aceptado por la AEAT por no existir la factura. + RECHAZADO = "No registrado", // <- Registro rechazado por la AEAT + ERROR = "Error servidor AEAT", // <- Error en el servidor de la AEAT. Se intentará reenviar el registro de facturación de nuevo +} + +export type VerifactuRecordStatus = `${VERIFACTU_RECORD_STATUS}`; + +export const getVerifactuRecordStatusButtonVariant = ( + status: VerifactuRecordStatus +): "default" | "secondary" | "outline" | "destructive" => { + switch (status) { + case "Pendiente": + return "outline"; + case "Aceptado con errores": + case "Duplicado": + return "secondary"; + case "Anulado": + return "default"; + case "Incorrecto": + case "Error servidor AEAT": + case "Factura inexistente": + case "No registrado": + return "destructive"; + case "Correcto": + return "default"; + default: + return "outline"; + } +}; + +export const getVerifactuRecordStatusColor = (status: VerifactuRecordStatus): string => { + switch (status) { + case "Pendiente": + return "bg-gray-100 text-gray-700 hover:bg-gray-100"; + case "Aceptado con errores": + case "Duplicado": + return "bg-yellow-100 text-yellow-700 hover:bg-yellow-100"; + case "Anulado": + return "bg-green-100 text-green-700 hover:bg-green-100"; + case "Incorrecto": + case "Error servidor AEAT": + case "Factura inexistente": + case "No registrado": + return "bg-red-100 text-red-700 hover:bg-red-100"; + case "Correcto": + return "bg-blue-100 text-blue-700 hover:bg-blue-100"; + default: + return "bg-gray-100 text-gray-700 hover:bg-gray-100"; + } +}; + +export const getVerifactuRecordStatusIcon = (status: VerifactuRecordStatus) => { + switch (status) { + case "Pendiente": + return HourglassIcon; + case "Aceptado con errores": + return CheckIcon; + case "Duplicado": + return CopyIcon; + case "Anulado": + return StopCircleIcon; + case "Incorrecto": + case "Error servidor AEAT": + case "Factura inexistente": + case "No registrado": + return BugIcon; + case "Correcto": + return CheckCheckIcon; + default: + return FileQuestionIcon; + } +}; diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx index 60a879fd..e8e2e396 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx @@ -1,6 +1,5 @@ import { Button, - Checkbox, Tooltip, TooltipContent, TooltipProvider, @@ -39,7 +38,7 @@ export function useProformasGridColumns( return React.useMemo[]>( () => [ - { + /*{ id: "select", header: ({ table }) => ( { diff --git a/modules/customer-invoices/templates/acana/issued-invoice.hbs b/modules/customer-invoices/templates/acana/issued-invoice.hbs index 73591ded..95ab9d3c 100644 --- a/modules/customer-invoices/templates/acana/issued-invoice.hbs +++ b/modules/customer-invoices/templates/acana/issued-invoice.hbs @@ -3,14 +3,11 @@ - Factura + +