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"),
|
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
|
||||||
queryFn: async (context) => {
|
queryFn: async (context) => {
|
||||||
const { signal } = context;
|
const { signal } = context;
|
||||||
if (!invoiceId) {
|
|
||||||
if (!invoiceId) throw new Error("invoiceId is required");
|
if (!invoiceId) throw new Error("invoiceId is required");
|
||||||
}
|
|
||||||
return await dataSource.getOne<Proforma>("customer-invoices", invoiceId, {
|
return await dataSource.getOne<Proforma>("customer-invoices", invoiceId, {
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -49,5 +49,6 @@ export function useDownloadInvoicePDFController() {
|
|||||||
return {
|
return {
|
||||||
download, // (id, reference) => void
|
download, // (id, reference) => void
|
||||||
isLoading: isFetching,
|
isLoading: isFetching,
|
||||||
|
loadingId: pending?.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,10 @@ export function useIssuedInvoiceListPageController() {
|
|||||||
return {
|
return {
|
||||||
listCtrl,
|
listCtrl,
|
||||||
|
|
||||||
|
downloadPDFCtrl,
|
||||||
|
|
||||||
handleDownloadPDF,
|
handleDownloadPDF,
|
||||||
|
pdfDownloadingId: downloadPDFCtrl.loadingId,
|
||||||
|
isPDFDownloading: downloadPDFCtrl.isLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
Spinner,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
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 * as React from "react";
|
||||||
import QrCode from "react-qr-code";
|
import QrCode from "react-qr-code";
|
||||||
|
|
||||||
@ -21,7 +22,8 @@ import type { IssuedInvoiceSummaryData } from "../../../../types";
|
|||||||
|
|
||||||
type GridActionHandlers = {
|
type GridActionHandlers = {
|
||||||
onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
||||||
onSendEmail?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
pdfDownloadingId?: string | null;
|
||||||
|
isPdfDownloading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useIssuedInvoicesGridColumns(
|
export function useIssuedInvoicesGridColumns(
|
||||||
@ -321,28 +323,37 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
minSize: 64,
|
minSize: 64,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const issuedInvoice = row.original;
|
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();
|
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{/* Descargar en PDF */}
|
{/* Descargar en PDF */}
|
||||||
|
{/* Descargar en PDF */}
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("common.download_pdf")}
|
className="size-8"
|
||||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
disabled={isPDFLoading || !isCompleted}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
stop(e);
|
stop(e);
|
||||||
actionHandlers.onDownloadPdf?.(issuedInvoice);
|
isCompleted ? actionHandlers.onDownloadPdf?.(issuedInvoice) : null;
|
||||||
}}
|
}}
|
||||||
size="icon-sm"
|
size="icon"
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
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>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t("common.download_pdf")}</TooltipContent>
|
<TooltipContent>Descargar PDF</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Menú demás acciones */}
|
{/* Menú demás acciones */}
|
||||||
@ -370,10 +381,7 @@ export function useIssuedInvoicesGridColumns(
|
|||||||
<DownloadIcon className="mr-2 size-4" />
|
<DownloadIcon className="mr-2 size-4" />
|
||||||
{t("common.download_pdf")}
|
{t("common.download_pdf")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem className="cursor-pointer" onClick={stop}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => actionHandlers.onSendEmail?.(issuedInvoice)}
|
|
||||||
>
|
|
||||||
<MailIcon className="mr-2 size-4" />
|
<MailIcon className="mr-2 size-4" />
|
||||||
{t("common.send_email")}
|
{t("common.send_email")}
|
||||||
</DropdownMenuItem>{" "}
|
</DropdownMenuItem>{" "}
|
||||||
|
|||||||
@ -22,11 +22,19 @@ import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
|
|||||||
export const IssuedInvoiceListPage = () => {
|
export const IssuedInvoiceListPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { listCtrl, handleDownloadPDF } = useIssuedInvoiceListPageController();
|
const {
|
||||||
|
listCtrl,
|
||||||
|
handleDownloadPDF,
|
||||||
|
isPDFDownloading: isPdfDownloading,
|
||||||
|
pdfDownloadingId,
|
||||||
|
} = useIssuedInvoiceListPageController();
|
||||||
|
|
||||||
const columns = useIssuedInvoicesGridColumns({
|
const columns = useIssuedInvoicesGridColumns({
|
||||||
onDownloadPdf: handleDownloadPDF,
|
onDownloadPdf: handleDownloadPDF,
|
||||||
onSendEmail: () => null,
|
pdfDownloadingId,
|
||||||
|
isPdfDownloading,
|
||||||
|
|
||||||
|
//onSendEmail: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hook con Sheet de shadcn
|
// 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 { useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { invoiceResumeDtoToFormAdapter } from "../../adapters/invoice-resume-dto.adapter";
|
|
||||||
import { useInvoicesQuery } from "../../hooks";
|
import { useInvoicesQuery } from "../../hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
import { issuedInvoiceResumeDtoToFormAdapter } from "../../issued-invoices/adapters/issued-invoice-resume-dto.adapter";
|
||||||
|
|
||||||
export const InvoiceListPage = () => {
|
export const InvoiceListPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -37,7 +37,7 @@ export const InvoiceListPage = () => {
|
|||||||
if (!data) return undefined;
|
if (!data) return undefined;
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
items: invoiceResumeDtoToFormAdapter.fromDto(data.items),
|
items: issuedInvoiceResumeDtoToFormAdapter.fromDto(data.items),
|
||||||
};
|
};
|
||||||
}, [data]);
|
}, [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 "../proformas/adapters/proforma-dto.adapter";
|
||||||
|
|
||||||
export * from "./invoice.form.schema";
|
export * from "./invoice.form.schema";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user