From 9cade597d73b6597a0d1edb9dfa880673cd47173 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 5 Aug 2026 13:17:55 +0200 Subject: [PATCH] feat: add proforma detail components and utilities - Introduced ProformaDetailContent, ProformaDetailHeader, ProformaDetailInspector, ProformaDocumentPane, ProformaGeneralPanel components for displaying detailed proforma information. - Created various sections including ProformaCommercialSection, ProformaContentSection, ProformaCustomerSection, ProformaDatesSection, ProformaFiscalSection, ProformaLinkedInvoiceSection, and ProformaSummarySection to organize proforma details. - Implemented utility functions to build proforma detail actions and view models, enhancing the management of proforma states and actions. - Added translations and improved accessibility features across components. --- .../src/common/locales/en.json | 125 +++- .../src/common/locales/es.json | 125 +++- .../use-proforma-detail-page-controller.ts | 691 +++++++++++------- .../web/proformas/detail/entities/index.ts | 1 + .../proforma-detail-view-models.entity.ts | 148 ++++ .../src/web/proformas/detail/index.ts | 2 + .../web/proformas/detail/ui/blocks/index.ts | 8 +- .../ui/blocks/proforma-detail-content.tsx | 17 + .../blocks/proforma-detail-header-actions.tsx | 155 ++-- .../ui/blocks/proforma-detail-header.tsx | 49 ++ .../ui/blocks/proforma-detail-inspector.tsx | 40 + .../blocks/proforma-detail-preview-card.tsx | 32 - .../ui/blocks/proforma-detail-sidebar.tsx | 28 - .../blocks/proforma-detail-summary-card.tsx | 159 ---- .../ui/blocks/proforma-document-pane.tsx | 90 +++ .../ui/blocks/proforma-general-panel.tsx | 32 + .../proformas/detail/ui/components/index.ts | 8 +- .../proforma-detail-archived-badge.tsx | 4 +- ...proforma-detail-billing-inspector-card.tsx | 40 - .../proforma-detail-dates-inspector-card.tsx | 41 -- .../ui/components/proforma-detail-field.tsx | 28 + .../proforma-detail-header-meta.tsx | 22 +- .../proforma-detail-inspector-card.tsx | 29 - .../proforma-detail-notes-inspector-card.tsx | 31 - .../ui/components/proforma-detail-section.tsx | 38 + .../components/proforma-detail-skeleton.tsx | 58 +- .../proforma-detail-status-inspector-card.tsx | 76 -- .../proforma-detail-totals-inspector-card.tsx | 49 -- .../src/web/proformas/detail/ui/index.ts | 1 + .../detail/ui/pages/proforma-detail-page.tsx | 223 +++--- .../web/proformas/detail/ui/sections/index.ts | 7 + .../sections/proforma-commercial-section.tsx | 35 + .../ui/sections/proforma-content-section.tsx | 48 ++ .../ui/sections/proforma-customer-section.tsx | 59 ++ .../ui/sections/proforma-dates-section.tsx | 29 + .../ui/sections/proforma-fiscal-section.tsx | 110 +++ .../proforma-linked-invoice-section.tsx | 53 ++ .../ui/sections/proforma-summary-section.tsx | 77 ++ .../utils/build-proforma-detail-actions.ts | 166 +++++ .../build-proforma-detail-view-models.ts | 242 ++++++ .../src/web/proformas/detail/utils/index.ts | 2 + .../hooks/use-download-proforma-pdf-query.ts | 8 +- .../use-proforma-grid-columns.tsx | 2 +- .../src/web/proformas/shared/hooks/keys.ts | 6 + .../shared/hooks/proforma-cache-strategy.ts | 89 ++- .../hooks/use-proforma-archive-mutation.ts | 4 +- .../use-proforma-change-status-mutation.ts | 6 +- .../hooks/use-proforma-issue-mutation.ts | 6 +- .../hooks/use-proforma-unarchive-mutation.ts | 4 +- modules/identity/README.md | 43 +- ...identity-auth-public-services.interface.ts | 2 +- packages/rdx-ui/src/components/pdf-viewer.tsx | 82 ++- 52 files changed, 2417 insertions(+), 1013 deletions(-) create mode 100644 modules/customer-invoices/src/web/proformas/detail/entities/index.ts create mode 100644 modules/customer-invoices/src/web/proformas/detail/entities/proforma-detail-view-models.entity.ts create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-content.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-header.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-inspector.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-preview-card.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-sidebar.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-summary-card.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-document-pane.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-general-panel.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-billing-inspector-card.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-dates-inspector-card.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-field.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-inspector-card.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-notes-inspector-card.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-section.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-status-inspector-card.tsx delete mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-totals-inspector-card.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/sections/index.ts create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/sections/proforma-commercial-section.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/sections/proforma-content-section.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/sections/proforma-customer-section.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/sections/proforma-dates-section.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/sections/proforma-fiscal-section.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/sections/proforma-linked-invoice-section.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/ui/sections/proforma-summary-section.tsx create mode 100644 modules/customer-invoices/src/web/proformas/detail/utils/build-proforma-detail-actions.ts create mode 100644 modules/customer-invoices/src/web/proformas/detail/utils/build-proforma-detail-view-models.ts create mode 100644 modules/customer-invoices/src/web/proformas/detail/utils/index.ts diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index aa47e814..3e77bc3c 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -183,6 +183,126 @@ "error_title": "The proforma could not be duplicated", "error_not_found_message": "The proforma no longer exists or is not available.", "error_unknown_message": "An unexpected error occurred while duplicating the proforma." + }, + "detail": { + "header": { + "title": "Proforma {{reference}}", + "archived_label": "Archived" + }, + "document": { + "title": "Document", + "description": "Preview of the generated PDF for this proforma.", + "error": "The PDF could not be loaded.", + "error_title": "The preview could not be loaded", + "loading": "Loading document...", + "refreshing": "Updating document...", + "retry": "Retry", + "empty_title": "Document unavailable", + "empty_description": "The proforma PDF is not available to display yet.", + "actions": { + "open": "Open PDF", + "download": "Download PDF", + "print": "Print PDF", + "zoom_in": "Zoom in", + "zoom_out": "Zoom out", + "zoom_select": "Select zoom", + "fit": "Fit" + } + }, + "regions": { + "document": "Document", + "details": "Details" + }, + "customer": { + "unknown": "Unnamed customer" + }, + "sections": { + "summary": "Summary", + "customer": "Customer", + "dates": "Dates", + "commercial": "Commercial terms", + "fiscal": "Tax", + "content": "Description and notes", + "linked_invoice": "Related document" + }, + "section_descriptions": { + "summary": "Primary operational information for the proforma.", + "customer": "", + "dates": "Dates available in the current detail contract.", + "commercial": "Payment method, payment term and commercial references.", + "fiscal": "Tax summary and breakdown prepared from the ViewModel.", + "content": "Textual content associated with the proforma.", + "linked_invoice": "Current state of the Proforma -> Invoice relationship." + }, + "fields": { + "total": "Total", + "reference": "Proforma number", + "status": "Status", + "currency": "Currency", + "lines": "Lines", + "units": "Units", + "global_discount": "Global discount", + "customer_tin": "TIN", + "address": "Address", + "proforma_date": "Proforma date", + "operation_date": "Operation date", + "payment_method": "Payment method", + "payment_term": "Payment term", + "language": "Language", + "internal_reference": "Internal reference", + "tax_regime": "Tax regime", + "tax_mode": "Tax mode", + "subtotal": "Subtotal", + "discount": "Discount", + "taxable_base": "Taxable base", + "tax": "Tax", + "rec": "Equivalence surcharge", + "retention": "Withholding", + "tax_breakdown": "Tax breakdown", + "description": "Description", + "notes": "Notes", + "linked_invoice_id": "Linked invoice identifier" + }, + "empty": { + "no_address": "No address recorded.", + "not_available": "—", + "no_description": "No description.", + "no_notes": "No notes." + }, + "actions": { + "open_customer": "View customer", + "disabled_reasons": { + "edit_archived": "Archived proformas must be unarchived before they can be edited.", + "status_archived": "Unarchive the proforma before changing its status." + } + }, + "fiscal": { + "unknown_tax_code": "Tax", + "tax_mode": { + "single": "Single tax", + "perLine": "Tax per line" + } + }, + "linked_invoice": { + "states": { + "not-issued": { + "title": "Pending issuance", + "description": "The proforma has not been converted into an invoice yet." + }, + "ready-to-issue": { + "title": "Ready to issue", + "description": "It can be converted into an invoice from the header." + }, + "issued-without-link": { + "title": "Issued", + "description": "No reference to the linked invoice was returned." + }, + "issued-linked-without-detail-route": { + "title": "Issued invoice", + "description": "The linked invoice exists, but its individual detail view is not available in this application yet." + } + } + } } }, "pages": { @@ -273,8 +393,7 @@ "description": "Delete the selected customer proforma" }, "delete.error_title": "Unable to delete the proforma", - "delete.error_unexpected_message": "An unexpected error occurred while deleting the proforma." - , + "delete.error_unexpected_message": "An unexpected error occurred while deleting the proforma.", "view": { "title": "View customer proforma", "description": "View the details of the selected customer proforma" @@ -471,4 +590,4 @@ } } } -} +} \ No newline at end of file diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index 5152e4d0..401a7fe8 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -184,6 +184,126 @@ "error_title": "No se pudo duplicar la proforma", "error_not_found_message": "La proforma ya no existe o no está disponible.", "error_unknown_message": "Ha ocurrido un error inesperado al duplicar la proforma." + }, + "detail": { + "header": { + "title": "Proforma {{reference}}", + "archived_label": "Archivada" + }, + "document": { + "title": "Documento", + "description": "Vista previa del PDF generado de la proforma.", + "error": "No se pudo cargar el PDF.", + "error_title": "No se pudo cargar la vista previa", + "loading": "Cargando documento...", + "refreshing": "Actualizando documento...", + "retry": "Reintentar", + "empty_title": "Documento no disponible", + "empty_description": "El PDF de la proforma todavía no está disponible para mostrarse.", + "actions": { + "open": "Abrir PDF", + "download": "Descargar PDF", + "print": "Imprimir PDF", + "zoom_in": "Acercar", + "zoom_out": "Alejar", + "zoom_select": "Seleccionar zoom", + "fit": "Ajustar" + } + }, + "regions": { + "document": "Documento", + "details": "Detalles" + }, + "customer": { + "unknown": "Cliente sin nombre" + }, + "sections": { + "summary": "Resumen", + "customer": "Cliente", + "dates": "Fechas", + "commercial": "Condiciones comerciales", + "fiscal": "Fiscalidad", + "content": "Descripción y notas", + "linked_invoice": "Documento relacionado" + }, + "section_descriptions": { + "summary": "Datos operativos principales de la proforma.", + "customer": "", + "dates": "Fechas disponibles en el contrato actual del detalle.", + "commercial": "Método, plazo y referencias comerciales configuradas.", + "fiscal": "Resumen fiscal y desglose preparado desde el ViewModel.", + "content": "Contenido textual asociado a la proforma.", + "linked_invoice": "Estado actual de la relación Proforma -> Factura." + }, + "fields": { + "total": "Total", + "reference": "Número de proforma", + "status": "Estado", + "currency": "Moneda", + "lines": "Líneas", + "units": "Unidades", + "global_discount": "Descuento global", + "customer_tin": "NIF", + "address": "Dirección", + "proforma_date": "Fecha de proforma", + "operation_date": "Fecha de operación", + "payment_method": "Método de pago", + "payment_term": "Plazo de pago", + "language": "Idioma", + "internal_reference": "Referencia interna", + "tax_regime": "Régimen fiscal", + "tax_mode": "Modo fiscal", + "subtotal": "Subtotal", + "discount": "Descuento", + "taxable_base": "Base imponible", + "tax": "Impuesto", + "rec": "Recargo de equivalencia", + "retention": "Retención", + "tax_breakdown": "Desglose fiscal", + "description": "Descripción", + "notes": "Notas", + "linked_invoice_id": "Identificador de factura vinculada" + }, + "empty": { + "no_address": "Sin dirección registrada.", + "not_available": "—", + "no_description": "Sin descripción.", + "no_notes": "Sin notas." + }, + "actions": { + "open_customer": "Ver cliente", + "disabled_reasons": { + "edit_archived": "Las proformas archivadas deben desarchivarse antes de editarse.", + "status_archived": "Desarchiva la proforma para poder cambiar su estado." + } + }, + "fiscal": { + "unknown_tax_code": "Impuesto", + "tax_mode": { + "single": "Impuesto único", + "perLine": "Impuestos por línea" + } + }, + "linked_invoice": { + "states": { + "not-issued": { + "title": "Pendiente de emitir", + "description": "La proforma todavía no se ha convertido en factura." + }, + "ready-to-issue": { + "title": "Lista para emitir", + "description": "Puede convertirse en factura desde la cabecera." + }, + "issued-without-link": { + "title": "Emitida", + "description": "No se ha recibido una referencia a la factura vinculada." + }, + "issued-linked-without-detail-route": { + "title": "Factura emitida", + "description": "La factura vinculada existe, pero su detalle individual todavía no está disponible en esta aplicación." + } + } + } } }, "pages": { @@ -274,8 +394,7 @@ "description": "Eliminar la proforma seleccionada" }, "delete.error_title": "No se pudo eliminar la proforma", - "delete.error_unexpected_message": "Ha ocurrido un error inesperado al eliminar la proforma." - , + "delete.error_unexpected_message": "Ha ocurrido un error inesperado al eliminar la proforma.", "view": { "title": "Ver proforma", "description": "Ver los detalles de la proforma seleccionada" @@ -463,4 +582,4 @@ } } } -} +} \ No newline at end of file diff --git a/modules/customer-invoices/src/web/proformas/detail/controllers/use-proforma-detail-page-controller.ts b/modules/customer-invoices/src/web/proformas/detail/controllers/use-proforma-detail-page-controller.ts index 1c287d4b..6e9e2d82 100644 --- a/modules/customer-invoices/src/web/proformas/detail/controllers/use-proforma-detail-page-controller.ts +++ b/modules/customer-invoices/src/web/proformas/detail/controllers/use-proforma-detail-page-controller.ts @@ -1,250 +1,441 @@ -import { useReturnToNavigation } from "@erp/core/hooks"; -import { - getPaymentMethodOptions, - usePaymentMethodsListQuery, -} from "@erp/catalogs/client/payment-methods"; -import { - getPaymentTermOptions, - usePaymentTermsListQuery, -} from "@erp/catalogs/client/payment-terms"; -import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers"; -import { useEffect, useMemo, useState } from "react"; -import { createSearchParams, useNavigate, useParams } from "react-router-dom"; - -import { useTranslation } from "../../../i18n"; -import { useDownloadProformaPDFController, useDownloadProformaPDFQuery } from "../../download-pdf"; -import { useIssueProformaDialogController } from "../../issue-proforma"; -import { prepareIssueProformaTarget } from "../../issue-proforma/utils"; -import { - useDuplicateProformaByIdMutation, - useProformaArchiveMutation, - useProformaGetQuery, - useProformaUnarchiveMutation, -} from "../../shared"; - -const PROFORMAS_FALLBACK_PATH = "/proformas"; - -const ACTIVE_ONLY_FILTERS = [ - { - field: "isActive", - operator: "EQUALS" as const, - value: "true", - }, -]; - -export const useProformaDetailPageController = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const { id = "" } = useParams<{ id: string }>(); - const [previewZoom, setPreviewZoom] = useState("fit"); - - const { currentReturnTo, navigateBack } = useReturnToNavigation({ - fallbackPath: PROFORMAS_FALLBACK_PATH, - }); - - const proformaQuery = useProformaGetQuery({ - id, - enabled: Boolean(id), - }); - - const issueDialogCtrl = useIssueProformaDialogController(); - const downloadCtrl = useDownloadProformaPDFController(); - const previewPdfQuery = useDownloadProformaPDFQuery(id, { - enabled: Boolean(id), - }); - const duplicateMutation = useDuplicateProformaByIdMutation(); - const archiveMutation = useProformaArchiveMutation(); - const unarchiveMutation = useProformaUnarchiveMutation(); - const [previewPdfUrl, setPreviewPdfUrl] = useState(null); - - useEffect(() => { - if (!previewPdfQuery.data) { - setPreviewPdfUrl(null); - return; - } - - const objectUrl = URL.createObjectURL(previewPdfQuery.data); - setPreviewPdfUrl(objectUrl); - - return () => { - URL.revokeObjectURL(objectUrl); - }; - }, [previewPdfQuery.data]); - - const paymentMethodsQuery = usePaymentMethodsListQuery({ - criteria: { - pageSize: 999, - filters: ACTIVE_ONLY_FILTERS, - }, - }); - - const paymentTermsQuery = usePaymentTermsListQuery({ - criteria: { - pageSize: 999, - filters: ACTIVE_ONLY_FILTERS, - }, - }); - - const paymentMethodOptions = useMemo(() => { - return getPaymentMethodOptions(paymentMethodsQuery.data?.items ?? []); - }, [paymentMethodsQuery.data?.items]); - - const paymentTermOptions = useMemo(() => { - return getPaymentTermOptions(paymentTermsQuery.data?.items ?? []); - }, [paymentTermsQuery.data?.items]); - - const proforma = proformaQuery.data; - - const paymentMethodLabel = - paymentMethodOptions.find((option) => option.value === proforma?.paymentMethodId)?.label ?? - null; - - const paymentTermLabel = - paymentTermOptions.find((option) => option.value === proforma?.paymentTermId)?.label ?? null; - - const handleEdit = () => { - if (!proforma) { - return; - } - - navigate({ - pathname: `/proformas/${proforma.id}/edit`, - search: createSearchParams({ - returnTo: currentReturnTo, - }).toString(), - }); - }; - - const handleDownload = () => { - if (!proforma) { - return; - } - - downloadCtrl.download(proforma.id, proforma.proformaReference); - }; - - const handleOpenPreviewPdf = () => { - if (previewPdfUrl) { - window.open(previewPdfUrl, "_blank", "noopener,noreferrer"); - return; - } - - handleDownload(); - }; - - const handlePrintPreview = () => { - handleOpenPreviewPdf(); - }; - - const handleDuplicate = async () => { - if (!proforma) { - return; - } - - try { - const duplicated = await duplicateMutation.mutateAsync({ proformaId: proforma.id }); - - showSuccessToast( - t("proformas.duplicate.success_title"), - duplicated.proformaReference - ? t("proformas.duplicate.success_with_reference", { - reference: duplicated.proformaReference, - }) - : t("proformas.duplicate.success_description") - ); - - navigate({ - pathname: `/proformas/${duplicated.id}/edit`, - search: createSearchParams({ - returnTo: currentReturnTo, - }).toString(), - }); - } catch (error) { - const message = - error instanceof Error - ? error.message - : t("proformas.duplicate.error_unknown_message"); - - showErrorToast(t("proformas.duplicate.error_title"), message); - } - }; - - const handleArchiveToggle = async () => { - if (!proforma) { - return; - } - - try { - if (proforma.isArchived) { - await unarchiveMutation.mutateAsync({ proformaId: proforma.id }); - - showSuccessToast("Proforma desarchivada", "La proforma vuelve a estar disponible."); - } else { - await archiveMutation.mutateAsync({ proformaId: proforma.id }); - - showSuccessToast("Proforma archivada", "La proforma se ha movido al archivo."); - } - - await proformaQuery.refetch(); - } catch (error) { - showErrorToast( - proforma.isArchived ? "No se pudo desarchivar" : "No se pudo archivar", - error instanceof Error ? error.message : "Inténtalo de nuevo más tarde." - ); - } - }; - - const primaryAction = useMemo(() => { - if (!proforma) { - return { - label: "Editar", - onClick: () => undefined, - }; - } - - if (proforma.isArchived) { - return { - label: "Desarchivar", - onClick: handleArchiveToggle, - }; - } - - if (proforma.status === "approved") { - return { - label: "Emitir factura", - onClick: () => issueDialogCtrl.openDialog(prepareIssueProformaTarget(proforma)), - }; - } - - return { - label: "Editar", - onClick: handleEdit, - }; - }, [handleArchiveToggle, issueDialogCtrl, proforma]); - - return { - id, - proforma, - proformaQuery, - paymentMethodLabel, - paymentTermLabel, - previewZoom, - setPreviewZoom, - previewPdfQuery, - previewPdfUrl, - currentReturnTo, - navigateBack, - primaryAction, - issueDialogCtrl, - downloadCtrl, - duplicateMutation, - archiveMutation, - unarchiveMutation, - handleEdit, - handleDownload, - handleOpenPreviewPdf, - handlePrintPreview, - handleDuplicate, - handleArchiveToggle, - }; -}; +import { + getPaymentMethodOptions, + usePaymentMethodsListQuery, +} from "@erp/catalogs/client/payment-methods"; +import { + getPaymentTermOptions, + usePaymentTermsListQuery, +} from "@erp/catalogs/client/payment-terms"; +import { useReturnToNavigation } from "@erp/core/hooks"; +import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers"; +import { useEffect, useMemo, useState } from "react"; +import { createSearchParams, useNavigate, useParams } from "react-router-dom"; + +import { useTranslation } from "../../../i18n"; +import { useChangeProformaStatusDialogController } from "../../change-status"; +import { useDownloadProformaPDFQuery } from "../../download-pdf"; +import { useIssueProformaDialogController } from "../../issue-proforma"; +import { prepareIssueProformaTarget } from "../../issue-proforma/utils"; +import { + useDuplicateProformaByIdMutation, + useProformaArchiveMutation, + useProformaGetQuery, + useProformaUnarchiveMutation, +} from "../../shared"; +import type { + ProformaDetailActionKey, + ProformaDetailActionsViewModel, + ProformaDetailDocumentState, + ProformaDetailDocumentViewModel, + ProformaDetailGeneralViewModel, + ProformaDetailHeaderViewModel, + ProformaDetailPageState, +} from "../entities"; +import { + buildProformaDetailActions, + buildProformaDetailDocumentViewModel, + buildProformaDetailGeneralViewModel, + buildProformaDetailHeaderViewModel, +} from "../utils"; + +const PROFORMAS_FALLBACK_PATH = "/proformas"; +const INVALID_FILENAME_CHARACTERS = /[<>:"/\\|?*\u0000-\u001F]/g; + +const ACTIVE_ONLY_FILTERS = [ + { + field: "isActive", + operator: "EQUALS" as const, + value: "true", + }, +]; + +const isUsablePdfBlob = (value: Blob | undefined): value is Blob => { + return value instanceof Blob && value.size > 0; +}; + +const buildProformaPdfFilename = (reference: string) => { + const sanitizedReference = reference + .trim() + .replace(INVALID_FILENAME_CHARACTERS, "-") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); + + return `proforma-${sanitizedReference || "documento"}.pdf`; +}; + +const triggerBlobDownload = (blob: Blob, filename: string) => { + const objectUrl = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = objectUrl; + anchor.download = filename; + anchor.click(); + + window.setTimeout(() => { + URL.revokeObjectURL(objectUrl); + }, 0); +}; + +export const useProformaDetailPageController = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { id = "" } = useParams<{ id: string }>(); + + const { currentReturnTo, navigateBack } = useReturnToNavigation({ + fallbackPath: PROFORMAS_FALLBACK_PATH, + }); + + const proformaQuery = useProformaGetQuery({ + id, + enabled: Boolean(id), + }); + + const issueDialogCtrl = useIssueProformaDialogController(); + const changeStatusDialogCtrl = useChangeProformaStatusDialogController(); + const previewPdfQuery = useDownloadProformaPDFQuery(id, { + enabled: Boolean(id), + }); + const duplicateMutation = useDuplicateProformaByIdMutation(); + const archiveMutation = useProformaArchiveMutation(); + const unarchiveMutation = useProformaUnarchiveMutation(); + const previewPdfBlob = isUsablePdfBlob(previewPdfQuery.data) ? previewPdfQuery.data : null; + const [previewPdfUrl, setPreviewPdfUrl] = useState(null); + + useEffect(() => { + if (!previewPdfBlob) { + setPreviewPdfUrl(null); + return; + } + + const objectUrl = URL.createObjectURL(previewPdfBlob); + setPreviewPdfUrl(objectUrl); + + return () => { + URL.revokeObjectURL(objectUrl); + }; + }, [previewPdfBlob]); + + const paymentMethodsQuery = usePaymentMethodsListQuery({ + criteria: { + pageSize: 999, + filters: ACTIVE_ONLY_FILTERS, + }, + }); + + const paymentTermsQuery = usePaymentTermsListQuery({ + criteria: { + pageSize: 999, + filters: ACTIVE_ONLY_FILTERS, + }, + }); + + const paymentMethodOptions = useMemo(() => { + return getPaymentMethodOptions(paymentMethodsQuery.data?.items ?? []); + }, [paymentMethodsQuery.data?.items]); + + const paymentTermOptions = useMemo(() => { + return getPaymentTermOptions(paymentTermsQuery.data?.items ?? []); + }, [paymentTermsQuery.data?.items]); + + const proforma = proformaQuery.data; + const paymentMethodLabel = + paymentMethodOptions.find((option) => option.value === proforma?.paymentMethodId)?.label ?? + null; + + const paymentTermLabel = + paymentTermOptions.find((option) => option.value === proforma?.paymentTermId)?.label ?? null; + + const paymentContext = useMemo( + () => ({ + translate: (key: string, params?: Record) => + t(key, params), + paymentMethodLabel, + paymentTermLabel, + }), + [paymentMethodLabel, paymentTermLabel, t] + ); + + const state: ProformaDetailPageState = { + isInitialLoading: proformaQuery.isLoading, + isRefetching: proformaQuery.isFetching && !proformaQuery.isLoading, + isError: proformaQuery.isError, + isNotFound: !(proformaQuery.isLoading || proformaQuery.isError || proforma), + errorMessage: proformaQuery.isError + ? proformaQuery.error instanceof Error + ? proformaQuery.error.message + : t("pages.proformas.update.load_error.message") + : null, + }; + + const documentState: ProformaDetailDocumentState = { + isInitialLoading: previewPdfQuery.isLoading && !previewPdfBlob, + isFetching: previewPdfQuery.isFetching && !previewPdfQuery.isLoading, + isError: previewPdfQuery.isError, + errorMessage: previewPdfQuery.isError + ? previewPdfQuery.error instanceof Error + ? previewPdfQuery.error.message + : t("proformas.detail.document.error") + : null, + }; + + const handleEdit = () => { + if (!proforma) { + return; + } + + navigate({ + pathname: `/proformas/${proforma.id}/edit`, + search: createSearchParams({ + returnTo: currentReturnTo, + }).toString(), + }); + }; + + const handleDownload = async () => { + if (!proforma) { + return; + } + + const filename = buildProformaPdfFilename(proforma.proformaReference); + + if (previewPdfBlob) { + triggerBlobDownload(previewPdfBlob, filename); + return; + } + + const result = await previewPdfQuery.refetch(); + + if (result.error) { + showErrorToast( + t("proformas.detail.document.error_title"), + result.error instanceof Error ? result.error.message : t("proformas.detail.document.error") + ); + return; + } + + if (!isUsablePdfBlob(result.data)) { + showErrorToast( + t("proformas.detail.document.empty_title"), + t("proformas.detail.document.empty_description") + ); + return; + } + + triggerBlobDownload(result.data, filename); + }; + + const handleOpenCustomer = () => { + if (!proforma?.customerId) { + return; + } + + navigate({ + pathname: `/customers/${proforma.customerId}`, + search: createSearchParams({ + returnTo: currentReturnTo, + }).toString(), + }); + }; + + const handleOpenPreviewPdf = () => { + if (!previewPdfUrl) { + return; + } + + window.open(previewPdfUrl, "_blank", "noopener,noreferrer"); + }; + + const handlePrintPreview = () => { + if (!previewPdfUrl) { + return; + } + + window.open(previewPdfUrl, "_blank", "noopener,noreferrer"); + }; + + const handleRetryDocument = async () => { + await previewPdfQuery.refetch(); + }; + + const handleDuplicate = async () => { + if (!proforma) { + return; + } + + try { + const duplicated = await duplicateMutation.mutateAsync({ proformaId: proforma.id }); + + showSuccessToast( + t("proformas.duplicate.success_title"), + duplicated.proformaReference + ? t("proformas.duplicate.success_with_reference", { + reference: duplicated.proformaReference, + }) + : t("proformas.duplicate.success_description") + ); + + navigate({ + pathname: `/proformas/${duplicated.id}/edit`, + search: createSearchParams({ + returnTo: currentReturnTo, + }).toString(), + }); + } catch (error) { + const message = + error instanceof Error ? error.message : t("proformas.duplicate.error_unknown_message"); + + showErrorToast(t("proformas.duplicate.error_title"), message); + } + }; + + const handleArchiveToggle = async () => { + if (!proforma) { + return; + } + + try { + if (proforma.isArchived) { + await unarchiveMutation.mutateAsync({ proformaId: proforma.id }); + + showSuccessToast("Proforma desarchivada", "La proforma vuelve a estar disponible."); + } else { + await archiveMutation.mutateAsync({ proformaId: proforma.id }); + + showSuccessToast("Proforma archivada", "La proforma se ha movido al archivo."); + } + + await proformaQuery.refetch(); + } catch (error) { + showErrorToast( + proforma.isArchived ? "No se pudo desarchivar" : "No se pudo archivar", + error instanceof Error ? error.message : "Inténtalo de nuevo más tarde." + ); + } + }; + + const header: ProformaDetailHeaderViewModel | null = proforma + ? buildProformaDetailHeaderViewModel(proforma, paymentContext) + : null; + + const document: ProformaDetailDocumentViewModel | null = proforma + ? buildProformaDetailDocumentViewModel(previewPdfUrl, paymentContext) + : null; + + const general: ProformaDetailGeneralViewModel | null = proforma + ? buildProformaDetailGeneralViewModel(proforma, paymentContext) + : null; + + const actions: ProformaDetailActionsViewModel = useMemo(() => { + if (!proforma) { + return { + items: [], + }; + } + + return buildProformaDetailActions({ + status: proforma.status, + isArchived: proforma.isArchived, + labels: { + edit: t("pages.proformas.list.actions.edit"), + changeStatus: t("pages.proformas.list.actions.change_status"), + issue: t("pages.proformas.list.actions.issue"), + duplicate: t("pages.proformas.list.actions.duplicate"), + downloadPdf: t("pages.proformas.list.actions.download_pdf"), + archive: t("pages.proformas.list.actions.archive"), + unarchive: t("pages.proformas.list.actions.unarchive"), + disabledReasons: { + archivedOnly: t("pages.proformas.list.actions.disabled_reasons.archived_only"), + archiveConflict: t("proformas.archive.error_conflict_message"), + editIssued: t("pages.proformas.list.actions.disabled_reasons.edit_issued"), + editArchived: t("proformas.detail.actions.disabled_reasons.edit_archived"), + statusIssued: t("pages.proformas.list.actions.disabled_reasons.status_issued"), + statusArchived: t("proformas.detail.actions.disabled_reasons.status_archived"), + issueNotAllowed: t("proformas.issue_proforma_dialog.not_allowed_description"), + operationInProgress: t( + "pages.proformas.list.actions.disabled_reasons.operation_in_progress" + ), + }, + }, + loading: { + issue: issueDialogCtrl.isSubmitting, + duplicate: duplicateMutation.isPending, + "download-pdf": previewPdfQuery.isFetching && !previewPdfBlob, + archive: archiveMutation.isPending, + unarchive: unarchiveMutation.isPending, + }, + }); + }, [ + archiveMutation.isPending, + duplicateMutation.isPending, + issueDialogCtrl.isSubmitting, + previewPdfBlob, + previewPdfQuery.isFetching, + proforma, + t, + unarchiveMutation.isPending, + ]); + + const executeAction = (action: ProformaDetailActionKey) => { + if (!proforma) { + return; + } + + switch (action) { + case "edit": + handleEdit(); + return; + case "change-status": + changeStatusDialogCtrl.openDialog([ + { + id: proforma.id, + reference: proforma.reference!, + status: proforma.status, + }, + ]); + return; + case "issue": + issueDialogCtrl.openDialog(prepareIssueProformaTarget(proforma)); + return; + case "duplicate": + handleDuplicate(); + return; + case "download-pdf": + handleDownload(); + return; + case "archive": + case "unarchive": + handleArchiveToggle(); + return; + default: + return; + } + }; + + return { + state, + documentState, + header, + document, + general, + actions, + navigation: { + currentReturnTo, + navigateBack, + openEdit: handleEdit, + openCustomer: handleOpenCustomer, + }, + handlers: { + executeAction, + }, + dialogs: { + issue: issueDialogCtrl, + changeStatus: changeStatusDialogCtrl, + }, + viewer: { + handleDownload, + handleOpenPreviewPdf, + handlePrintPreview, + handleRetryDocument, + }, + }; +}; diff --git a/modules/customer-invoices/src/web/proformas/detail/entities/index.ts b/modules/customer-invoices/src/web/proformas/detail/entities/index.ts new file mode 100644 index 00000000..b604cadd --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/detail/entities/index.ts @@ -0,0 +1 @@ +export * from "./proforma-detail-view-models.entity"; diff --git a/modules/customer-invoices/src/web/proformas/detail/entities/proforma-detail-view-models.entity.ts b/modules/customer-invoices/src/web/proformas/detail/entities/proforma-detail-view-models.entity.ts new file mode 100644 index 00000000..5bcd5927 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/detail/entities/proforma-detail-view-models.entity.ts @@ -0,0 +1,148 @@ +import type { ProformaStatus } from "../../shared"; + +export interface ProformaDetailPageState { + isInitialLoading: boolean; + isRefetching: boolean; + isError: boolean; + isNotFound: boolean; + errorMessage: string | null; +} + +export interface ProformaDetailHeaderViewModel { + title: string; + customerName: string; + documentReference: string; + status: ProformaStatus; + statusLabel: string; + isArchived: boolean; + archivedLabel: string | null; +} + +export interface ProformaDetailDocumentViewModel { + title: string; + description: string; + src: string | null; + mode: "pdf"; + capabilities: { + canOpen: boolean; + canDownload: boolean; + canPrint: boolean; + }; +} + +export interface ProformaDetailDocumentState { + isInitialLoading: boolean; + isFetching: boolean; + isError: boolean; + errorMessage: string | null; +} + +export interface ProformaDetailSummaryViewModel { + totalLabel: string; + documentReference: string; + statusLabel: string; + currencyCode: string; + lineCount: number; + totalUnitsLabel: string | null; + globalDiscountLabel: string | null; +} + +export interface ProformaDetailCustomerViewModel { + customerId: string; + name: string; + tin: string | null; + addressLines: string[]; + canOpenCustomer: boolean; +} + +export interface ProformaDetailDatesViewModel { + proformaDateLabel: string; + operationDateLabel: string | null; +} + +export interface ProformaDetailCommercialViewModel { + paymentMethodLabel: string | null; + paymentTermLabel: string | null; + languageLabel: string; + internalReferenceLabel: string | null; +} + +export interface ProformaDetailFiscalBreakdownItemViewModel { + key: string; + taxLabel: string; + taxableAmountLabel: string; + taxAmountLabel: string; + recLabel: string | null; + recAmountLabel: string | null; + retentionLabel: string | null; + retentionAmountLabel: string | null; +} + +export interface ProformaDetailFiscalViewModel { + taxRegimeLabel: string | null; + taxModeLabel: string; + subtotalLabel: string; + discountLabel: string; + taxableAmountLabel: string; + taxAmountLabel: string; + recAmountLabel: string | null; + retentionAmountLabel: string | null; + totalAmountLabel: string; + breakdown: ProformaDetailFiscalBreakdownItemViewModel[]; + usesRec: boolean; + usesRetention: boolean; +} + +export interface ProformaDetailContentViewModel { + description: string | null; + notes: string | null; +} + +export type ProformaDetailLinkedInvoiceState = + | "not-issued" + | "ready-to-issue" + | "issued-without-link" + | "issued-linked-without-detail-route"; + +export interface ProformaDetailLinkedInvoiceViewModel { + state: ProformaDetailLinkedInvoiceState; + linkedInvoiceId: string | null; + primaryText: string; + secondaryText: string | null; + canOpenLinkedInvoice: false; +} + +export interface ProformaDetailGeneralViewModel { + summary: ProformaDetailSummaryViewModel; + customer: ProformaDetailCustomerViewModel; + dates: ProformaDetailDatesViewModel; + commercial: ProformaDetailCommercialViewModel; + fiscal: ProformaDetailFiscalViewModel; + content: ProformaDetailContentViewModel; + linkedInvoice: ProformaDetailLinkedInvoiceViewModel; +} + +export type ProformaDetailActionKey = + | "edit" + | "change-status" + | "issue" + | "duplicate" + | "download-pdf" + | "archive" + | "unarchive"; + +export type ProformaDetailActionPlacement = "primary" | "secondary" | "menu"; + +export interface ProformaDetailActionViewModel { + key: ProformaDetailActionKey; + label: string; + visibility: "visible" | "hidden"; + enabled: boolean; + loading: boolean; + disabledReason: string | null; + placement: ProformaDetailActionPlacement; +} + +export interface ProformaDetailActionsViewModel { + items: ProformaDetailActionViewModel[]; +} diff --git a/modules/customer-invoices/src/web/proformas/detail/index.ts b/modules/customer-invoices/src/web/proformas/detail/index.ts index 0d88782e..e498b784 100644 --- a/modules/customer-invoices/src/web/proformas/detail/index.ts +++ b/modules/customer-invoices/src/web/proformas/detail/index.ts @@ -1,2 +1,4 @@ export * from "./controllers"; +export * from "./entities"; export * from "./ui"; +export * from "./utils"; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/index.ts b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/index.ts index e36cd5d3..b286def7 100644 --- a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/index.ts +++ b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/index.ts @@ -1,4 +1,6 @@ export * from "./proforma-detail-header-actions"; -export * from "./proforma-detail-preview-card"; -export * from "./proforma-detail-sidebar"; -export * from "./proforma-detail-summary-card"; +export * from "./proforma-detail-content"; +export * from "./proforma-detail-header"; +export * from "./proforma-detail-inspector"; +export * from "./proforma-general-panel"; +export * from "./proforma-document-pane"; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-content.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-content.tsx new file mode 100644 index 00000000..8da43061 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-content.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react"; + +type ProformaDetailContentProps = { + documentPane: ReactNode; + inspector: ReactNode; +}; + +export const ProformaDetailContent = ({ documentPane, inspector }: ProformaDetailContentProps) => { + return ( +
+
+ {documentPane} + {inspector} +
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-header-actions.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-header-actions.tsx index 17504cf7..5d1f6d51 100644 --- a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-header-actions.tsx +++ b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-header-actions.tsx @@ -1,57 +1,124 @@ -import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@repo/shadcn-ui/components"; -import { DownloadIcon, MoreHorizontalIcon, PencilIcon, RefreshCwIcon } from "lucide-react"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@repo/shadcn-ui/components"; +import { + ArchiveIcon, + CopyIcon, + DownloadIcon, + FileOutputIcon, + MoreHorizontalIcon, + PencilIcon, + RefreshCwIcon, + TagsIcon, + Undo2Icon, +} from "lucide-react"; + +import { useTranslation } from "../../../../i18n"; +import type { ProformaDetailActionKey, ProformaDetailActionsViewModel } from "../../entities"; type ProformaDetailHeaderActionsProps = { - primaryAction: { - label: string; - onClick: () => void; - }; - isPrimaryLoading: boolean; - isArchived: boolean; - onDownload: () => void; - onDuplicate: () => void; - onArchiveToggle: () => void; + actions: ProformaDetailActionsViewModel; + onExecuteAction: (action: ProformaDetailActionKey) => void; }; export const ProformaDetailHeaderActions = ({ - primaryAction, - isPrimaryLoading, - isArchived, - onDownload, - onDuplicate, - onArchiveToggle, + actions, + onExecuteAction, }: ProformaDetailHeaderActionsProps) => { + const { t } = useTranslation(); + const visibleItems = actions.items.filter( + (item) => item.visibility === "visible" && item.key !== "download-pdf" + ); + const primaryAction = visibleItems.find((item) => item.placement === "primary") ?? null; + const secondaryActions = visibleItems.filter((item) => item.placement === "secondary"); + const menuActions = visibleItems.filter((item) => item.placement === "menu"); + + const getActionIcon = (action: ProformaDetailActionKey) => { + switch (action) { + case "change-status": + return TagsIcon; + case "issue": + return FileOutputIcon; + case "duplicate": + return CopyIcon; + case "download-pdf": + return DownloadIcon; + case "archive": + return ArchiveIcon; + case "unarchive": + return Undo2Icon; + case "edit": + default: + return PencilIcon; + } + }; + const PrimaryIcon = primaryAction ? getActionIcon(primaryAction.key) : null; + return (
- + {primaryAction ? ( + + ) : null} - + {secondaryActions.map((action) => { + const Icon = getActionIcon(action.key); - - - - - } - /> - - Duplicar proforma - - {isArchived ? "Desarchivar" : "Archivar"} - - - + return ( + + ); + })} + + {menuActions.length > 0 ? ( + + + + + } + /> + + {menuActions.map((action) => ( + onExecuteAction(action.key)} + > + {action.label} + + ))} + + + ) : null}
); }; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-header.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-header.tsx new file mode 100644 index 00000000..60163985 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-header.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from "../../../../i18n"; +import { ProformaStatusBadge } from "../../../list/ui/components"; +import { ProformaPageHeader } from "../../../shared"; +import type { + ProformaDetailActionKey, + ProformaDetailActionsViewModel, + ProformaDetailHeaderViewModel, +} from "../../entities"; +import { ProformaDetailArchivedBadge, ProformaDetailHeaderMeta } from "../components"; + +import { ProformaDetailHeaderActions } from "./proforma-detail-header-actions"; + +type ProformaDetailHeaderProps = { + header: ProformaDetailHeaderViewModel; + actions: ProformaDetailActionsViewModel; + onBack: () => void; + onExecuteAction: (action: ProformaDetailActionKey) => void; +}; + +export const ProformaDetailHeader = ({ + header, + actions, + onBack, + onExecuteAction, +}: ProformaDetailHeaderProps) => { + const { t } = useTranslation(); + + return ( + } + backLabel={t("pages.proformas.create.back_to_list")} + onBack={onBack} + title={header.title} + > +
+
+ + {header.archivedLabel ? ( + + ) : null} + +
+
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-inspector.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-inspector.tsx new file mode 100644 index 00000000..80c5c17d --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-inspector.tsx @@ -0,0 +1,40 @@ +import { ScrollArea, Separator } from "@repo/shadcn-ui/components"; + +import { useTranslation } from "../../../../i18n"; +import type { ProformaDetailGeneralViewModel } from "../../entities"; + +import { ProformaGeneralPanel } from "./proforma-general-panel"; + +type ProformaDetailInspectorProps = { + general: ProformaDetailGeneralViewModel; + onOpenCustomer: () => void; +}; + +export const ProformaDetailInspector = ({ + general, + onOpenCustomer, +}: ProformaDetailInspectorProps) => { + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-preview-card.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-preview-card.tsx deleted file mode 100644 index 2f2bcfe1..00000000 --- a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-preview-card.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { PDFViewer } from "@repo/rdx-ui/components"; - -type ProformaDetailPreviewCardProps = { - src: string | null; - isLoading: boolean; - error: string | null; - onOpen: () => void; - onDownload: () => void; - onPrint: () => void; -}; - -export const ProformaDetailPreviewCard = ({ - src, - isLoading, - error, - onOpen, - onDownload, - onPrint, -}: ProformaDetailPreviewCardProps) => { - return ( - - ); -}; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-sidebar.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-sidebar.tsx deleted file mode 100644 index c85c52fe..00000000 --- a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-sidebar.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Proforma } from "../../../shared"; -import { ProformaDetailStatusInspectorCard } from "../components"; - -import { ProformaDetailSummaryCard } from "./proforma-detail-summary-card"; - -type ProformaDetailSidebarProps = { - proforma: Proforma; - paymentMethodLabel: string | null; - paymentTermLabel: string | null; - onEdit: () => void; -}; - -export const ProformaDetailSidebar = ({ - proforma, - paymentMethodLabel, - paymentTermLabel, -}: ProformaDetailSidebarProps) => { - return ( -
- - -
- ); -}; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-summary-card.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-summary-card.tsx deleted file mode 100644 index dd8cc9a3..00000000 --- a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-detail-summary-card.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { FieldValueList, FormSectionCard, FormSectionGrid } from "@repo/rdx-ui/components"; -import { Separator } from "@repo/shadcn-ui/components"; -import { LandmarkIcon } from "lucide-react"; - -import { type Proforma } from "../../../shared"; -import { formatProformaDate, formatProformaMoney } from "../components/proforma-detail-formatters"; - -type ProformaDetailSummaryCardProps = { - proforma: Proforma; - paymentMethodLabel: string | null; - paymentTermLabel: string | null; -}; - -const SectionHeading = ({ title, description }: { title: string; description?: string }) => { - return ( -
-

{title}

- {description ?

{description}

: null} -
- ); -}; - -export const ProformaDetailSummaryCard = ({ - proforma, - paymentMethodLabel, - paymentTermLabel, -}: ProformaDetailSummaryCardProps) => { - const billingItems = [ - { - label: "Método de pago", - value: paymentMethodLabel ?? "\u2014", - }, - { - label: "Plazo", - value: paymentTermLabel ?? "\u2014", - }, - { - label: "Moneda", - value: proforma.currencyCode, - }, - ]; - - const datesItems = [ - { - label: "Fecha proforma", - value: formatProformaDate(proforma.proformaDate), - }, - { - label: "Fecha operación", - value: formatProformaDate(proforma.operationDate), - }, - { - label: "Serie", - value: proforma.series ?? "\u2014", - }, - { - label: "Referencia", - value: proforma.reference ?? "\u2014", - }, - ]; - - const totalsItems = [ - { - label: "Subtotal", - value: formatProformaMoney(proforma.subtotalAmount, proforma.currencyCode, proforma.languageCode), - }, - { - label: "Descuentos", - value: `-${formatProformaMoney(proforma.totalDiscountAmount, proforma.currencyCode, proforma.languageCode)}`, - valueClassName: "text-red-600", - }, - { - label: "IVA", - value: formatProformaMoney(proforma.ivaAmount, proforma.currencyCode, proforma.languageCode), - }, - { - label: "Total", - value: formatProformaMoney(proforma.totalAmount, proforma.currencyCode, proforma.languageCode), - valueClassName: "text-lg font-semibold text-foreground", - }, - ]; - - return ( - } - title="Resumen de proforma" - > - -
- - -
- -
- -
- -
- - -
- -
- -
- -
- -
-
-

Descripción

-

{proforma.description ?? "Sin descripción."}

-
- -
-

Observaciones

-

{proforma.notes ?? "Sin notas adicionales."}

-
-
-
- -
- -
- -
- - -
-
-
- ); -}; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-document-pane.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-document-pane.tsx new file mode 100644 index 00000000..dd2dd5ba --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-document-pane.tsx @@ -0,0 +1,90 @@ +import { PDFViewer } from "@repo/rdx-ui/components"; +import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { useId } from "react"; + +import { useTranslation } from "../../../../i18n"; +import type { ProformaDetailDocumentState, ProformaDetailDocumentViewModel } from "../../entities"; + +type ProformaDocumentPaneProps = { + document: ProformaDetailDocumentViewModel; + state: ProformaDetailDocumentState; + onDownload: () => void; + onOpen: () => void; + onPrint: () => void; + onRetry?: () => void; +}; + +export const ProformaDocumentPane = ({ + document, + state, + onDownload, + onOpen, + onPrint, + onRetry, +}: ProformaDocumentPaneProps) => { + const { t } = useTranslation(); + const headingId = useId(); + const isRefreshing = state.isFetching && Boolean(document.src); + const shouldRenderBlockingError = state.isError && !document.src; + const shouldRenderStaleError = state.isError && Boolean(document.src); + + return ( +
+
+

+ {document.title} +

+ + {shouldRenderStaleError ? ( + + {t("proformas.detail.document.error_title")} + +

{state.errorMessage ?? t("proformas.detail.document.error")}

+ {onRetry ? ( + + ) : null} +
+
+ ) : null} + + +
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-general-panel.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-general-panel.tsx new file mode 100644 index 00000000..b8f0e1a4 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/detail/ui/blocks/proforma-general-panel.tsx @@ -0,0 +1,32 @@ +import type { ProformaDetailGeneralViewModel } from "../../entities"; +import { + ProformaCommercialSection, + ProformaCustomerSection, + ProformaFiscalSection, + ProformaLinkedInvoiceSection, + ProformaSummarySection, +} from "../sections"; + +export interface ProformaGeneralPanelProps { + general: ProformaDetailGeneralViewModel; + onOpenCustomer: () => void; +} + +export const ProformaGeneralPanel = ({ + general, + onOpenCustomer, +}: ProformaGeneralPanelProps) => { + return ( +
+ + + + + +
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/components/index.ts b/modules/customer-invoices/src/web/proformas/detail/ui/components/index.ts index a6873af8..14166ff8 100644 --- a/modules/customer-invoices/src/web/proformas/detail/ui/components/index.ts +++ b/modules/customer-invoices/src/web/proformas/detail/ui/components/index.ts @@ -1,9 +1,5 @@ export * from "./proforma-detail-archived-badge"; -export * from "./proforma-detail-billing-inspector-card"; -export * from "./proforma-detail-dates-inspector-card"; +export * from "./proforma-detail-field"; export * from "./proforma-detail-header-meta"; -export * from "./proforma-detail-inspector-card"; -export * from "./proforma-detail-notes-inspector-card"; +export * from "./proforma-detail-section"; export * from "./proforma-detail-skeleton"; -export * from "./proforma-detail-status-inspector-card"; -export * from "./proforma-detail-totals-inspector-card"; diff --git a/modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-archived-badge.tsx b/modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-archived-badge.tsx index e49d2dc1..c9706afe 100644 --- a/modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-archived-badge.tsx +++ b/modules/customer-invoices/src/web/proformas/detail/ui/components/proforma-detail-archived-badge.tsx @@ -3,10 +3,12 @@ import { cn } from "@repo/shadcn-ui/lib/utils"; import { ArchiveIcon } from "lucide-react"; type ProformaDetailArchivedBadgeProps = { + label: string; className?: string; }; export const ProformaDetailArchivedBadge = ({ + label, className, }: ProformaDetailArchivedBadgeProps) => { return ( @@ -21,7 +23,7 @@ export const ProformaDetailArchivedBadge = ({ )} >