Facturas de cliente
This commit is contained in:
parent
323860b5b8
commit
7513553f49
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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: {},
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-issue-proforma-mutation";
|
||||||
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user