Facturas de cliente

This commit is contained in:
David Arranz 2025-11-22 18:22:12 +01:00
parent 323860b5b8
commit 7513553f49
8 changed files with 82 additions and 48 deletions

View File

@ -11,7 +11,7 @@ interface ChangeStatusDialogState {
}
export function useChangeStatusDialogController() {
const { changeStatus } = useChangeProformaStatus();
const { changeStatus, isPending } = useChangeProformaStatus();
const [state, setState] = React.useState<ChangeStatusDialogState>({
open: false,
@ -31,10 +31,6 @@ export function useChangeStatusDialogController() {
setState((s) => ({ ...s, open: false }));
};
const setTargetStatus = (status: string | null) => {
setState((s) => ({ ...s, targetStatus: status }));
};
const confirmChangeStatus = async (status: PROFORMA_STATUS) => {
if (!state.proformas.length) return;

View File

@ -1,6 +1,6 @@
import type { IDataSource } from "@erp/core/client";
export interface IssueInvoiceResponse {
export interface IssueProformaInvoiceResponse {
invoiceId: string;
proformaId: string;
customerId: string;
@ -9,8 +9,8 @@ export interface IssueInvoiceResponse {
export async function issueProformaInvoiceApi(
dataSource: IDataSource,
proformaId: string
): Promise<IssueInvoiceResponse> {
return dataSource.custom<IssueInvoiceResponse>({
): Promise<IssueProformaInvoiceResponse> {
return dataSource.custom<IssueProformaInvoiceResponse>({
path: `proformas/${proformaId}/issue`,
method: "put",
data: {},

View File

@ -1,3 +1,4 @@
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import * as React from "react";
import type { ProformaSummaryData } from "../../types";
@ -6,19 +7,21 @@ import { useIssueProformaMutation } from "../hooks/use-issue-proforma-mutation";
interface State {
open: boolean;
proforma: ProformaSummaryData | null;
loading: boolean;
}
export function useProformaIssueDialogController() {
const { mutate, isPending } = useIssueProformaMutation();
const { issueProforma, isPending } = useIssueProformaMutation();
const [state, setState] = React.useState<State>({
open: false,
proforma: null,
loading: false,
});
// abrir diálogo
const openDialog = (p: ProformaSummaryData) => {
setState({ open: true, proforma: p });
const openDialog = (proforma: ProformaSummaryData) => {
setState({ open: true, proforma: proforma, loading: false });
};
// cerrar diálogo
@ -27,17 +30,23 @@ export function useProformaIssueDialogController() {
};
// confirmar emisión
const confirmIssue = () => {
const confirmIssue = async () => {
if (!state.proforma) return;
mutate(
{ proformaId: state.proforma.id },
{
onSuccess() {
closeDialog();
},
}
);
setState((s) => ({ ...s, loading: true }));
await issueProforma(state.proforma.id, {
onSuccess: () => {
showSuccessToast("Proforma emitida a factura");
},
onError: (err: unknown) => {
const error = err as Error;
showErrorToast("Error al emitir la proforma a factura", error.message);
},
});
setState((s) => ({ ...s, loading: false }));
closeDialog();
};
return {

View File

@ -0,0 +1 @@
export * from "./use-issue-proforma-mutation";

View File

@ -1,24 +1,24 @@
import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
import { issueProformaInvoiceApi } from "../api";
import { type IssueProformaInvoiceResponse, issueProformaInvoiceApi } from "../api";
interface IssueProformaPayload {
proformaId: string;
}
interface IssueProformaResponse {
proformaId: string;
success: boolean;
interface IssueProformaOptions {
onSuccess?: () => void;
onError?: (err: unknown) => void;
onLoadingChange?: (loading: boolean) => void;
}
export function useIssueProformaMutation() {
const dataSource = useDataSource();
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ proformaId }: IssueProformaPayload) =>
issueProformaInvoiceApi(dataSource, proformaId),
const mutation = useMutation<IssueProformaInvoiceResponse, DefaultError, IssueProformaPayload>({
mutationFn: ({ proformaId }) => issueProformaInvoiceApi(dataSource, proformaId),
onSuccess(_data, _vars, _ctx) {
// Refresca el listado de proformas
@ -28,4 +28,21 @@ export function useIssueProformaMutation() {
queryClient.invalidateQueries({ queryKey: ["invoices"] });
},
});
async function issueProforma(proformaId: string, opts?: IssueProformaOptions) {
try {
opts?.onLoadingChange?.(true);
await mutation.mutateAsync({ proformaId });
opts?.onSuccess?.();
} catch (err) {
opts?.onError?.(err);
} finally {
opts?.onLoadingChange?.(false);
}
}
return {
issueProforma,
isPending: mutation.isPending,
};
}

View File

@ -9,31 +9,40 @@ import {
Spinner,
} from "@repo/shadcn-ui/components";
import { useProformaIssueDialogController } from "../controllers";
import { useTranslation } from "../../../i18n";
import type { ProformaSummaryData } from "../../types";
interface ProformaIssueDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
proformaId: string;
proformaReference: string;
proforma: ProformaSummaryData | null;
isSubmitting: boolean;
onConfirm: (proforma?: ProformaSummaryData) => void;
}
export function ProformaIssueDialog({
open,
onOpenChange,
proformaId,
proformaReference,
proforma,
isSubmitting,
onConfirm,
}: ProformaIssueDialogProps) {
const { issue, isSubmitting } = useProformaIssueDialogController();
const { t } = useTranslation();
return (
<AlertDialog onOpenChange={onOpenChange} open={open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Emitir factura</AlertDialogTitle>
<AlertDialogDescription>
¿Seguro que quieres emitir la factura desde la proforma{" "}
<strong>{proformaReference}</strong>? Esta acción es irreversible.
<AlertDialogTitle>Emitir a factura</AlertDialogTitle>
<AlertDialogDescription className="space-y-4">
<p>
¿Seguro que quieres emitir la factura desde la proforma{" "}
<strong>{proforma?.reference}</strong>?
</p>
<p>
Esta acción creará una nueva factura definitiva y la proforma pasará al estado
"Emitida", no pudiendo modificarse ni eliminarse posteriormente.
</p>
</AlertDialogDescription>
</AlertDialogHeader>
@ -42,17 +51,14 @@ export function ProformaIssueDialog({
Cancelar
</Button>
<Button
disabled={isSubmitting}
onClick={() => issue(proformaId, () => onOpenChange(false))}
>
<Button disabled={isSubmitting} onClick={() => onConfirm(proforma)}>
{isSubmitting ? (
<>
<Spinner className="mr-2 size-4" />
Emitiendo...
</>
) : (
"Emitir factura"
<>Emitir factura</>
)}
</Button>
</AlertDialogFooter>

View File

@ -93,13 +93,18 @@ export function useProformasGridColumns(
<ProformaStatusBadge status={proforma.status} />
{/* Enlace discreto a factura real */}
{isIssued && invoiceId && (
{isIssued && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button asChild className="size-6" size="icon" variant="ghost">
<Button
asChild
className="size-6 text-foreground hover:text-primary"
size="icon"
variant="ghost"
>
<a href={`/facturas/${invoiceId}`}>
<ExternalLinkIcon className="size-3 text-muted-foreground" />
<ExternalLinkIcon />
<span className="sr-only">Ver factura {invoiceId}</span>
</a>
</Button>

View File

@ -121,8 +121,7 @@ export const ProformaListPage = () => {
onConfirm={issueDialogCtrl.confirmIssue}
onOpenChange={(open) => !open && issueDialogCtrl.closeDialog()}
open={issueDialogCtrl.open}
proformaId={issueDialogCtrl.proforma?.id ?? 0}
proformaReference={issueDialogCtrl.proforma?.reference ?? ""}
proforma={issueDialogCtrl.proforma}
/>
{/* Cambiar estado */}
@ -141,6 +140,7 @@ export const ProformaListPage = () => {
onOpenChange={(open) => !open && deleteDialogCtrl.closeDialog()}
open={deleteDialogCtrl.open}
proformas={deleteDialogCtrl.proformas}
requireSecondConfirm={true}
/>
</AppContent>
</>