Facturas de cliente

This commit is contained in:
David Arranz 2025-11-23 11:04:03 +01:00
parent f39e55ca92
commit 6e0900afae
30 changed files with 535 additions and 81 deletions

View File

@ -15,7 +15,7 @@ const ProformasListPage = lazy(() =>
); );
const IssuedInvoiceListPage = lazy(() => const IssuedInvoiceListPage = lazy(() =>
import("./issued-invoices/pages").then((m) => ({ default: m.IssuedInvoiceListPage })) import("./issued-invoices/list").then((m) => ({ default: m.IssuedInvoiceListPage }))
); );
/*const CustomerInvoiceAdd = lazy(() => /*const CustomerInvoiceAdd = lazy(() =>

View File

@ -0,0 +1,14 @@
import type { IDataSource } from "@erp/core/client";
export async function downloadInvoicePDFApi(
dataSource: IDataSource,
invoiceId: string,
params?: Record<string, unknown>
): Promise<Blob> {
return dataSource.custom<Blob>({
...params,
path: `issued-invoices/${invoiceId}/report`,
method: "get",
responseType: "blob",
});
}

View File

@ -0,0 +1 @@
export * from "./download-invoice-pdf.api";

View File

@ -0,0 +1 @@
export * from "./use-download-invoice-pdf.controller";

View File

@ -0,0 +1,53 @@
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import * as React from "react";
import { useDownloadInvoicePDFQuery } from "../hooks";
interface PendingDownload {
id: string;
invoice_number: string;
}
export function useDownloadInvoicePDFController() {
const [pending, setPending] = React.useState<PendingDownload | null>(null);
const { data, isFetching, refetch } = useDownloadInvoicePDFQuery(pending?.id);
// Efecto: cuando hay blob + pending, disparamos la descarga
React.useEffect(() => {
if (!(pending && data)) return;
const blob = data;
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `invoice-${pending.invoice_number}.pdf`;
a.click();
URL.revokeObjectURL(url);
showSuccessToast(
"Descarga iniciada",
`La factura ${pending.invoice_number} se está descargando.`
);
setPending(null);
}, [data, pending]);
const download = (id: string, invoice_number: string) => {
setPending({ id, invoice_number });
// refetch se dispara en un efecto, o aquí si prefieres:
refetch().catch((err) => {
showErrorToast(
"Error al descargar",
err instanceof Error ? err.message : "Error desconocido"
);
setPending(null);
});
};
return {
download, // (id, reference) => void
isLoading: isFetching,
};
}

View File

@ -0,0 +1 @@
export * from "./use-download-invoice-pdf-query";

View File

@ -0,0 +1,55 @@
// features/invoices/view-pdf/hooks/use-download-invoice-pdf.ts
import { useDataSource } from "@erp/core/hooks";
import { type QueryKey, useQuery } from "@tanstack/react-query";
import { downloadInvoicePDFApi } from "../api";
export const ISSUED_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issued_invoice", id] as const;
type DownloadInvoicePDFOptions = {
enabled?: boolean;
};
export function useDownloadInvoicePDFQuery(
invoiceId?: string,
options?: DownloadInvoicePDFOptions
) {
const dataSource = useDataSource();
const enabled = (options?.enabled ?? true) && !!invoiceId;
return useQuery({
queryKey: ISSUED_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
queryFn: async (context) => {
if (!invoiceId) throw new Error("invoiceId is required");
const { signal } = context;
return await downloadInvoicePDFApi(dataSource, invoiceId, {
signal,
});
},
enabled,
staleTime: 0,
refetchOnWindowFocus: false,
});
}
/*
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>
TQueryFnData: the type returned from the queryFn.
TError: the type of Errors to expect from the queryFn.
TData: the type our data property will eventually have.
Only relevant if you use the select option,
because then the data property can be different
from what the queryFn returns.
Otherwise, it will default to whatever the queryFn returns.
TQueryKey: the type of our queryKey, only relevant
if you use the queryKey that is passed to your queryFn.
*/

View File

@ -4,13 +4,14 @@ import type {
IssuedInvoiceSummaryData, IssuedInvoiceSummaryData,
IssuedInvoiceSummaryPage, IssuedInvoiceSummaryPage,
IssuedInvoiceSummaryPageData, IssuedInvoiceSummaryPageData,
} from "../schema"; } from "../../types";
/** /**
* Convierte el DTO completo de API a datos numéricos para el formulario. * Convierte el DTO completo de API a datos numéricos para el formulario.
*/ */
export const IssuedInvoiceSummaryDtoAdapter = { export const IssuedInvoiceSummaryDtoAdapter = {
fromDto(pageDto: IssuedInvoiceSummaryPage, context?: unknown): IssuedInvoiceSummaryPageData { fromDto(pageDto: IssuedInvoiceSummaryPage, context?: unknown): IssuedInvoiceSummaryPageData {
console.log(pageDto);
return { return {
...pageDto, ...pageDto,
items: pageDto.items.map( items: pageDto.items.map(

View File

@ -0,0 +1,18 @@
import type { CriteriaDTO } from "@erp/core";
import type { IDataSource } from "@erp/core/client";
import type { IssuedInvoiceSummaryPage } from "../../types";
export async function getIssuedInvoiceListApi(
dataSource: IDataSource,
signal: AbortSignal,
criteria: CriteriaDTO
) {
const response = dataSource.getList<IssuedInvoiceSummaryPage>("issued-invoices", {
signal,
...criteria,
});
//return mapIssuedInvoiceList(raw);
return response;
}

View File

@ -0,0 +1 @@
export * from "./get-issued-invoice-list.api";

View File

@ -0,0 +1,2 @@
export * from "./use-issued-invoice-list.controller";
export * from "./use-issued-invoice-list-page.controller.ts";

View File

@ -0,0 +1,24 @@
import React from "react";
import { useDownloadInvoicePDFController } from "../../download-pdf/controller";
import type { IssuedInvoiceSummaryData } from "../../types";
import { useIssuedInvoiceListController } from "./use-issued-invoice-list.controller";
export function useIssuedInvoiceListPageController() {
const listCtrl = useIssuedInvoiceListController();
const downloadPDFCtrl = useDownloadInvoicePDFController();
const handleDownloadPDF = React.useCallback(
(issuedInvoice: IssuedInvoiceSummaryData) => {
downloadPDFCtrl.download(issuedInvoice.id, issuedInvoice.invoice_number);
},
[downloadPDFCtrl]
);
return {
listCtrl,
handleDownloadPDF,
};
}

View File

@ -0,0 +1,51 @@
import type { CriteriaDTO } from "@erp/core";
import { useDebounce } from "@repo/rdx-ui/components";
import { useMemo, useState } from "react";
import { IssuedInvoiceSummaryDtoAdapter } from "../adapters";
import { useIssuedInvoiceListQuery } from "../hooks";
export const useIssuedInvoiceListController = () => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState("");
const [status, setStatus] = useState("all");
const debouncedQ = useDebounce(search, 300);
const criteria = useMemo<CriteriaDTO>(() => {
const baseFilters =
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
return {
q: debouncedQ || "",
pageSize,
pageNumber: pageIndex,
order: "desc",
orderBy: "invoice_date",
filters: baseFilters,
};
}, [pageSize, pageIndex, debouncedQ, status]);
const query = useIssuedInvoiceListQuery({ criteria });
const data = useMemo(
() => (query.data ? IssuedInvoiceSummaryDtoAdapter.fromDto(query.data) : undefined),
[query.data]
);
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
return {
...query,
data,
pageIndex,
pageSize,
search,
setPageIndex,
setPageSize,
setSearchValue,
setStatusFilter,
};
};

View File

@ -0,0 +1 @@
export * from "./use-issued-invoice-list-query";

View File

@ -0,0 +1,38 @@
import type { CriteriaDTO } from "@erp/core";
import { useDataSource } from "@erp/core/hooks";
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
import type { IssuedInvoiceSummaryPage } from "../../types";
import { getIssuedInvoiceListApi } from "../api";
export const ISSUED_INVOICES_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
"issued_invoices",
{
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
];
type IssuedInvoicesQueryOptions = {
enabled?: boolean;
criteria?: CriteriaDTO;
};
// Obtener todas las facturas
export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesQueryOptions) => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? true;
const criteria = options?.criteria ?? {};
return useQuery<IssuedInvoiceSummaryPage, DefaultError>({
queryKey: ISSUED_INVOICES_QUERY_KEY(criteria),
queryFn: async ({ signal }) => getIssuedInvoiceListApi(dataSource, signal, criteria),
enabled,
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
});
};

View File

@ -0,0 +1 @@
export * from "./ui";

View File

@ -0,0 +1 @@
export * from "./issued-invoices-grid";

View File

@ -0,0 +1,2 @@
export * from "./issued-invoices-grid";
export * from "./use-issued-invoices-grid-columns";

View File

@ -0,0 +1,125 @@
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
import type { ColumnDef } from "@tanstack/react-table";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../../../i18n";
import type { IssuedInvoiceSummaryData, IssuedInvoiceSummaryPageData } from "../../../../types";
export type InvoiceUpdateCompProps = {
data: IssuedInvoiceSummaryPageData;
loading?: boolean;
columns: ColumnDef<IssuedInvoiceSummaryData, unknown>[];
pageIndex: number;
pageSize: number;
onPageChange?: (pageNumber: number) => void;
onPageSizeChange?: (pageSize: number) => void;
onRowClick?: (
row: IssuedInvoiceSummaryPageData,
index: number,
event: React.MouseEvent<HTMLTableRowElement>
) => void;
};
// Create new GridExample component
export const IssuedInvoicesGrid = ({
data,
loading,
columns,
pageIndex,
pageSize,
onPageChange,
onPageSizeChange,
onRowClick,
}: InvoiceUpdateCompProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { items, total_items } = data;
// 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: IssuedInvoiceSummaryPageData, _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={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>*/}
</>
);
};

View File

@ -14,21 +14,20 @@ import {
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import { DownloadIcon, MailIcon, MoreVerticalIcon, QrCodeIcon } from "lucide-react"; import { DownloadIcon, 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";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../../i18n";
import type { IssuedInvoiceSummaryData } from "../../../schema"; import type { IssuedInvoiceSummaryData } from "../../../../types";
type GridActionHandlers = { type GridActionHandlers = {
onDownloadPdf?: (proforma: IssuedInvoiceSummaryData) => void; onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
onSendEmail?: (proforma: IssuedInvoiceSummaryData) => void; onSendEmail?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
}; };
export function useIssuedInvoicesGridColumns( export function useIssuedInvoicesGridColumns(
actionHandlers: GridActionHandlers = {} actionHandlers: GridActionHandlers = {}
): ColumnDef<IssuedInvoiceSummaryData, unknown>[] { ): ColumnDef<IssuedInvoiceSummaryData, unknown>[] {
const { t } = useTranslation(); const { t } = useTranslation();
const { onDownloadPdf, onSendEmail } = actionHandlers;
return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>( return React.useMemo<ColumnDef<IssuedInvoiceSummaryData>[]>(
() => [ () => [
@ -101,7 +100,7 @@ export function useIssuedInvoicesGridColumns(
</a> </a>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="m-0 p-3"> <TooltipContent className="m-0 p-3">
<QRCode className="bg-white p-8" value={verifactu.url} /> <QrCode className="bg-white p-8" value={verifactu.url} />
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}
@ -132,15 +131,15 @@ export function useIssuedInvoicesGridColumns(
size: 140, size: 140,
minSize: 120, minSize: 120,
cell: ({ row }) => { cell: ({ row }) => {
const c = row.original.recipient; const r = row.original.recipient;
return ( return (
<div className="flex items-start gap-1"> <div className="flex items-start gap-1">
<div className="min-w-0 grid gap-1"> <div className="min-w-0 grid gap-1">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<span className="font-semibold truncate text-primary">{c.name}</span> <span className="font-semibold truncate text-primary">{r.name}</span>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{c.tin && <span className="font-base truncate">{c.tin}</span>} {r.tin && <span className="font-base truncate">{r.tin}</span>}
</div> </div>
</div> </div>
</div> </div>
@ -321,7 +320,7 @@ export function useIssuedInvoicesGridColumns(
size: 64, size: 64,
minSize: 64, minSize: 64,
cell: ({ row }) => { cell: ({ row }) => {
const proforma = row.original; const issuedInvoice = row.original;
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation(); const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
return ( return (
@ -333,8 +332,8 @@ export function useIssuedInvoicesGridColumns(
aria-label={t("common.download_pdf")} aria-label={t("common.download_pdf")}
className="cursor-pointer text-muted-foreground hover:text-primary" className="cursor-pointer text-muted-foreground hover:text-primary"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); stop(e);
onDownloadPdf?.(proforma); actionHandlers.onDownloadPdf?.(issuedInvoice);
}} }}
size="icon-sm" size="icon-sm"
type="button" type="button"
@ -366,14 +365,14 @@ export function useIssuedInvoicesGridColumns(
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem <DropdownMenuItem
className="cursor-pointer" className="cursor-pointer"
onClick={() => onDownloadPdf?.(proforma)} onClick={() => actionHandlers.onDownloadPdf?.(issuedInvoice)}
> >
<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" className="cursor-pointer"
onClick={() => onSendEmail?.(proforma)} onClick={() => actionHandlers.onSendEmail?.(issuedInvoice)}
> >
<MailIcon className="mr-2 size-4" /> <MailIcon className="mr-2 size-4" />
{t("common.send_email")} {t("common.send_email")}
@ -389,6 +388,6 @@ export function useIssuedInvoicesGridColumns(
}, },
}, },
], ],
[t, onDownloadPdf, onSendEmail] [t, actionHandlers]
); );
} }

View File

@ -0,0 +1 @@
export * from "./pages";

View File

@ -0,0 +1 @@
export * from "./issued-invoice-list-page";

View File

@ -0,0 +1,123 @@
import { PageHeader, SimpleSearchInput } from "@erp/core/components";
import { ErrorAlert } from "@erp/customers/components";
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
import {
Alert,
AlertDescription,
AlertTitle,
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn-ui/components";
import { FilterIcon, LockIcon, PlusIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../../i18n";
import { useIssuedInvoiceListPageController } from "../../controllers";
import { IssuedInvoicesGrid, useIssuedInvoicesGridColumns } from "../blocks";
export const IssuedInvoiceListPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { listCtrl, handleDownloadPDF } = useIssuedInvoiceListPageController();
const columns = useIssuedInvoicesGridColumns({
onDownloadPdf: handleDownloadPDF,
onSendEmail: () => null,
});
// Hook con Sheet de shadcn
/*const preview = usePinnedPreviewSheet<IssuedInvoiceSummaryPageData>({
persistKey: "invoice-preview-pin",
widthClass: "w-[500px]",
});*/
if (listCtrl.isError || !listCtrl.data) {
return (
<AppContent>
<ErrorAlert
message={(listCtrl.error as Error)?.message || "Error al cargar el listado"}
title={t("pages.issued_invoices.list.loadErrorTitle")}
/>
<BackHistoryButton />
</AppContent>
);
}
return (
<>
<AppHeader>
<PageHeader
description={t("pages.issued_invoices.list.description")}
rightSlot={
<Button
aria-label={t("pages.issued_invoices.create.title")}
onClick={() => navigate("/issued-invoices/create")}
>
<PlusIcon aria-hidden className="mr-2 size-4" />
{t("pages.issued_invoices.create.title")}
</Button>
}
title={t("pages.issued_invoices.list.title")}
/>
</AppHeader>
<AppContent>
<Alert className="bg-green-50 text-green-800">
<LockIcon />
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
<AlertDescription className="text-green-800">
Las facturas de esta pantalla son de solo lectura. Se generan automáticamente al emitir
una proforma aprobada y no se pueden modificar ni eliminar. Solo puedes descargar el PDF
de cada factura.
</AlertDescription>
</Alert>
{/* Search and filters */}
<div className="flex items-center justify-between gap-16">
<SimpleSearchInput
loading={listCtrl.isLoading}
onSearchChange={listCtrl.setSearchValue}
/>
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
<SelectTrigger className="w-full sm:w-48">
<FilterIcon aria-hidden className="mr-2 size-4" />
<SelectValue placeholder={t("filters.status")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t("catalog.issuedInvoices.status.all.label")}</SelectItem>
<SelectItem value="draft">
{t("catalog.issuedInvoices.status.draft.label")}
</SelectItem>
<SelectItem value="sent">{t("catalog.issuedInvoices.status.sent.label")}</SelectItem>
<SelectItem value="approved">
{t("catalog.issuedInvoices.status.approved.label")}
</SelectItem>
<SelectItem value="rejected">
{t("catalog.issuedInvoices.status.rejected.label")}
</SelectItem>
<SelectItem value="issued">
{t("catalog.issuedInvoices.status.issued.label")}
</SelectItem>
</SelectContent>
</Select>
</div>
<IssuedInvoicesGrid
columns={columns}
data={listCtrl.data}
loading={listCtrl.isLoading}
onPageChange={listCtrl.setPageIndex}
onPageSizeChange={listCtrl.setPageSize}
// acciones rápidas del grid → page controller
//onRowClick={(id) => navigate(`/issuedInvoices/${id}`)}
pageIndex={listCtrl.pageIndex}
pageSize={listCtrl.pageSize}
/>
</AppContent>
</>
);
};

View File

@ -1,2 +1,3 @@
export * from "./use-issued-invoices-grid-columns"; export * from "../../../list/ui/blocks/issued-invoices-grid/use-issued-invoices-grid-columns";
export * from "./use-issued-invoices-list"; export * from "./use-issued-invoices-list";

View File

@ -1,61 +0,0 @@
import { PageHeader } from "@erp/core/components";
import { ErrorAlert } from "@erp/customers/components";
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../i18n";
import { useIssuedInvoicesList } from "./hooks";
import { IssuedInvoicesGrid } from "./ui/issued-invoices-grid";
export const IssuedInvoiceListPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const list = useIssuedInvoicesList();
if (list.isError || !list.data) {
return (
<AppContent>
<ErrorAlert
message={(list.error as Error)?.message || "Error al cargar el listado"}
title={t("pages.issued_invoices.list.loadErrorTitle")}
/>
<BackHistoryButton />
</AppContent>
);
}
return (
<>
<AppHeader>
<PageHeader
description={t("pages.issued_invoices.list.description")}
rightSlot={
<Button
aria-label={t("pages.issued_invoices.create.title")}
onClick={() => navigate("/issued-invoices/create")}
>
<PlusIcon aria-hidden className="mr-2 size-4" />
{t("pages.issued_invoices.create.title")}
</Button>
}
title={t("pages.issued_invoices.list.title")}
/>
</AppHeader>
<AppContent>
<IssuedInvoicesGrid
data={list.data}
loading={list.isLoading}
onPageChange={list.setPageIndex}
onPageSizeChange={list.setPageSize}
onSearchChange={list.setSearchValue}
pageIndex={list.pageIndex}
pageSize={list.pageSize}
searchValue={list.search}
/>
</AppContent>
</>
);
};

View File

@ -138,7 +138,7 @@ export function useProformasGridColumns(
return ( return (
<div> <div>
<a <a
className="text-primary hover:underline" className="text-primary hover:underline font-semibold"
href={`/customers/${proforma.customer_id}`} href={`/customers/${proforma.customer_id}`}
> >
{proforma.recipient.name} {proforma.recipient.name}