feat: add AFIP Factura C issuance support to ArcaCredentials and update cash flow model

This commit is contained in:
2026-08-07 14:23:47 -03:00
parent a93681357f
commit ebd8096112
9 changed files with 1332 additions and 94 deletions
@@ -26,6 +26,41 @@ export type ArcaWsfeLastVoucherResult = {
lastVoucherNumber: number;
};
export type CreateInvoiceFromCashMovementParams = {
cashMovementId: string;
billingDate: string | Date;
paymentMethod: string;
description: string;
sessionUser: string;
};
export type FindInvoicesParams = {
companyId?: string;
cashMovementId?: string;
cashMovementIds?: string[];
sessionUser: string;
};
export type InvoiceStatus = "PENDING_ARCA" | "APPROVED" | "PENDING_VERIFICATION" | "REJECTED" | "FAILED";
export type Invoice = {
id?: string;
cashMovementId: string;
companyId: string;
amount: number;
type: "FACTURA_C";
status: InvoiceStatus;
billingDate: Date;
paymentMethod: string;
description: string;
cae?: string;
caeExpiresAt?: Date;
pointOfSale?: number;
voucherNumber?: number;
voucherType?: string;
arcaErrorMessage?: string;
};
const schemaCreateCashFlowMovement = Yup.object().shape({
paymentMethod: Yup.string().required("Debe introducir el tipo de movimiento."),
companyId: Yup.string().required("La organización no es valida."),
@@ -91,3 +126,13 @@ export const getArcaWsfeLastVoucher = async (
): Promise<ArcaWsfeLastVoucherResult> => {
return ApiRequest.post<ArcaWsfeLastVoucherResult>("arca-credentials/wsfe-last-voucher", data);
};
export const createInvoiceFromCashMovement = async (
data: CreateInvoiceFromCashMovementParams
): Promise<Invoice> => {
return ApiRequest.post<Invoice>("invoices/create-from-cash-movement", data);
};
export const findInvoices = async (data: FindInvoicesParams): Promise<Invoice[]> => {
return ApiRequest.post<Invoice[]>("invoices/find", data);
};
@@ -0,0 +1,368 @@
.movementsPage {
padding: 16px;
padding-bottom: 32px;
}
.movementsList {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.movementCard {
border: 1px solid var(--gray-light);
border-radius: 18px;
background: linear-gradient(135deg, var(--white) 0%, #f9fafb 100%);
box-shadow: 0 14px 34px rgba(15, 23, 42, 0.08);
overflow: hidden;
padding: 16px;
position: relative;
}
.movementCard::before {
background: radial-gradient(circle, rgba(255, 255, 255, 0.9) 0%, transparent 68%);
border-radius: 999px;
content: "";
height: 150px;
position: absolute;
right: -74px;
top: -80px;
width: 150px;
}
.movementCardIcon {
color: var(--movement-card-accent, var(--gray-dark));
height: 74px;
opacity: 0.2;
pointer-events: none;
position: absolute;
right: 24px;
bottom: 14px;
width: 74px;
z-index: 0;
}
.movementContent {
position: relative;
z-index: 1;
}
.movementCardCash {
--movement-card-accent: #16803f;
background: linear-gradient(135deg, #ffffff 0%, #f0fbf4 100%);
}
.movementCardBankTransfer {
--movement-card-accent: #2563eb;
background: linear-gradient(135deg, #ffffff 0%, #eef5ff 100%);
}
.movementCardCreditCard {
--movement-card-accent: #7c3aed;
background: linear-gradient(135deg, #ffffff 0%, #f5f0ff 100%);
}
.movementCardDebitCard {
--movement-card-accent: #0f766e;
background: linear-gradient(135deg, #ffffff 0%, #edfbf9 100%);
}
.movementCardClientAccount {
--movement-card-accent: #b45309;
background: linear-gradient(135deg, #ffffff 0%, #fff7ed 100%);
}
.movementCardOther {
--movement-card-accent: #64748b;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
}
.movementHeader {
display: flex;
flex-direction: column;
gap: 14px;
}
.movementSummary {
min-width: 0;
}
.movementDescription {
color: var(--black-light);
font-size: 16px;
line-height: 1.25;
margin: 0 0 6px;
}
.movementDate {
color: var(--gray-dark);
display: block;
font-size: 13px;
}
.movementAmount {
align-self: flex-start;
border-radius: 999px;
font-size: 18px;
font-weight: 800;
padding: 8px 12px;
}
.movementAmountPositive {
background: rgba(29, 141, 75, 0.12);
color: var(--green-darkestX2);
}
.movementAmountNegative {
background: rgba(188, 32, 42, 0.12);
color: var(--red);
}
.movementMeta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.movementMetaItem {
background: rgba(255, 255, 255, 0.72);
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.22);
color: var(--gray-dark);
font-size: 12px;
font-weight: 600;
backdrop-filter: blur(8px);
padding: 6px 10px;
}
.movementActionsSlot {
display: flex;
margin-top: 16px;
}
.movementInvoiceButton {
background: var(--movement-card-accent, var(--black-light));
border: 0;
border-radius: 999px;
color: var(--white);
cursor: pointer;
font-size: 13px;
font-weight: 700;
padding: 8px 14px;
}
.movementInvoiceButton:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.movementInvoiceBadge {
background: rgba(22, 128, 63, 0.12);
border: 1px solid rgba(22, 128, 63, 0.24);
border-radius: 999px;
color: #166534;
font-size: 12px;
font-weight: 700;
padding: 7px 10px;
}
.emptyState {
background: var(--white);
border: 1px dashed var(--gray-light);
border-radius: 18px;
color: var(--gray-dark);
padding: 28px 16px;
text-align: center;
}
.paginationWrapper {
display: flex;
justify-content: center;
margin-top: 18px;
}
.invoiceModalBackdrop {
align-items: flex-end;
background: rgba(15, 23, 42, 0.48);
display: flex;
inset: 0;
justify-content: center;
padding: 14px;
position: fixed;
z-index: 1000;
}
.invoiceModal {
background: var(--white);
border-radius: 24px 24px 18px 18px;
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.28);
max-height: calc(100vh - 28px);
overflow: auto;
padding: 18px;
width: 100%;
}
.invoiceModalHeader {
align-items: flex-start;
display: flex;
gap: 12px;
justify-content: space-between;
margin-bottom: 16px;
}
.invoiceModalHeader h2 {
color: var(--black-light);
font-size: 20px;
line-height: 1.2;
margin: 0;
}
.invoiceModalClose {
align-items: center;
background: #f1f5f9;
border: 0;
border-radius: 999px;
color: var(--gray-dark);
cursor: pointer;
display: flex;
font-size: 24px;
height: 36px;
justify-content: center;
line-height: 1;
width: 36px;
}
.invoiceAmountBox {
background: linear-gradient(135deg, #f0fbf4 0%, #ffffff 100%);
border: 1px solid rgba(22, 128, 63, 0.16);
border-radius: 18px;
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 16px;
padding: 14px;
}
.invoiceAmountBox span {
color: var(--gray-dark);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
.invoiceAmountBox strong {
color: var(--green-darkestX2);
font-size: 22px;
}
.invoiceField {
color: var(--black-light);
display: flex;
flex-direction: column;
font-size: 13px;
font-weight: 700;
gap: 7px;
margin-bottom: 14px;
}
.invoiceField input,
.invoiceField select,
.invoiceField textarea {
background: var(--white);
border: 1px solid var(--gray-light);
border-radius: 14px;
color: var(--black-light);
font: inherit;
font-weight: 500;
padding: 11px 12px;
width: 100%;
}
.invoiceField textarea {
min-height: 104px;
resize: vertical;
}
.invoiceModalActions {
display: flex;
flex-direction: column-reverse;
gap: 10px;
margin-top: 18px;
}
.invoiceCancelButton,
.invoiceConfirmButton {
border: 0;
border-radius: 999px;
cursor: pointer;
font-size: 14px;
font-weight: 800;
padding: 12px 16px;
}
.invoiceCancelButton {
background: #f1f5f9;
color: var(--gray-dark);
}
.invoiceConfirmButton {
background: var(--green-darkestX2);
color: var(--white);
}
.invoiceCancelButton:disabled,
.invoiceConfirmButton:disabled,
.invoiceModalClose:disabled {
cursor: not-allowed;
opacity: 0.55;
}
@media (min-width: 768px) {
.movementsPage {
padding: 22px;
}
.movementsList {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.movementHeader {
align-items: flex-start;
flex-direction: row;
justify-content: space-between;
}
.movementAmount {
align-self: auto;
text-align: right;
white-space: nowrap;
}
.movementCardIcon {
height: 82px;
right: 28px;
width: 82px;
}
.invoiceModalBackdrop {
align-items: center;
}
.invoiceModal {
border-radius: 24px;
max-width: 520px;
}
.invoiceModalActions {
flex-direction: row;
justify-content: flex-end;
}
}
@media (min-width: 1200px) {
.movementsList {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@@ -7,7 +7,11 @@ import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountBalanceOutlinedIcon from "@mui/icons-material/AccountBalanceOutlined";
import AccountMovementsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import DataGrid, { DataGridColumn } from "@core/app/components/DataGrid/DataGrid";
import AccountBalanceWalletOutlinedIcon from "@mui/icons-material/AccountBalanceWalletOutlined";
import CreditCardOutlinedIcon from "@mui/icons-material/CreditCardOutlined";
import MoreHorizOutlinedIcon from "@mui/icons-material/MoreHorizOutlined";
import PaymentOutlinedIcon from "@mui/icons-material/PaymentOutlined";
import PointOfSaleOutlinedIcon from "@mui/icons-material/PointOfSaleOutlined";
import { formatPrice } from "@core/app/helpers/Numbers";
import dayjs from "dayjs";
import {
@@ -15,7 +19,7 @@ import {
PaginateCashFlowParams,
PaginateCashFlowResults,
} from "@core/Models/CashFlow.model";
import { cashFlowPaginate } from "../Cash.Service";
import { cashFlowPaginate, createInvoiceFromCashMovement, findInvoices, Invoice } from "../Cash.Service";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import Filterable from "@core/app/components/Filterable/Filterable";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
@@ -26,7 +30,18 @@ import { useFilterStore } from "@core/Store/Filter.Store";
import { FindEmployeesParams } from "@core/Models/Collaborators.model";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import { FindClientsParams } from "@core/Models/Clients.model";
import { paymentMethods, PAYMENT_METHODS } from "@core/Models/Payments.model";
import {
paymentMethods,
PAYMENT_METHODS,
PAYMENT_METHODS_CLIENT_ACCOUNT,
} from "@core/Models/Payments.model";
import {
CLIENT_ACCOUNT_MOVEMENT_TYPES,
CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "@core/Models/ClientAccountMovements.model";
import PaginationControls from "@components/PaginationControls/PaginationControls";
import { useAlert } from "@core/Store/Alert.Store";
import style from "./page.module.css";
type UrlData = {
id: string;
@@ -161,6 +176,101 @@ const FilterContent = (props: FilterContentProps) => {
);
};
const movementTypeLabels: Record<string, string> = {
deposit: "Ingreso",
withdrawal: "Egreso",
};
const getPaymentMethodName = (paymentMethod: string) => {
return paymentMethods.find((method) => method.id === paymentMethod)?.name || paymentMethod;
};
const getMovementId = (movement: ICashFlow) => movement.id || movement._id || "";
const todayInputValue = () => dayjs().format("YYYY-MM-DD");
const invoicePaymentMethodOptions = [
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH, name: "Efectivo" },
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.BANK_TRANSFER, name: "Transferencia bancaria" },
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.CREDIT_CARD, name: "Otros medios de pago electrónicos" },
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.DEBIT_CARD, name: "Tarjeta de débito" },
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.OTHER, name: "Otros" },
];
const isInvoicePaymentMethod = (paymentMethod?: string) => {
return invoicePaymentMethodOptions.some((option) => option.id === paymentMethod);
};
const getInvoicePaymentMethod = (paymentMethod?: string) => {
if (paymentMethod && isInvoicePaymentMethod(paymentMethod)) {
return paymentMethod;
}
return CLIENT_ACCOUNT_PAYMENT_METHODS.CASH;
};
type InvoiceDraft = {
movement: ICashFlow;
movementId: string;
billingDate: string;
paymentMethod: string;
description: string;
};
const getPaymentMethodCardVisual = (paymentMethod?: PAYMENT_METHODS | string) => {
switch (paymentMethod) {
case CLIENT_ACCOUNT_PAYMENT_METHODS.CASH:
return {
Icon: PointOfSaleOutlinedIcon,
className: style.movementCardCash,
};
case CLIENT_ACCOUNT_PAYMENT_METHODS.BANK_TRANSFER:
return {
Icon: AccountBalanceOutlinedIcon,
className: style.movementCardBankTransfer,
};
case CLIENT_ACCOUNT_PAYMENT_METHODS.CREDIT_CARD:
return {
Icon: CreditCardOutlinedIcon,
className: style.movementCardCreditCard,
};
case CLIENT_ACCOUNT_PAYMENT_METHODS.DEBIT_CARD:
return {
Icon: PaymentOutlinedIcon,
className: style.movementCardDebitCard,
};
case PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT:
return {
Icon: AccountBalanceWalletOutlinedIcon,
className: style.movementCardClientAccount,
};
default:
return {
Icon: MoreHorizOutlinedIcon,
className: style.movementCardOther,
};
}
};
const getInvoiceStatusMessage = (invoice: Invoice) => {
const voucher = invoice.pointOfSale && invoice.voucherNumber
? ` Comprobante intentado: ${invoice.pointOfSale}-${invoice.voucherNumber}.`
: "";
switch (invoice.status) {
case "APPROVED":
return "Factura emitida correctamente.";
case "PENDING_ARCA":
case "PENDING_VERIFICATION":
return `${invoice.arcaErrorMessage || "La factura quedó pendiente de verificación en ARCA."}${voucher}`;
case "REJECTED":
case "FAILED":
return invoice.arcaErrorMessage || "ARCA no autorizó la factura.";
default:
return "No se pudo confirmar el estado de la factura.";
}
};
export default function CashFlowMovementsList() {
const data = useParams<UrlData>();
const id = data.id;
@@ -168,6 +278,7 @@ export default function CashFlowMovementsList() {
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
const filter = useFilterStore();
const [clientFilterData, setClientFilterData] = useState<TextObjectFilterResult[]>([]);
@@ -181,52 +292,6 @@ export default function CashFlowMovementsList() {
const [methodFilterData, setMethodFilterData] = useState<TextObjectFilterResult[]>([]);
const [methodId, setMethodId] = useState<string>("");
const [dataGridColumns] = useState<DataGridColumn<ICashFlow>[]>([
{
field: "description",
title: "Detalle",
width: null,
render: (value: ICashFlow) => (
<>
<div
style={{
paddingLeft: "10px",
paddingRight: "10px",
paddingTop: "10px",
}}
>
{value.description}
</div>
<div
style={{
paddingLeft: "10px",
paddingRight: "10px",
color: "var(--gray-dark)",
}}
>
{dayjs(value.createdAt).format("DD MMMM YYYY - HH:mm")}
</div>
</>
),
},
{
field: "amount",
title: "Importe",
width: 150,
render: (value: ICashFlow) => (
<div
style={{
padding: "10px",
textAlign: "right",
color: value.amount > 0 ? "var(--green-darkestX2)" : "var(--red)",
}}
>
{formatPrice(value.amount)}
</div>
),
},
]);
const [pageSize] = useState<number>(
parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
@@ -235,6 +300,10 @@ export default function CashFlowMovementsList() {
)
);
const [movements, setMovements] = useState<PaginateCashFlowResults>();
const [invoicesByMovementId, setInvoicesByMovementId] = useState<Record<string, Invoice>>({});
const [invoicingMovementId, setInvoicingMovementId] = useState<string>("");
const [invoiceDraft, setInvoiceDraft] = useState<InvoiceDraft | null>(null);
const [invoiceDraftsByMovementId, setInvoiceDraftsByMovementId] = useState<Record<string, InvoiceDraft>>({});
const movementsRef = useRef(movements);
const loadMovements = (page: number = 1, pageSize: number = 30) => {
@@ -262,6 +331,20 @@ export default function CashFlowMovementsList() {
cashFlowPaginate(filterData)
.then((res) => {
setMovements(res);
const movementIds = res.data
.map((movement) => getMovementId(movement))
.filter((movementId): movementId is string => Boolean(movementId));
if (movementIds.length > 0) {
return findInvoices({ companyId: id, cashMovementIds: movementIds, sessionUser: SessionInfo.userId }).then((invoices) => {
setInvoicesByMovementId((current) => ({
...current,
...Object.fromEntries(
invoices.map((invoice) => [String(invoice.cashMovementId), invoice])
),
}));
}).catch(() => undefined);
}
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
@@ -345,6 +428,132 @@ export default function CashFlowMovementsList() {
loadMovements(loadPage, pageSize);
}, [clientId, collaboratorId, methodId]);
const renderMovementMeta = (movement: ICashFlow) => {
const details = [
movement.paymentMethod ? getPaymentMethodName(movement.paymentMethod) : null,
movement.type ? movementTypeLabels[movement.type] || movement.type : null,
movement.reference ? `Ref. ${movement.reference}` : null,
].filter((detail): detail is string => Boolean(detail));
if (details.length === 0) {
return null;
}
return (
<div className={style.movementMeta}>
{details.map((detail) => (
<span key={detail} className={style.movementMetaItem}>
{detail}
</span>
))}
</div>
);
};
const canInvoiceMovement = (movement: ICashFlow) => {
return movement.type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT && movement.amount > 0;
};
const openInvoiceModal = (movement: ICashFlow) => {
const movementId = getMovementId(movement);
if (!canInvoiceMovement(movement) || !movementId || invoicingMovementId) {
return;
}
const savedDraft = invoiceDraftsByMovementId[movementId];
setInvoiceDraft(savedDraft ? { ...savedDraft, movement } : {
movement,
movementId,
billingDate: dayjs(movement.createdAt).format("YYYY-MM-DD"),
paymentMethod: getInvoicePaymentMethod(movement.paymentMethod),
description: movement.description || "",
});
};
const closeInvoiceModal = () => {
if (!invoicingMovementId) {
setInvoiceDraftsByMovementId((current) => {
if (!invoiceDraft) {
return current;
}
const { [invoiceDraft.movementId]: _discarded, ...remaining } = current;
return remaining;
});
setInvoiceDraft(null);
}
};
const handleInvoiceMovement = () => {
if (!invoiceDraft || invoicingMovementId) {
return;
}
if (!invoiceDraft.billingDate) {
alert.showError("Debe seleccionar la fecha del comprobante.");
return;
}
if (!invoiceDraft.paymentMethod) {
alert.showError("Debe seleccionar el medio de pago.");
return;
}
if (!invoiceDraft.description.trim()) {
alert.showError("Debe ingresar el detalle de la factura.");
return;
}
setInvoicingMovementId(invoiceDraft.movementId);
eventHandler.setEventType(EVENT_TYPES.LOADING);
createInvoiceFromCashMovement({
cashMovementId: invoiceDraft.movementId,
billingDate: invoiceDraft.billingDate,
paymentMethod: invoiceDraft.paymentMethod,
description: invoiceDraft.description.trim(),
sessionUser: SessionInfo.userId,
})
.then((invoice) => {
setInvoicesByMovementId((current) => ({
...current,
[invoiceDraft.movementId]: invoice,
}));
if (invoice.status === "APPROVED") {
setInvoiceDraftsByMovementId((current) => {
const { [invoiceDraft.movementId]: _discarded, ...remaining } = current;
return remaining;
});
alert.showSuccess(getInvoiceStatusMessage(invoice));
return;
}
if (invoice.status === "REJECTED" || invoice.status === "FAILED") {
setInvoiceDraftsByMovementId((current) => ({
...current,
[invoiceDraft.movementId]: invoiceDraft,
}));
}
alert.showError(getInvoiceStatusMessage(invoice));
})
.catch((error: Error) => {
setInvoiceDraftsByMovementId((current) => ({
...current,
[invoiceDraft.movementId]: invoiceDraft,
}));
alert.showError(error.message || "No se pudo emitir la factura.");
})
.finally(() => {
setInvoicingMovementId("");
setInvoiceDraft(null);
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
return (
<>
<Location />
@@ -352,28 +561,190 @@ export default function CashFlowMovementsList() {
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<>
<div
style={{
position: "absolute",
top: "80px",
left: "0px",
right: "0px",
bottom: "30px",
}}
>
<DataGrid
columns={dataGridColumns}
data={movements ? movements.data : []}
pagination={true}
page={movements?.page}
pages={movements?.pages}
pageSize={pageSize}
handleRefresh={loadMovements}
handleRowClick={() => { }}
/>
<div className={style.movementsPage}>
<section className={style.movementsList} aria-label="Movimientos de caja">
{movements && movements.data.length === 0 && (
<div className={style.emptyState}>
No se encontraron movimientos con los filtros aplicados.
</div>
)}
{(movements ? movements.data : []).map((movement, index) => {
const visual = getPaymentMethodCardVisual(movement.paymentMethod);
const PaymentMethodIcon = visual.Icon;
const movementId = getMovementId(movement);
const movementKey = movementId || `${movement.createdAt}-${movement.reference}-${index}`;
const invoice = movementId ? invoicesByMovementId[movementId] : undefined;
const isInvoiced = invoice?.status === "APPROVED";
const isPendingInvoice = invoice?.status === "PENDING_ARCA" || invoice?.status === "PENDING_VERIFICATION";
return (
<article
key={movementKey}
className={`${style.movementCard} ${visual.className}`}
>
<PaymentMethodIcon
className={style.movementCardIcon}
aria-hidden="true"
/>
<div className={style.movementContent}>
<div className={style.movementHeader}>
<div className={style.movementSummary}>
<h3 className={style.movementDescription}>
{movement.description}
</h3>
<time className={style.movementDate}>
{dayjs(movement.createdAt).format("DD MMMM YYYY - HH:mm")}
</time>
</div>
<div
className={`${style.movementAmount} ${
movement.amount > 0
? style.movementAmountPositive
: style.movementAmountNegative
}`}
>
{formatPrice(movement.amount)}
</div>
</div>
{renderMovementMeta(movement)}
<div className={style.movementActionsSlot}>
{isInvoiced ? (
<span className={style.movementInvoiceBadge}>
Factura C {invoice?.pointOfSale}-{invoice?.voucherNumber}
</span>
) : isPendingInvoice ? (
<span className={style.movementInvoiceBadge} title={getInvoiceStatusMessage(invoice)}>
Factura pendiente
</span>
) : !canInvoiceMovement(movement) ? null : (
<button
type="button"
className={style.movementInvoiceButton}
disabled={invoicingMovementId === movementId}
onClick={() => openInvoiceModal(movement)}
>
{invoicingMovementId === movementId ? "Facturando..." : "Facturar"}
</button>
)}
</div>
</div>
</article>
);
})}
</section>
{movements && movements.pages > 1 && (
<div className={style.paginationWrapper}>
<PaginationControls
current={movements.page}
size={pageSize}
max={movements.pages}
onChange={loadMovements}
/>
</div>
)}
</div>
<div style={{ marginTop: "15px" }}></div>
{invoiceDraft && (
<div className={style.invoiceModalBackdrop} role="presentation">
<section
className={style.invoiceModal}
role="dialog"
aria-modal="true"
aria-labelledby="invoice-modal-title"
>
<div className={style.invoiceModalHeader}>
<h2 id="invoice-modal-title">Emitir Factura C</h2>
<button
type="button"
className={style.invoiceModalClose}
onClick={closeInvoiceModal}
disabled={Boolean(invoicingMovementId)}
aria-label="Cerrar"
>
×
</button>
</div>
<div className={style.invoiceAmountBox}>
<span>Importe</span>
<strong>{formatPrice(invoiceDraft.movement.amount)}</strong>
</div>
<label className={style.invoiceField}>
<span>Fecha de comprobante</span>
<input
type="date"
value={invoiceDraft.billingDate}
max={todayInputValue()}
onChange={(event) =>
setInvoiceDraft((current) =>
current ? { ...current, billingDate: event.target.value } : current
)
}
disabled={Boolean(invoicingMovementId)}
/>
</label>
<label className={style.invoiceField}>
<span>Medio de pago</span>
<select
value={invoiceDraft.paymentMethod}
onChange={(event) =>
setInvoiceDraft((current) =>
current ? { ...current, paymentMethod: event.target.value } : current
)
}
disabled={Boolean(invoicingMovementId)}
>
{invoicePaymentMethodOptions.map((option) => (
<option key={option.id} value={option.id}>
{option.name}
</option>
))}
</select>
</label>
<label className={style.invoiceField}>
<span>Detalle</span>
<textarea
value={invoiceDraft.description}
rows={4}
onChange={(event) =>
setInvoiceDraft((current) =>
current ? { ...current, description: event.target.value } : current
)
}
disabled={Boolean(invoicingMovementId)}
/>
</label>
<div className={style.invoiceModalActions}>
<button
type="button"
className={style.invoiceCancelButton}
onClick={closeInvoiceModal}
disabled={Boolean(invoicingMovementId)}
>
Cancelar
</button>
<button
type="button"
className={style.invoiceConfirmButton}
onClick={handleInvoiceMovement}
disabled={Boolean(invoicingMovementId)}
>
{invoicingMovementId ? "Facturando..." : "Emitir factura"}
</button>
</div>
</section>
</div>
)}
</>
</SecureByPlan>
</>