Compare commits

..

No commits in common. "1e7f1863b095a4ef9bd265816a53db88b6eec7ab" and "323860b5b8e4771d8113d1e94aa009f213b5c936" have entirely different histories.

11 changed files with 55 additions and 89 deletions

View File

@ -128,9 +128,9 @@
"title": "Customer invoices", "title": "Customer invoices",
"description": "List all customer invoices", "description": "List all customer invoices",
"grid_columns": { "grid_columns": {
"series_invoice_number": "#", "series_invoice_number": "Serie & #",
"verifactu_status": "Status", "verifactu_status": "Status",
"verifactu_qr_code": "QR", "verifactu_qr_code": "QR Code",
"verifactu_url": "URL", "verifactu_url": "URL",
"invoice_number": "Inv. number", "invoice_number": "Inv. number",
"series": "Serie", "series": "Serie",

View File

@ -128,9 +128,9 @@
"title": "Facturas de cliente", "title": "Facturas de cliente",
"description": "Lista todas las facturas de cliente", "description": "Lista todas las facturas de cliente",
"grid_columns": { "grid_columns": {
"series_invoice_number": "#", "series_invoice_number": "Serie & #",
"verifactu_status": "Estado", "verifactu_status": "Estado",
"verifactu_qr_code": "QR", "verifactu_qr_code": "Código QR",
"verifactu_url": "URL", "verifactu_url": "URL",
"invoice_number": "Nº factura", "invoice_number": "Nº factura",
"series": "Serie", "series": "Serie",

View File

@ -50,9 +50,9 @@ export function useIssuedInvoicesGridColumns(
), ),
enableHiding: false, enableHiding: false,
enableSorting: false, enableSorting: false,
maxSize: 96, maxSize: 64,
size: 96, size: 64,
minSize: 96, minSize: 64,
meta: { meta: {
title: t("pages.issued_invoices.list.grid_columns.series_invoice_number"), title: t("pages.issued_invoices.list.grid_columns.series_invoice_number"),
}, },

View File

@ -11,7 +11,7 @@ interface ChangeStatusDialogState {
} }
export function useChangeStatusDialogController() { export function useChangeStatusDialogController() {
const { changeStatus, isPending } = useChangeProformaStatus(); const { changeStatus } = useChangeProformaStatus();
const [state, setState] = React.useState<ChangeStatusDialogState>({ const [state, setState] = React.useState<ChangeStatusDialogState>({
open: false, open: false,
@ -31,6 +31,10 @@ 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 IssueProformaInvoiceResponse { export interface IssueInvoiceResponse {
invoiceId: string; invoiceId: string;
proformaId: string; proformaId: string;
customerId: string; customerId: string;
@ -9,8 +9,8 @@ export interface IssueProformaInvoiceResponse {
export async function issueProformaInvoiceApi( export async function issueProformaInvoiceApi(
dataSource: IDataSource, dataSource: IDataSource,
proformaId: string proformaId: string
): Promise<IssueProformaInvoiceResponse> { ): Promise<IssueInvoiceResponse> {
return dataSource.custom<IssueProformaInvoiceResponse>({ return dataSource.custom<IssueInvoiceResponse>({
path: `proformas/${proformaId}/issue`, path: `proformas/${proformaId}/issue`,
method: "put", method: "put",
data: {}, data: {},

View File

@ -1,4 +1,3 @@
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";
@ -7,21 +6,19 @@ 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 { issueProforma, isPending } = useIssueProformaMutation(); const { mutate, 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 = (proforma: ProformaSummaryData) => { const openDialog = (p: ProformaSummaryData) => {
setState({ open: true, proforma: proforma, loading: false }); setState({ open: true, proforma: p });
}; };
// cerrar diálogo // cerrar diálogo
@ -30,23 +27,17 @@ export function useProformaIssueDialogController() {
}; };
// confirmar emisión // confirmar emisión
const confirmIssue = async () => { const confirmIssue = () => {
if (!state.proforma) return; if (!state.proforma) return;
setState((s) => ({ ...s, loading: true })); mutate(
{ proformaId: state.proforma.id },
await issueProforma(state.proforma.id, { {
onSuccess: () => { onSuccess() {
showSuccessToast("Proforma emitida a factura"); closeDialog();
}, },
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

@ -1 +0,0 @@
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 { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { type IssueProformaInvoiceResponse, issueProformaInvoiceApi } from "../api"; import { issueProformaInvoiceApi } from "../api";
interface IssueProformaPayload { interface IssueProformaPayload {
proformaId: string; proformaId: string;
} }
interface IssueProformaOptions { interface IssueProformaResponse {
onSuccess?: () => void; proformaId: string;
onError?: (err: unknown) => void; success: boolean;
onLoadingChange?: (loading: boolean) => void;
} }
export function useIssueProformaMutation() { export function useIssueProformaMutation() {
const dataSource = useDataSource(); const dataSource = useDataSource();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation<IssueProformaInvoiceResponse, DefaultError, IssueProformaPayload>({ return useMutation({
mutationFn: ({ proformaId }) => issueProformaInvoiceApi(dataSource, proformaId), mutationFn: ({ proformaId }: IssueProformaPayload) =>
issueProformaInvoiceApi(dataSource, proformaId),
onSuccess(_data, _vars, _ctx) { onSuccess(_data, _vars, _ctx) {
// Refresca el listado de proformas // Refresca el listado de proformas
@ -28,21 +28,4 @@ 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,40 +9,31 @@ import {
Spinner, Spinner,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { useTranslation } from "../../../i18n"; import { useProformaIssueDialogController } from "../controllers";
import type { ProformaSummaryData } from "../../types";
interface ProformaIssueDialogProps { interface ProformaIssueDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
proforma: ProformaSummaryData | null; proformaId: string;
isSubmitting: boolean; proformaReference: string;
onConfirm: (proforma?: ProformaSummaryData) => void;
} }
export function ProformaIssueDialog({ export function ProformaIssueDialog({
open, open,
onOpenChange, onOpenChange,
proforma, proformaId,
isSubmitting, proformaReference,
onConfirm,
}: ProformaIssueDialogProps) { }: ProformaIssueDialogProps) {
const { t } = useTranslation(); const { issue, isSubmitting } = useProformaIssueDialogController();
return ( return (
<AlertDialog onOpenChange={onOpenChange} open={open}> <AlertDialog onOpenChange={onOpenChange} open={open}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Emitir a factura</AlertDialogTitle> <AlertDialogTitle>Emitir factura</AlertDialogTitle>
<AlertDialogDescription className="space-y-4"> <AlertDialogDescription>
<p> ¿Seguro que quieres emitir la factura desde la proforma{" "}
¿Seguro que quieres emitir la factura desde la proforma{" "} <strong>{proformaReference}</strong>? Esta acción es irreversible.
<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>
@ -51,14 +42,17 @@ export function ProformaIssueDialog({
Cancelar Cancelar
</Button> </Button>
<Button disabled={isSubmitting} onClick={() => onConfirm(proforma)}> <Button
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,18 +93,13 @@ export function useProformasGridColumns(
<ProformaStatusBadge status={proforma.status} /> <ProformaStatusBadge status={proforma.status} />
{/* Enlace discreto a factura real */} {/* Enlace discreto a factura real */}
{isIssued && ( {isIssued && invoiceId && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button asChild className="size-6" size="icon" variant="ghost">
asChild
className="size-6 text-foreground hover:text-primary"
size="icon"
variant="ghost"
>
<a href={`/facturas/${invoiceId}`}> <a href={`/facturas/${invoiceId}`}>
<ExternalLinkIcon /> <ExternalLinkIcon className="size-3 text-muted-foreground" />
<span className="sr-only">Ver factura {invoiceId}</span> <span className="sr-only">Ver factura {invoiceId}</span>
</a> </a>
</Button> </Button>

View File

@ -121,7 +121,8 @@ 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}
proforma={issueDialogCtrl.proforma} proformaId={issueDialogCtrl.proforma?.id ?? 0}
proformaReference={issueDialogCtrl.proforma?.reference ?? ""}
/> />
{/* Cambiar estado */} {/* Cambiar estado */}
@ -140,7 +141,6 @@ 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>
</> </>