From 451a1013d04fd238914eb262cc8d24aa839f207a Mon Sep 17 00:00:00 2001 From: david Date: Thu, 30 Jul 2026 19:20:00 +0200 Subject: [PATCH] feat: add verifactu status management and enhance issued invoices UI components --- .../issued-invoice-v2.repository.ts | 1 + .../repositories/issued-invoice.repository.ts | 5 + .../use-issued-invoice-list.controller.ts | 68 ++++++- .../list/ui/components/index.ts | 1 + .../components/issued-invoices-grid-empty.tsx | 22 ++- ...ssued-invoices-status-segmented-filter.tsx | 183 ++++++++++++++++++ .../ui/pages/list-issued-invoices-page.tsx | 115 ++++------- .../adapters/list-issued-invoice.adapter.ts | 2 + .../issued-invoices/shared/entities/index.ts | 1 + .../entities/issued-invoice-list.entity.ts | 2 + .../issued-invoice-status-counts.entity.ts | 25 +++ 11 files changed, 335 insertions(+), 90 deletions(-) create mode 100644 modules/customer-invoices/src/web/issued-invoices/list/ui/components/issued-invoices-status-segmented-filter.tsx create mode 100644 modules/customer-invoices/src/web/issued-invoices/shared/entities/issued-invoice-status-counts.entity.ts diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice-v2.repository.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice-v2.repository.ts index 79b7dc9b..8669587a 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice-v2.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice-v2.repository.ts @@ -182,6 +182,7 @@ export class SequelizeIssuedInvoiceRepositoryV2 reference: { type: "root", column: "reference" }, description: { type: "root", column: "description" }, recipient_name: { type: "root", column: "customer_name" }, + verifactu_status: { type: "association", association: "verifactu", column: "estado" }, }, sortableFields: ["recipient_name", "invoice_number", "invoice_date", "id", "created_at", "serie"], fullTextTableAlias: "IssuedInvoiceModel", diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts index cbae09a3..b7f1ea4d 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts @@ -292,6 +292,11 @@ export class IssuedInvoiceRepository type: "root", column: "recipient_name", }, + verifactu_status: { + type: "association", + association: "verifactu", + column: "estado", + }, }, sortableFields: [ "recipient_name", diff --git a/modules/customer-invoices/src/web/issued-invoices/list/controllers/use-issued-invoice-list.controller.ts b/modules/customer-invoices/src/web/issued-invoices/list/controllers/use-issued-invoice-list.controller.ts index 8c3a723a..3a2d6236 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/controllers/use-issued-invoice-list.controller.ts +++ b/modules/customer-invoices/src/web/issued-invoices/list/controllers/use-issued-invoice-list.controller.ts @@ -1,12 +1,18 @@ import { getUserErrorMessage } from "@erp/core/client"; -import { useDebounce } from "@erp/core/hooks"; +import { useDataSource, useDebounce } from "@erp/core/hooks"; +import { useQueries } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import type { IssuedInvoiceList, + IssuedInvoiceStatusCounts, ListIssuedInvoicesByCriteriaParams, VerifactuRecordStatus, } from "../../shared"; +import { + EMPTY_ISSUED_INVOICE_STATUS_COUNTS, + getListIssuedInvoicesByCriteria, +} from "../../shared"; import { useIssuedInvoiceListQuery } from "../../shared/"; type VerifactuRecordListStatusFilter = "all" | VerifactuRecordStatus; @@ -17,9 +23,26 @@ const EMPTY_ISSUED_INVOICES_LIST: IssuedInvoiceList = { perPage: 5, totalPages: 0, totalItems: 0, + statusCounts: EMPTY_ISSUED_INVOICE_STATUS_COUNTS, }; +const STATUS_COUNT_OPTIONS: ReadonlyArray<{ + value: VerifactuRecordStatus; + countKey: keyof Omit; +}> = [ + { value: "Pendiente", countKey: "pendiente" }, + { value: "Correcto", countKey: "correcto" }, + { value: "Aceptado con errores", countKey: "aceptadoConErrores" }, + { value: "Incorrecto", countKey: "incorrecto" }, + { value: "Duplicado", countKey: "duplicado" }, + { value: "Anulado", countKey: "anulado" }, + { value: "Factura inexistente", countKey: "facturaInexistente" }, + { value: "No registrado", countKey: "rechazado" }, + { value: "Error servidor AEAT", countKey: "errorServidorAeat" }, +]; + export const useIssuedInvoiceListController = () => { + const dataSource = useDataSource(); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(5); const [search, setSearch] = useState(""); @@ -38,14 +61,50 @@ export const useIssuedInvoiceListController = () => { filters: verifactuStatusFilter === "all" ? [] - : [{ field: "verifactu.status", operator: "EQUALS", value: verifactuStatusFilter }], + : [{ field: "verifactu_status", operator: "EQUALS", value: verifactuStatusFilter }], }), [debouncedSearch, pageIndex, pageSize, verifactuStatusFilter] ); const query = useIssuedInvoiceListQuery({ criteria }); + const statusCountQueries = useQueries({ + queries: STATUS_COUNT_OPTIONS.map((statusOption) => ({ + queryKey: ["issued_invoices", "status_count", debouncedSearch || "", statusOption.value], + queryFn: async ({ signal }: { signal: AbortSignal }) => { + const response = await getListIssuedInvoicesByCriteria(dataSource, { + signal, + criteria: { + q: debouncedSearch || "", + pageNumber: 0, + pageSize: 1, + filters: [ + { + field: "verifactu_status", + operator: "EQUALS", + value: statusOption.value, + }, + ], + }, + }); + + return response.total_items; + }, + })), + }); const errorMessage = query.error ? getUserErrorMessage(query.error) : null; + const statusCounts = useMemo(() => { + const counts = { + ...EMPTY_ISSUED_INVOICE_STATUS_COUNTS, + all: query.data?.totalItems ?? 0, + }; + + STATUS_COUNT_OPTIONS.forEach((statusOption, index) => { + counts[statusOption.countKey] = statusCountQueries[index]?.data ?? 0; + }); + + return counts; + }, [query.data?.totalItems, statusCountQueries]); const setStatusFilterValue = (value: string) => { const nextValue = (value || "all") as VerifactuRecordListStatusFilter; @@ -85,7 +144,10 @@ export const useIssuedInvoiceListController = () => { }; return { - data: query.data ?? EMPTY_ISSUED_INVOICES_LIST, + data: { + ...(query.data ?? EMPTY_ISSUED_INVOICES_LIST), + statusCounts, + }, isLoading: query.isLoading, isFetching: query.isFetching, 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 index d9f7dce1..96b02e87 100644 --- 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 @@ -1,2 +1,3 @@ export * from "./issued-invoices-grid-empty"; +export * from "./issued-invoices-status-segmented-filter"; export * from "./verifactu-status-badge"; diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/components/issued-invoices-grid-empty.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/components/issued-invoices-grid-empty.tsx index 8d6d9f66..b81af543 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/ui/components/issued-invoices-grid-empty.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/components/issued-invoices-grid-empty.tsx @@ -1,17 +1,19 @@ -import { Card, CardContent } from "@repo/shadcn-ui/components"; import { cn } from "@repo/shadcn-ui/lib/utils"; import { ChartNoAxesColumnIncreasingIcon } from "lucide-react"; export const IssuedInvoicesEmptyGrid = ({ className }: React.ComponentProps<"div">) => { return ( - - -
- -

No data to show

-

May take 15 minutes for data to load

-
-
-
+
+ +

Todavia no hay facturas emitidas para mostrar

+

+ Cuando emitas proformas aprobadas, apareceran aqui con su estado en Verifactu. +

+
); }; diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/components/issued-invoices-status-segmented-filter.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/components/issued-invoices-status-segmented-filter.tsx new file mode 100644 index 00000000..50d1f316 --- /dev/null +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/components/issued-invoices-status-segmented-filter.tsx @@ -0,0 +1,183 @@ +import { Badge, ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { + AlertTriangleIcon, + BanIcon, + BugIcon, + CheckCheckIcon, + CheckIcon, + Clock3Icon, + CopyIcon, + FileQuestionIcon, + XCircleIcon, +} from "lucide-react"; +import type { ElementType } from "react"; + +import { useTranslation } from "../../../../i18n"; +import type { IssuedInvoiceStatusCounts, VerifactuRecordStatus } from "../../../shared"; + +export type IssuedInvoiceStatusFilter = "all" | VerifactuRecordStatus; + +export type IssuedInvoicesStatusSegmentedFilterProps = { + value: IssuedInvoiceStatusFilter; + onValueChange: (value: IssuedInvoiceStatusFilter) => void; + counts: IssuedInvoiceStatusCounts; + disabled?: boolean; +}; + +const ISSUED_INVOICE_STATUS_FILTER_OPTIONS = [ + { + value: "all", + label: "Todas", + icon: null, + countKey: "all", + activeClassName: + "data-[state=on]:border-primary data-[state=on]:bg-primary data-[state=on]:text-primary-foreground", + }, + { + value: "Pendiente", + label: "Pendiente", + icon: Clock3Icon, + countKey: "pendiente", + activeClassName: + "data-[state=on]:border-slate-300 data-[state=on]:bg-slate-100 data-[state=on]:text-slate-700", + }, + { + value: "Correcto", + label: "Correcto", + icon: CheckCheckIcon, + countKey: "correcto", + activeClassName: + "data-[state=on]:border-blue-300 data-[state=on]:bg-blue-50 data-[state=on]:text-blue-700", + }, + { + value: "Aceptado con errores", + label: "Aceptado con errores", + icon: CheckIcon, + countKey: "aceptadoConErrores", + activeClassName: + "data-[state=on]:border-amber-300 data-[state=on]:bg-amber-50 data-[state=on]:text-amber-700", + }, + { + value: "Incorrecto", + label: "Incorrecto", + icon: XCircleIcon, + countKey: "incorrecto", + activeClassName: + "data-[state=on]:border-red-300 data-[state=on]:bg-red-50 data-[state=on]:text-red-700", + }, + { + value: "Duplicado", + label: "Duplicado", + icon: CopyIcon, + countKey: "duplicado", + activeClassName: + "data-[state=on]:border-yellow-300 data-[state=on]:bg-yellow-50 data-[state=on]:text-yellow-700", + }, + { + value: "Anulado", + label: "Anulado", + icon: BanIcon, + countKey: "anulado", + activeClassName: + "data-[state=on]:border-emerald-300 data-[state=on]:bg-emerald-50 data-[state=on]:text-emerald-700", + }, + { + value: "Factura inexistente", + label: "Factura inexistente", + icon: FileQuestionIcon, + countKey: "facturaInexistente", + activeClassName: + "data-[state=on]:border-rose-300 data-[state=on]:bg-rose-50 data-[state=on]:text-rose-700", + }, + { + value: "No registrado", + label: "No registrado", + icon: AlertTriangleIcon, + countKey: "rechazado", + activeClassName: + "data-[state=on]:border-orange-300 data-[state=on]:bg-orange-50 data-[state=on]:text-orange-700", + }, + { + value: "Error servidor AEAT", + label: "Error servidor AEAT", + icon: BugIcon, + countKey: "errorServidorAeat", + activeClassName: + "data-[state=on]:border-red-300 data-[state=on]:bg-red-50 data-[state=on]:text-red-700", + }, +] as const satisfies readonly { + value: IssuedInvoiceStatusFilter; + label: string; + icon: ElementType | null; + countKey: keyof IssuedInvoiceStatusCounts; + activeClassName: string; +}[]; + +const isIssuedInvoiceStatusFilter = (value: string): value is IssuedInvoiceStatusFilter => { + return ISSUED_INVOICE_STATUS_FILTER_OPTIONS.some((option) => option.value === value); +}; + +export const IssuedInvoicesStatusSegmentedFilter = ({ + value, + onValueChange, + counts, + disabled = false, +}: IssuedInvoicesStatusSegmentedFilterProps) => { + const { t } = useTranslation(); + + const handleValueChange = (nextValue: string) => { + if (!(nextValue && isIssuedInvoiceStatusFilter(nextValue)) || nextValue === value) { + return; + } + + onValueChange(nextValue); + }; + + return ( + + {ISSUED_INVOICE_STATUS_FILTER_OPTIONS.map((option) => ( + { + if (option.value === value) { + event.preventDefault(); + } + }} + value={option.value} + > + + {option.icon ? : null} + {t(`catalog.issued_invoices.status.${option.value.toLowerCase()}.label`, option.label)} + + {counts[option.countKey]} + + + + ))} + + ); +}; diff --git a/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx b/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx index 7383df13..a9bdc52d 100644 --- a/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx +++ b/modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx @@ -1,24 +1,17 @@ import { ErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components"; import { LogoVerifactu } from "@repo/rdx-ui/components"; -import { - Alert, - AlertDescription, - AlertTitle, - Button, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@repo/shadcn-ui/components"; -import { FilterIcon, PlusIcon } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components"; +import { PlusIcon } from "lucide-react"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "../../../../i18n"; import { useIssuedInvoiceListPageController } from "../../controllers"; import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks"; -import { IssuedInvoicesEmptyGrid } from "../components"; +import { + IssuedInvoicesEmptyGrid, + IssuedInvoicesStatusSegmentedFilter, +} from "../components"; export const ListIssuedInvoicesPage = () => { const { t } = useTranslation(); @@ -96,77 +89,45 @@ export const ListIssuedInvoicesPage = () => { - {/* Search and filters */} - {listCtrl.data.totalItems === 0 && } - - {listCtrl.data.totalItems !== 0 && ( - <> -
- navigate(`/issuedInvoices/${id}`)} - pageIndex={listCtrl.pageIndex} - pageSize={listCtrl.pageSize} - searching={isSearchPending} - topContent={ -
+
+ navigate(`/issuedInvoices/${id}`)} + pageIndex={listCtrl.pageIndex} + pageSize={listCtrl.pageSize} + searching={isSearchPending} + topContent={ +
+
+
- -
- } - /> -
- - )} +
+ + +
+ } + /> +
+ + {listCtrl.data.totalItems === 0 ? : null}
); diff --git a/modules/customer-invoices/src/web/issued-invoices/shared/adapters/list-issued-invoice.adapter.ts b/modules/customer-invoices/src/web/issued-invoices/shared/adapters/list-issued-invoice.adapter.ts index 33ed1f20..0b4cc473 100644 --- a/modules/customer-invoices/src/web/issued-invoices/shared/adapters/list-issued-invoice.adapter.ts +++ b/modules/customer-invoices/src/web/issued-invoices/shared/adapters/list-issued-invoice.adapter.ts @@ -2,6 +2,7 @@ import { MoneyDTOHelper } from "@erp/core"; import { MoneyHelper } from "@repo/rdx-utils"; import type { ListIssuedInvoicesResponseDTO } from "../../../../common"; +import { EMPTY_ISSUED_INVOICE_STATUS_COUNTS } from "../entities"; import type { IssuedInvoiceList, IssuedInvoiceListRow, IssuedInvoiceStatus } from "../entities"; import type { VerifactuRecordStatus } from "../entities/verifactu-record-status.entity"; @@ -13,6 +14,7 @@ export const ListIssuedInvoicesAdapter = { perPage: dto.per_page, totalPages: dto.total_pages, totalItems: dto.total_items, + statusCounts: EMPTY_ISSUED_INVOICE_STATUS_COUNTS, }; }, }; diff --git a/modules/customer-invoices/src/web/issued-invoices/shared/entities/index.ts b/modules/customer-invoices/src/web/issued-invoices/shared/entities/index.ts index 4c58c6b2..3f33f681 100644 --- a/modules/customer-invoices/src/web/issued-invoices/shared/entities/index.ts +++ b/modules/customer-invoices/src/web/issued-invoices/shared/entities/index.ts @@ -1,4 +1,5 @@ export * from "./issued-invoice-list.entity"; export * from "./issued-invoice-list-row.entity"; export * from "./issued-invoice-status.entity"; +export * from "./issued-invoice-status-counts.entity"; export * from "./verifactu-record-status.entity"; diff --git a/modules/customer-invoices/src/web/issued-invoices/shared/entities/issued-invoice-list.entity.ts b/modules/customer-invoices/src/web/issued-invoices/shared/entities/issued-invoice-list.entity.ts index 05a01ac8..c05d59d4 100644 --- a/modules/customer-invoices/src/web/issued-invoices/shared/entities/issued-invoice-list.entity.ts +++ b/modules/customer-invoices/src/web/issued-invoices/shared/entities/issued-invoice-list.entity.ts @@ -1,4 +1,5 @@ import type { IssuedInvoiceListRow } from "./issued-invoice-list-row.entity"; +import type { IssuedInvoiceStatusCounts } from "./issued-invoice-status-counts.entity"; /** * Interface que representa la respuesta paginada de una lista de proformas, @@ -11,4 +12,5 @@ export interface IssuedInvoiceList { totalItems: number; page: number; perPage: number; + statusCounts: IssuedInvoiceStatusCounts; } diff --git a/modules/customer-invoices/src/web/issued-invoices/shared/entities/issued-invoice-status-counts.entity.ts b/modules/customer-invoices/src/web/issued-invoices/shared/entities/issued-invoice-status-counts.entity.ts new file mode 100644 index 00000000..ca38c444 --- /dev/null +++ b/modules/customer-invoices/src/web/issued-invoices/shared/entities/issued-invoice-status-counts.entity.ts @@ -0,0 +1,25 @@ +export interface IssuedInvoiceStatusCounts { + all: number; + pendiente: number; + correcto: number; + aceptadoConErrores: number; + incorrecto: number; + duplicado: number; + anulado: number; + facturaInexistente: number; + rechazado: number; + errorServidorAeat: number; +} + +export const EMPTY_ISSUED_INVOICE_STATUS_COUNTS: IssuedInvoiceStatusCounts = { + all: 0, + pendiente: 0, + correcto: 0, + aceptadoConErrores: 0, + incorrecto: 0, + duplicado: 0, + anulado: 0, + facturaInexistente: 0, + rechazado: 0, + errorServidorAeat: 0, +};