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

View File

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

View File

@ -1,3 +1,4 @@
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import * as React from "react"; import * as React from "react";
import type { ProformaSummaryData } from "../../types"; import type { ProformaSummaryData } from "../../types";
@ -6,19 +7,21 @@ import { useIssueProformaMutation } from "../hooks/use-issue-proforma-mutation";
interface State { interface State {
open: boolean; open: boolean;
proforma: ProformaSummaryData | null; proforma: ProformaSummaryData | null;
loading: boolean;
} }
export function useProformaIssueDialogController() { export function useProformaIssueDialogController() {
const { mutate, isPending } = useIssueProformaMutation(); const { issueProforma, isPending } = useIssueProformaMutation();
const [state, setState] = React.useState<State>({ const [state, setState] = React.useState<State>({
open: false, open: false,
proforma: null, proforma: null,
loading: false,
}); });
// abrir diálogo // abrir diálogo
const openDialog = (p: ProformaSummaryData) => { const openDialog = (proforma: ProformaSummaryData) => {
setState({ open: true, proforma: p }); setState({ open: true, proforma: proforma, loading: false });
}; };
// cerrar diálogo // cerrar diálogo
@ -27,17 +30,23 @@ export function useProformaIssueDialogController() {
}; };
// confirmar emisión // confirmar emisión
const confirmIssue = () => { const confirmIssue = async () => {
if (!state.proforma) return; if (!state.proforma) return;
mutate( setState((s) => ({ ...s, loading: true }));
{ proformaId: state.proforma.id },
{ await issueProforma(state.proforma.id, {
onSuccess() { onSuccess: () => {
closeDialog(); 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 { 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 { 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 { interface IssueProformaPayload {
proformaId: string; proformaId: string;
} }
interface IssueProformaResponse { interface IssueProformaOptions {
proformaId: string; onSuccess?: () => void;
success: boolean; onError?: (err: unknown) => void;
onLoadingChange?: (loading: boolean) => void;
} }
export function useIssueProformaMutation() { export function useIssueProformaMutation() {
const dataSource = useDataSource(); const dataSource = useDataSource();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ const mutation = useMutation<IssueProformaInvoiceResponse, DefaultError, IssueProformaPayload>({
mutationFn: ({ proformaId }: IssueProformaPayload) => mutationFn: ({ proformaId }) => issueProformaInvoiceApi(dataSource, proformaId),
issueProformaInvoiceApi(dataSource, proformaId),
onSuccess(_data, _vars, _ctx) { onSuccess(_data, _vars, _ctx) {
// Refresca el listado de proformas // Refresca el listado de proformas
@ -28,4 +28,21 @@ export function useIssueProformaMutation() {
queryClient.invalidateQueries({ queryKey: ["invoices"] }); 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, Spinner,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { useProformaIssueDialogController } from "../controllers"; import { useTranslation } from "../../../i18n";
import type { ProformaSummaryData } from "../../types";
interface ProformaIssueDialogProps { interface ProformaIssueDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
proformaId: string; proforma: ProformaSummaryData | null;
proformaReference: string; isSubmitting: boolean;
onConfirm: (proforma?: ProformaSummaryData) => void;
} }
export function ProformaIssueDialog({ export function ProformaIssueDialog({
open, open,
onOpenChange, onOpenChange,
proformaId, proforma,
proformaReference, isSubmitting,
onConfirm,
}: ProformaIssueDialogProps) { }: ProformaIssueDialogProps) {
const { issue, isSubmitting } = useProformaIssueDialogController(); const { t } = useTranslation();
return ( return (
<AlertDialog onOpenChange={onOpenChange} open={open}> <AlertDialog onOpenChange={onOpenChange} open={open}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Emitir factura</AlertDialogTitle> <AlertDialogTitle>Emitir a factura</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription className="space-y-4">
¿Seguro que quieres emitir la factura desde la proforma{" "} <p>
<strong>{proformaReference}</strong>? Esta acción es irreversible. ¿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> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
@ -42,17 +51,14 @@ export function ProformaIssueDialog({
Cancelar Cancelar
</Button> </Button>
<Button <Button disabled={isSubmitting} onClick={() => onConfirm(proforma)}>
disabled={isSubmitting}
onClick={() => issue(proformaId, () => onOpenChange(false))}
>
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Spinner className="mr-2 size-4" /> <Spinner className="mr-2 size-4" />
Emitiendo... Emitiendo...
</> </>
) : ( ) : (
"Emitir factura" <>Emitir factura</>
)} )}
</Button> </Button>
</AlertDialogFooter> </AlertDialogFooter>

View File

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

View File

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