Facturas de cliente
This commit is contained in:
parent
6e0900afae
commit
c1399cd67d
@ -1,3 +0,0 @@
|
||||
export * from "../proformas/adapters/proforma-dto.adapter";
|
||||
|
||||
export * from "./invoice-resume-dto.adapter";
|
||||
@ -1,63 +0,0 @@
|
||||
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
||||
|
||||
import type { InvoiceSummaryFormData } from "../schemas/invoice-resume.form.schema";
|
||||
import type { CustomerInvoiceSummary } from "../schemas/invoices.api.schema";
|
||||
|
||||
/**
|
||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||
*/
|
||||
export const invoiceResumeDtoToFormAdapter = {
|
||||
fromDto(dtos: CustomerInvoiceSummary[], context?: any) {
|
||||
return dtos.map(
|
||||
(dto) =>
|
||||
({
|
||||
...dto,
|
||||
|
||||
subtotal_amount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
||||
subtotal_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
discount_percentage: PercentageDTOHelper.toNumber(dto.discount_percentage),
|
||||
discount_percentage_fmt: PercentageDTOHelper.toNumericString(dto.discount_percentage),
|
||||
|
||||
discount_amount: MoneyDTOHelper.toNumber(dto.discount_amount),
|
||||
discount_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.discount_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
taxable_amount: MoneyDTOHelper.toNumber(dto.taxable_amount),
|
||||
taxable_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.taxable_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
taxes_amount: MoneyDTOHelper.toNumber(dto.taxes_amount),
|
||||
taxes_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.taxes_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
total_amount: MoneyDTOHelper.toNumber(dto.total_amount),
|
||||
total_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(dto.total_amount),
|
||||
Number(dto.total_amount.scale || 2),
|
||||
dto.currency_code,
|
||||
dto.language_code
|
||||
),
|
||||
|
||||
//taxes: dto.taxes,
|
||||
}) as unknown as InvoiceSummaryFormData
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -18,9 +18,8 @@ export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOption
|
||||
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
||||
queryFn: async (context) => {
|
||||
const { signal } = context;
|
||||
if (!invoiceId) {
|
||||
if (!invoiceId) throw new Error("invoiceId is required");
|
||||
}
|
||||
|
||||
return await dataSource.getOne<Proforma>("customer-invoices", invoiceId, {
|
||||
signal,
|
||||
});
|
||||
|
||||
@ -49,5 +49,6 @@ export function useDownloadInvoicePDFController() {
|
||||
return {
|
||||
download, // (id, reference) => void
|
||||
isLoading: isFetching,
|
||||
loadingId: pending?.id,
|
||||
};
|
||||
}
|
||||
|
||||
@ -19,6 +19,10 @@ export function useIssuedInvoiceListPageController() {
|
||||
return {
|
||||
listCtrl,
|
||||
|
||||
downloadPDFCtrl,
|
||||
|
||||
handleDownloadPDF,
|
||||
pdfDownloadingId: downloadPDFCtrl.loadingId,
|
||||
isPDFDownloading: downloadPDFCtrl.isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,12 +7,13 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { DownloadIcon, MailIcon, MoreVerticalIcon, QrCodeIcon } from "lucide-react";
|
||||
import { DownloadIcon, FileDownIcon, MailIcon, MoreVerticalIcon, QrCodeIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import QrCode from "react-qr-code";
|
||||
|
||||
@ -21,7 +22,8 @@ import type { IssuedInvoiceSummaryData } from "../../../../types";
|
||||
|
||||
type GridActionHandlers = {
|
||||
onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
||||
onSendEmail?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
||||
pdfDownloadingId?: string | null;
|
||||
isPdfDownloading?: boolean;
|
||||
};
|
||||
|
||||
export function useIssuedInvoicesGridColumns(
|
||||
@ -321,28 +323,37 @@ export function useIssuedInvoicesGridColumns(
|
||||
minSize: 64,
|
||||
cell: ({ row }) => {
|
||||
const issuedInvoice = row.original;
|
||||
const isCompleted = issuedInvoice.verifactu.status === "Correcto";
|
||||
const isPDFLoading =
|
||||
actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === issuedInvoice.id;
|
||||
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
{/* Descargar en PDF */}
|
||||
{/* Descargar en PDF */}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("common.download_pdf")}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
className="size-8"
|
||||
disabled={isPDFLoading || !isCompleted}
|
||||
onClick={(e) => {
|
||||
stop(e);
|
||||
actionHandlers.onDownloadPdf?.(issuedInvoice);
|
||||
isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null;
|
||||
}}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" className="size-4" />
|
||||
{isPDFLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<FileDownIcon className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">Descargar PDF</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.download_pdf")}</TooltipContent>
|
||||
<TooltipContent>Descargar PDF</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Menú demás acciones */}
|
||||
@ -370,10 +381,7 @@ export function useIssuedInvoicesGridColumns(
|
||||
<DownloadIcon className="mr-2 size-4" />
|
||||
{t("common.download_pdf")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => actionHandlers.onSendEmail?.(issuedInvoice)}
|
||||
>
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={stop}>
|
||||
<MailIcon className="mr-2 size-4" />
|
||||
{t("common.send_email")}
|
||||
</DropdownMenuItem>{" "}
|
||||
|
||||
@ -22,11 +22,19 @@ import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
||||
export const IssuedInvoiceListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { listCtrl, handleDownloadPDF } = useIssuedInvoiceListPageController();
|
||||
const {
|
||||
listCtrl,
|
||||
handleDownloadPDF,
|
||||
isPDFDownloading: isPdfDownloading,
|
||||
pdfDownloadingId,
|
||||
} = useIssuedInvoiceListPageController();
|
||||
|
||||
const columns = useIssuedInvoicesGridColumns({
|
||||
onDownloadPdf: handleDownloadPDF,
|
||||
onSendEmail: () => null,
|
||||
pdfDownloadingId,
|
||||
isPdfDownloading,
|
||||
|
||||
//onSendEmail: () => null,
|
||||
});
|
||||
|
||||
// Hook con Sheet de shadcn
|
||||
|
||||
@ -1,178 +0,0 @@
|
||||
import { SimpleSearchInput } from "@erp/core/components";
|
||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { usePinnedPreviewSheet } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import type { InvoiceSummaryFormData, InvoicesPageFormData } from "../../schemas";
|
||||
|
||||
export type InvoiceUpdateCompProps = {
|
||||
invoicesPage: InvoicesPageFormData;
|
||||
loading?: boolean;
|
||||
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
onPageChange?: (pageNumber: number) => void;
|
||||
onPageSizeChange?: (pageSize: number) => void;
|
||||
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
|
||||
onRowClick?: (
|
||||
row: InvoiceSummaryFormData,
|
||||
index: number,
|
||||
event: React.MouseEvent<HTMLTableRowElement>
|
||||
) => void;
|
||||
};
|
||||
|
||||
// Create new GridExample component
|
||||
export const InvoicesListGrid = ({
|
||||
invoicesPage,
|
||||
loading,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onRowClick,
|
||||
}: InvoiceUpdateCompProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { items, total_items } = invoicesPage;
|
||||
|
||||
// Hook con Sheet de shadcn
|
||||
const preview = usePinnedPreviewSheet<InvoiceSummaryFormData>({
|
||||
persistKey: "invoice-preview-pin",
|
||||
widthClass: "w-[500px]",
|
||||
});
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState("todas");
|
||||
|
||||
const columns = useProformasGridColumns({
|
||||
onEdit: (invoice) => navigate(`/customer-invoices/${invoice.id}/edit`),
|
||||
onDuplicate: (invoice) => null, //duplicateInvoice(inv.id),
|
||||
onDownloadPdf: (invoice) => null, //downloadInvoicePdf(inv.id),
|
||||
onSendEmail: (invoice) => null, //sendInvoiceEmail(inv.id),
|
||||
onDelete: (invoice) => null, //confirmDelete(inv.id),
|
||||
});
|
||||
|
||||
// Navegación accesible (click o teclado)
|
||||
const goToRow = useCallback(
|
||||
(id: string, newTab = false) => {
|
||||
const url = `/customer-invoices/${id}/edit`;
|
||||
newTab ? window.open(url, "_blank", "noopener,noreferrer") : navigate(url);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const onRowClicked = useCallback(
|
||||
(e: RowClickedEvent<any>) => {
|
||||
if (!e.data) return;
|
||||
const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
|
||||
goToRow(e.data.id, newTab);
|
||||
},
|
||||
[goToRow]
|
||||
);
|
||||
|
||||
const onCellKeyDown = useCallback(
|
||||
(e: CellKeyDownEvent<any>) => {
|
||||
if (!e.data) return;
|
||||
|
||||
const ev = e.event;
|
||||
if (!(ev && ev instanceof KeyboardEvent)) return;
|
||||
|
||||
const key = ev.key;
|
||||
if (key === "Enter" || key === " ") {
|
||||
ev.preventDefault();
|
||||
goToRow(e.data.id);
|
||||
}
|
||||
if ((ev.ctrlKey || ev.metaKey) && key === "Enter") {
|
||||
ev.preventDefault();
|
||||
goToRow(e.data.id, true);
|
||||
}
|
||||
},
|
||||
[goToRow]
|
||||
);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(invoice: InvoiceSummaryFormData, _i: number, e: React.MouseEvent) => {
|
||||
const url = `/customer-invoices/${invoice.id}/edit`;
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
preview.open(invoice);
|
||||
},
|
||||
[preview]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<SkeletonDataTable
|
||||
columns={columns.length}
|
||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||
rows={Math.max(6, pageSize)}
|
||||
showFooter
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render principal
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Barra de filtros */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
|
||||
{/*<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full sm:w-48 bg-white border-gray-200 shadow-sm">
|
||||
<FilterIcon className="mr-2 h-4 w-4" />
|
||||
<SelectValue placeholder="Estado" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todas">Todas</SelectItem>
|
||||
<SelectItem value="pagada">Pagadas</SelectItem>
|
||||
<SelectItem value="pendiente">Pendientes</SelectItem>
|
||||
<SelectItem value="vencida">Vencidas</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" className="border-blue-200 text-blue-600 hover:bg-blue-50 bg-transparent">
|
||||
<FileDownIcon className="mr-2 h-4 w-4" />
|
||||
Exportar
|
||||
</Button>*/}
|
||||
</div>
|
||||
<div className="relative flex">
|
||||
<div className={preview.isPinned ? "flex-1 mr-[500px]" : "flex-1"}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
enablePagination
|
||||
enableRowSelection
|
||||
manualPagination
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
onRowClick={handleRowClick}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
readOnly
|
||||
totalItems={total_items}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*<preview.Preview>
|
||||
{({ item, isPinned, close, togglePin }) => (
|
||||
<InvoicePreviewPanel
|
||||
invoice={item}
|
||||
isPinned={isPinned}
|
||||
onClose={close}
|
||||
onTogglePin={togglePin}
|
||||
/>
|
||||
)}
|
||||
</preview.Preview>*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -6,9 +6,9 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { invoiceResumeDtoToFormAdapter } from "../../adapters/invoice-resume-dto.adapter";
|
||||
import { useInvoicesQuery } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { issuedInvoiceResumeDtoToFormAdapter } from "../../issued-invoices/adapters/issued-invoice-resume-dto.adapter";
|
||||
|
||||
export const InvoiceListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -37,7 +37,7 @@ export const InvoiceListPage = () => {
|
||||
if (!data) return undefined;
|
||||
return {
|
||||
...data,
|
||||
items: invoiceResumeDtoToFormAdapter.fromDto(data.items),
|
||||
items: issuedInvoiceResumeDtoToFormAdapter.fromDto(data.items),
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export * from "../adapters/invoice-resume-dto.adapter";
|
||||
export * from "../issued-invoices/adapters/issued-invoice-resume-dto.adapter";
|
||||
export * from "../proformas/adapters/proforma-dto.adapter";
|
||||
|
||||
export * from "./invoice.form.schema";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user