.
This commit is contained in:
parent
700445499b
commit
132408ac23
@ -12,6 +12,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
type SimpleSearchInputProps = {
|
type SimpleSearchInputProps = {
|
||||||
|
value: string;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
maxHistory?: number;
|
maxHistory?: number;
|
||||||
@ -20,12 +21,13 @@ type SimpleSearchInputProps = {
|
|||||||
const SEARCH_HISTORY_KEY = "search_history";
|
const SEARCH_HISTORY_KEY = "search_history";
|
||||||
|
|
||||||
export const SimpleSearchInput = ({
|
export const SimpleSearchInput = ({
|
||||||
|
value,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
loading = false,
|
loading = false,
|
||||||
maxHistory = 8,
|
maxHistory = 8,
|
||||||
}: SimpleSearchInputProps) => {
|
}: SimpleSearchInputProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState(value);
|
||||||
const [lastSearch, setLastSearch] = useState("");
|
const [lastSearch, setLastSearch] = useState("");
|
||||||
const [history, setHistory] = useState<string[]>([]);
|
const [history, setHistory] = useState<string[]>([]);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|||||||
5
modules/core/src/web/lib/helpers/http-url-utils.ts
Normal file
5
modules/core/src/web/lib/helpers/http-url-utils.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function safeHTTPUrl(url: string) {
|
||||||
|
if (!url) return "#";
|
||||||
|
if (/^https?:\/\//i.test(url)) return url;
|
||||||
|
return `https://${url}`;
|
||||||
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./date-func";
|
export * from "./date-func";
|
||||||
export * from "./form-utils";
|
export * from "./form-utils";
|
||||||
|
export * from "./http-url-utils";
|
||||||
|
|||||||
@ -103,7 +103,14 @@
|
|||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"deleting": "Eliminando...",
|
"deleting": "Eliminando...",
|
||||||
"success_title": "Proforma eliminada",
|
"success_title": "Proforma eliminada",
|
||||||
"error_title": "Error al eliminar la proforma"
|
"error_title": "Error al eliminar la proforma",
|
||||||
|
"single_title": "Eliminar proforma {{reference}}",
|
||||||
|
"second_confirm_title": "Confirmación adicional",
|
||||||
|
"multiple_title": "Eliminar {{count}} proformas",
|
||||||
|
"second_confirm_description": "Estás a punto de borrar {{total}} proformas. Esta acción masiva no se puede deshacer. ¿Seguro que quieres continuar?",
|
||||||
|
"single_description": "¿Seguro que deseas eliminar esta proforma? Esta acción no se puede deshacer.",
|
||||||
|
"multiple_description": "¿Seguro que deseas eliminar las {{total}} proformas seleccionadas? Esta acción no se puede deshacer.",
|
||||||
|
"list_item": "Proforma {{reference}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
|
|||||||
@ -2,19 +2,17 @@ import type { ModuleClientParams } from "@erp/core/client";
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
import { Outlet, type RouteObject } from "react-router-dom";
|
import { Outlet, type RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
import { ProformaCreatePage } from "./proformas/create";
|
|
||||||
|
|
||||||
const ProformaLayout = lazy(() =>
|
const ProformaLayout = lazy(() =>
|
||||||
import("./proformas/ui").then((m) => ({ default: m.ProformaLayout }))
|
import("./proformas/shared").then((m) => ({ default: m.ProformaLayout }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const ProformasListPage = lazy(() =>
|
const ProformasListPage = lazy(() =>
|
||||||
import("./proformas/list").then((m) => ({ default: m.ProformaListPage }))
|
import("./proformas/list").then((m) => ({ default: m.ProformaListPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const ProformasCreatePage = lazy(() =>
|
/*const ProformasCreatePage = lazy(() =>
|
||||||
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
|
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
|
||||||
);
|
);*/
|
||||||
|
|
||||||
/*const InvoiceUpdatePage = lazy(() =>
|
/*const InvoiceUpdatePage = lazy(() =>
|
||||||
import("./pages").then((m) => ({ default: m.InvoiceUpdatePage }))
|
import("./pages").then((m) => ({ default: m.InvoiceUpdatePage }))
|
||||||
@ -40,7 +38,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
children: [
|
children: [
|
||||||
{ path: "", index: true, element: <ProformasListPage /> }, // index
|
{ path: "", index: true, element: <ProformasListPage /> }, // index
|
||||||
{ path: "list", element: <ProformasListPage /> },
|
{ path: "list", element: <ProformasListPage /> },
|
||||||
{ path: "create", element: <ProformaCreatePage /> },
|
//{ path: "create", element: <ProformaCreatePage /> },
|
||||||
//{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
//{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./use-download-invoice-pdf-query";
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./use-issued-invoice-list-query";
|
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./use-download-invoice-pdf-query";
|
||||||
|
export * from "./use-issued-invoice-list-query";
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { type QueryKey, useQuery } from "@tanstack/react-query";
|
import { type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { downloadInvoicePDFApi } from "../api";
|
import { downloadInvoicePDFApi } from "../../download-pdf/api";
|
||||||
|
|
||||||
export const ISSUED_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issued_invoice", id] as const;
|
export const ISSUED_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issued_invoice", id] as const;
|
||||||
|
|
||||||
@ -3,8 +3,8 @@ import { useDataSource } from "@erp/core/hooks";
|
|||||||
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
||||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { getIssuedInvoiceListApi } from "../../list/api";
|
||||||
import type { IssuedInvoiceSummaryPage } from "../../types";
|
import type { IssuedInvoiceSummaryPage } from "../../types";
|
||||||
import { getIssuedInvoiceListApi } from "../api";
|
|
||||||
|
|
||||||
export const ISSUED_INVOICES_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
export const ISSUED_INVOICES_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
||||||
"issued_invoices",
|
"issued_invoices",
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./hooks";
|
||||||
@ -38,8 +38,8 @@ import { useFieldArray, useForm } from "react-hook-form";
|
|||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerInvoicePricesCard } from "../../shared/ui/components";
|
import { CustomerInvoicePricesCard } from "../../proformas/shared/ui/components";
|
||||||
import { CustomerInvoiceItemsCardEditor } from "../../shared/ui/components/items";
|
import { CustomerInvoiceItemsCardEditor } from "../../proformas/shared/ui/components/items";
|
||||||
|
|
||||||
import type { CustomerInvoiceData } from "./customer-invoice.schema";
|
import type { CustomerInvoiceData } from "./customer-invoice.schema";
|
||||||
import { formatCurrency } from "./utils";
|
import { formatCurrency } from "./utils";
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./change-proforma-status.api";
|
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./use-change-status-dialog-controller";
|
export * from "./use-change-proforma-status-dialog-controller";
|
||||||
|
|||||||
@ -0,0 +1,160 @@
|
|||||||
|
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
import type { ProformaListRow } from "../../shared";
|
||||||
|
import type { PROFORMA_STATUS } from "../../shared/entities";
|
||||||
|
import { useChangeProformaStatusMutation } from "../../shared/hooks";
|
||||||
|
|
||||||
|
interface ChangeStatusDialogState {
|
||||||
|
open: boolean;
|
||||||
|
proformas: ProformaListRow[];
|
||||||
|
isSubmitting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_STATE: ChangeStatusDialogState = {
|
||||||
|
open: false,
|
||||||
|
proformas: [],
|
||||||
|
isSubmitting: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function canChangeStatus(isSubmitting: boolean, proformas: ProformaListRow[]): boolean {
|
||||||
|
return !isSubmitting && proformas.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChangeProformaStatusDialogController() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { changeProformaStatus } = useChangeProformaStatusMutation();
|
||||||
|
|
||||||
|
const [state, setState] = React.useState<ChangeStatusDialogState>(INITIAL_STATE);
|
||||||
|
const { isSubmitting, proformas } = state;
|
||||||
|
|
||||||
|
const openDialog = React.useCallback((proformas: ProformaListRow[]) => {
|
||||||
|
setState({
|
||||||
|
open: true,
|
||||||
|
proformas,
|
||||||
|
isSubmitting: false,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeDialog = React.useCallback(() => {
|
||||||
|
setState(INITIAL_STATE);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const changeStatusSelectedProformas = React.useCallback(
|
||||||
|
async (proformas: ProformaListRow[], newStatus: PROFORMA_STATUS) => {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
proformas.map((proforma) =>
|
||||||
|
changeProformaStatus({
|
||||||
|
proformaId: proforma.id,
|
||||||
|
newStatus,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const successCount = results.filter((result) => result.status === "fulfilled").length;
|
||||||
|
const errorCount = results.length - successCount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
successCount,
|
||||||
|
errorCount,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[changeProformaStatus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const notifyChangeResult = React.useCallback(
|
||||||
|
(proformas: ProformaListRow[], successCount: number, errorCount: number) => {
|
||||||
|
if (proformas.length === 1 && successCount === 1) {
|
||||||
|
const proforma = proformas[0];
|
||||||
|
|
||||||
|
showSuccessToast(
|
||||||
|
t("pages.proformas.change_status.successTitle"),
|
||||||
|
t("pages.proformas.change_status.successSingleMessage", {
|
||||||
|
reference: proforma.reference || `#${proforma.id}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (successCount > 0) {
|
||||||
|
showSuccessToast(
|
||||||
|
t("pages.proformas.change_status.successTitle"),
|
||||||
|
t("pages.proformas.change_status.successMultipleMessage", {
|
||||||
|
count: successCount,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCount > 0) {
|
||||||
|
showErrorToast(
|
||||||
|
t("pages.proformas.change_status.errorTitle"),
|
||||||
|
proformas.length === 1
|
||||||
|
? t("pages.proformas.change_status.errorSingleMessage")
|
||||||
|
: t("pages.proformas.change_status.errorMultipleMessage", {
|
||||||
|
count: errorCount,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeStatus = React.useCallback(
|
||||||
|
async (newStatus: PROFORMA_STATUS) => {
|
||||||
|
setState((current) => ({
|
||||||
|
...current,
|
||||||
|
isSubmitting: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { successCount, errorCount } = await changeStatusSelectedProformas(
|
||||||
|
proformas,
|
||||||
|
newStatus
|
||||||
|
);
|
||||||
|
|
||||||
|
notifyChangeResult(proformas, successCount, errorCount);
|
||||||
|
|
||||||
|
if (errorCount === 0) {
|
||||||
|
closeDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((current) => ({
|
||||||
|
...current,
|
||||||
|
isSubmitting: false,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
showErrorToast(
|
||||||
|
t("pages.proformas.change_status.errorTitle"),
|
||||||
|
t("pages.proformas.change_status.errorUnexpectedMessage")
|
||||||
|
);
|
||||||
|
|
||||||
|
setState((current) => ({
|
||||||
|
...current,
|
||||||
|
isSubmitting: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[closeDialog, changeStatusSelectedProformas, notifyChangeResult, proformas, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmChangeStatus = React.useCallback(
|
||||||
|
async (newStatus: PROFORMA_STATUS) => {
|
||||||
|
if (!canChangeStatus(isSubmitting, proformas)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await changeStatus(newStatus);
|
||||||
|
},
|
||||||
|
[changeStatus, isSubmitting, proformas]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
open: state.open,
|
||||||
|
proformas: state.proformas,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
isBulkDelete: state.proformas.length > 1,
|
||||||
|
|
||||||
|
openDialog,
|
||||||
|
closeDialog,
|
||||||
|
confirmChangeStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import type { PROFORMA_STATUS, ProformaSummaryData } from "../../types";
|
|
||||||
import { useChangeProformaStatus } from "../hooks/use-change-status";
|
|
||||||
|
|
||||||
interface ChangeStatusDialogState {
|
|
||||||
open: boolean;
|
|
||||||
proformas: ProformaSummaryData[];
|
|
||||||
loading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useChangeStatusDialogController() {
|
|
||||||
const { changeStatus, isPending } = useChangeProformaStatus();
|
|
||||||
|
|
||||||
const [state, setState] = React.useState<ChangeStatusDialogState>({
|
|
||||||
open: false,
|
|
||||||
proformas: [] as ProformaSummaryData[],
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const openDialog = (proformas: ProformaSummaryData[]) => {
|
|
||||||
setState({
|
|
||||||
open: true,
|
|
||||||
proformas,
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeDialog = () => {
|
|
||||||
setState((s) => ({ ...s, open: false }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmChangeStatus = async (status: PROFORMA_STATUS) => {
|
|
||||||
if (!state.proformas.length) return;
|
|
||||||
|
|
||||||
setState((s) => ({ ...s, loading: true }));
|
|
||||||
|
|
||||||
for (const proforma of state.proformas) {
|
|
||||||
await changeStatus(proforma.id, status, {
|
|
||||||
onSuccess: () => {
|
|
||||||
showSuccessToast("Estado cambiado");
|
|
||||||
},
|
|
||||||
onError: (err: unknown) => {
|
|
||||||
const error = err as Error;
|
|
||||||
showErrorToast("Error cambiando estado", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setState((s) => ({ ...s, loading: false }));
|
|
||||||
closeDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
open: state.open,
|
|
||||||
proformas: state.proformas,
|
|
||||||
isSubmitting: state.loading,
|
|
||||||
|
|
||||||
openDialog,
|
|
||||||
closeDialog,
|
|
||||||
confirmChangeStatus,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./use-change-status";
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import type { PROFORMA_STATUS } from "../../types";
|
|
||||||
import { changeProformaStatusApi } from "../api/change-proforma-status.api";
|
|
||||||
|
|
||||||
interface ChangeProformaStatusOptions {
|
|
||||||
onSuccess?: () => void;
|
|
||||||
onError?: (err: unknown) => void;
|
|
||||||
onLoadingChange?: (loading: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChangeProformaStatusPayload {
|
|
||||||
proformaId: string;
|
|
||||||
newStatus: PROFORMA_STATUS;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useChangeProformaStatus() {
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: ({ proformaId, newStatus }: ChangeProformaStatusPayload) =>
|
|
||||||
changeProformaStatusApi(dataSource, proformaId, newStatus),
|
|
||||||
|
|
||||||
onSuccess() {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["proformas"] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function changeStatus(
|
|
||||||
proformaId: string,
|
|
||||||
newStatus: string,
|
|
||||||
opts?: ChangeProformaStatusOptions
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
opts?.onLoadingChange?.(true);
|
|
||||||
await mutation.mutateAsync({ proformaId, newStatus: newStatus as PROFORMA_STATUS });
|
|
||||||
opts?.onSuccess?.();
|
|
||||||
} catch (err) {
|
|
||||||
opts?.onError?.(err);
|
|
||||||
} finally {
|
|
||||||
opts?.onLoadingChange?.(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
changeStatus,
|
|
||||||
isPending: mutation.isPending,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -119,7 +119,13 @@ export function ChangeStatusDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onOpenChange={onOpenChange} open={open}>
|
<Dialog
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
onOpenChange(nextOpen);
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
<DialogContent className="sm:max-w-4xl">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Cambiar estado de la proforma</DialogTitle>
|
<DialogTitle>Cambiar estado de la proforma</DialogTitle>
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./delete-proforma.api";
|
|
||||||
@ -2,76 +2,182 @@ import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import type { ProformaSummaryData } from "../../types";
|
import { type ProformaListRow, useDeleteProformaMutation } from "../../shared";
|
||||||
import { useDeleteProforma } from "../hooks";
|
|
||||||
|
|
||||||
interface DeleteProformaDialogState {
|
interface DeleteProformaDialogState {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
proformas: ProformaSummaryData[];
|
proformas: ProformaListRow[];
|
||||||
loading: boolean;
|
isSubmitting: boolean;
|
||||||
requireSecondConfirm: boolean;
|
requiresSecondConfirm: boolean;
|
||||||
|
confirmStep: "initial" | "second";
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_STATE: DeleteProformaDialogState = {
|
||||||
|
open: false,
|
||||||
|
proformas: [],
|
||||||
|
isSubmitting: false,
|
||||||
|
requiresSecondConfirm: false,
|
||||||
|
confirmStep: "initial",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECOND_CONFIRM_THRESHOLD = 5;
|
||||||
|
|
||||||
|
function canSubmitDelete(isSubmitting: boolean, proformas: ProformaListRow[]): boolean {
|
||||||
|
return !isSubmitting && proformas.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldMoveToSecondConfirmStep(
|
||||||
|
requiresSecondConfirm: boolean,
|
||||||
|
confirmStep: "initial" | "second"
|
||||||
|
): boolean {
|
||||||
|
return requiresSecondConfirm && confirmStep === "initial";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDeleteProformaDialogController() {
|
export function useDeleteProformaDialogController() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { deleteProforma } = useDeleteProforma();
|
const { deleteProforma } = useDeleteProformaMutation();
|
||||||
|
|
||||||
const [state, setState] = React.useState<DeleteProformaDialogState>({
|
const [state, setState] = React.useState<DeleteProformaDialogState>(INITIAL_STATE);
|
||||||
open: false,
|
const { isSubmitting, proformas, requiresSecondConfirm, confirmStep } = state;
|
||||||
proformas: [],
|
|
||||||
loading: false,
|
const openDialog = React.useCallback((proformas: ProformaListRow[]) => {
|
||||||
requireSecondConfirm: false,
|
const requiresSecondConfirm = proformas.length > SECOND_CONFIRM_THRESHOLD;
|
||||||
});
|
|
||||||
|
|
||||||
const openDialog = (proformas: ProformaSummaryData[]) => {
|
|
||||||
const needDoubleCheck = proformas.length > 5;
|
|
||||||
setState({
|
setState({
|
||||||
open: true,
|
open: true,
|
||||||
proformas,
|
proformas,
|
||||||
loading: false,
|
isSubmitting: false,
|
||||||
requireSecondConfirm: needDoubleCheck,
|
requiresSecondConfirm,
|
||||||
|
confirmStep: "initial",
|
||||||
});
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeDialog = React.useCallback(() => {
|
||||||
|
setState(INITIAL_STATE);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const moveToSecondConfirmStep = React.useCallback(() => {
|
||||||
|
setState((current) => ({
|
||||||
|
...current,
|
||||||
|
confirmStep: "second",
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deleteSelectedProformas = React.useCallback(
|
||||||
|
async (proformas: ProformaListRow[]) => {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
proformas.map((proforma) =>
|
||||||
|
deleteProforma({
|
||||||
|
proformaId: proforma.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const successCount = results.filter((result) => result.status === "fulfilled").length;
|
||||||
|
const errorCount = results.length - successCount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
successCount,
|
||||||
|
errorCount,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
[deleteProforma]
|
||||||
|
);
|
||||||
|
|
||||||
const closeDialog = () => {
|
const notifyDeleteResult = React.useCallback(
|
||||||
setState((s) => ({ ...s, open: false }));
|
(proformas: ProformaListRow[], successCount: number, errorCount: number) => {
|
||||||
};
|
if (proformas.length === 1 && successCount === 1) {
|
||||||
|
const proforma = proformas[0];
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
|
||||||
if (state.proformas.length === 0) return;
|
|
||||||
|
|
||||||
if (state.requireSecondConfirm) {
|
|
||||||
setState((s) => ({ ...s, requireSecondConfirm: false }));
|
|
||||||
return; // ahora el UI mostrará un segundo mensaje de confirmación
|
|
||||||
}
|
|
||||||
|
|
||||||
setState((s) => ({ ...s, loading: true }));
|
|
||||||
|
|
||||||
for (const p of state.proformas) {
|
|
||||||
await deleteProforma(p.id, {
|
|
||||||
onSuccess: () => {
|
|
||||||
showSuccessToast(
|
showSuccessToast(
|
||||||
"Proforma eliminada",
|
t("pages.proformas.delete.successTitle"),
|
||||||
`La proforma ${p.reference ?? `#${p.id}`} ha sido eliminada.`
|
t("pages.proformas.delete.successSingleMessage", {
|
||||||
|
reference: proforma.reference || `#${proforma.id}`,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
},
|
} else if (successCount > 0) {
|
||||||
onError: (err) => {
|
showSuccessToast(
|
||||||
showErrorToast(
|
t("pages.proformas.delete.successTitle"),
|
||||||
"Error al eliminar",
|
t("pages.proformas.delete.successMultipleMessage", {
|
||||||
err instanceof Error ? err.message : "Ocurrió un error al eliminar la proforma"
|
count: successCount,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState((s) => ({ ...s, loading: false }));
|
if (errorCount > 0) {
|
||||||
|
showErrorToast(
|
||||||
|
t("pages.proformas.delete.errorTitle"),
|
||||||
|
proformas.length === 1
|
||||||
|
? t("pages.proformas.delete.errorSingleMessage")
|
||||||
|
: t("pages.proformas.delete.errorMultipleMessage", {
|
||||||
|
count: errorCount,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitDelete = React.useCallback(async () => {
|
||||||
|
setState((current) => ({
|
||||||
|
...current,
|
||||||
|
isSubmitting: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { successCount, errorCount } = await deleteSelectedProformas(proformas);
|
||||||
|
|
||||||
|
notifyDeleteResult(proformas, successCount, errorCount);
|
||||||
|
|
||||||
|
if (errorCount === 0) {
|
||||||
closeDialog();
|
closeDialog();
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((current) => ({
|
||||||
|
...current,
|
||||||
|
isSubmitting: false,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
showErrorToast(
|
||||||
|
t("pages.proformas.delete.errorTitle"),
|
||||||
|
t("pages.proformas.delete.errorUnexpectedMessage")
|
||||||
|
);
|
||||||
|
|
||||||
|
setState((current) => ({
|
||||||
|
...current,
|
||||||
|
isSubmitting: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [closeDialog, deleteSelectedProformas, notifyDeleteResult, proformas, t]);
|
||||||
|
|
||||||
|
const confirmDelete = React.useCallback(async () => {
|
||||||
|
if (!canSubmitDelete(isSubmitting, proformas)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldMoveToSecondConfirmStep(requiresSecondConfirm, confirmStep)) {
|
||||||
|
moveToSecondConfirmStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await submitDelete();
|
||||||
|
}, [
|
||||||
|
moveToSecondConfirmStep,
|
||||||
|
submitDelete,
|
||||||
|
isSubmitting,
|
||||||
|
proformas,
|
||||||
|
requiresSecondConfirm,
|
||||||
|
confirmStep,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
open: state.open,
|
open: state.open,
|
||||||
proformas: state.proformas,
|
proformas: state.proformas,
|
||||||
isSubmitting: state.loading,
|
isSubmitting: state.isSubmitting,
|
||||||
|
requiresSecondConfirm: state.requiresSecondConfirm,
|
||||||
|
isSecondConfirmStep: state.confirmStep === "second",
|
||||||
|
isBulkDelete: state.proformas.length > 1,
|
||||||
|
|
||||||
openDialog,
|
openDialog,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./use-delete-proforma";
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { deleteProformaApi } from "../api";
|
|
||||||
|
|
||||||
interface DeleteProformaOptions {
|
|
||||||
onSuccess?: () => void;
|
|
||||||
onError?: (err: unknown) => void;
|
|
||||||
onLoadingChange?: (loading: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteProformaPayload {
|
|
||||||
proformaId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDeleteProforma() {
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: ({ proformaId }: DeleteProformaPayload) =>
|
|
||||||
deleteProformaApi(dataSource, proformaId),
|
|
||||||
|
|
||||||
onSuccess() {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["proformas"] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function deleteProforma(proformaId: string, opts?: DeleteProformaOptions) {
|
|
||||||
try {
|
|
||||||
opts?.onLoadingChange?.(true);
|
|
||||||
await mutation.mutateAsync({ proformaId });
|
|
||||||
opts?.onSuccess?.();
|
|
||||||
} catch (err) {
|
|
||||||
opts?.onError?.(err);
|
|
||||||
} finally {
|
|
||||||
opts?.onLoadingChange?.(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
deleteProforma,
|
|
||||||
isPending: mutation.isPending,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./controllers";
|
export * from "./controllers";
|
||||||
|
export * from "./ui";
|
||||||
|
|||||||
@ -8,18 +8,18 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { ProformaSummaryData } from "../../types";
|
import type { ProformaListRow } from "../../../shared";
|
||||||
|
|
||||||
interface DeleteProformaDialogProps {
|
interface DeleteProformaDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
proformas: ProformaSummaryData[];
|
proformas: ProformaListRow[];
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
requireSecondConfirm: boolean;
|
requiresSecondConfirm: boolean;
|
||||||
|
isSecondConfirmStep: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeleteProformaDialog({
|
export function DeleteProformaDialog({
|
||||||
@ -28,56 +28,53 @@ export function DeleteProformaDialog({
|
|||||||
proformas,
|
proformas,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
requireSecondConfirm,
|
requiresSecondConfirm,
|
||||||
|
isSecondConfirmStep,
|
||||||
}: DeleteProformaDialogProps) {
|
}: DeleteProformaDialogProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const total = proformas.length;
|
const total = proformas.length;
|
||||||
const isSingle = total === 1;
|
const isSingle = total === 1;
|
||||||
|
const firstProforma = proformas[0];
|
||||||
|
|
||||||
const title = requireSecondConfirm
|
const title = isSecondConfirmStep
|
||||||
? "Confirmación adicional"
|
? t("proformas.delete_proforma_dialog.second_confirm_title", { count: total })
|
||||||
: isSingle
|
: isSingle
|
||||||
? `Eliminar proforma ${proformas[0].reference ?? `#${proformas[0].id}`}`
|
? t("proformas.delete_proforma_dialog.single_title", {
|
||||||
: `Eliminar ${total} proformas`;
|
reference: firstProforma?.reference ?? `#${firstProforma?.id}`,
|
||||||
|
})
|
||||||
|
: t("proformas.delete_proforma_dialog.multiple_title", { count: total });
|
||||||
|
|
||||||
const description = requireSecondConfirm
|
const description = isSecondConfirmStep
|
||||||
? `Estás a punto de borrar ${total} proformas. Esta acción masiva no se puede deshacer. ¿Seguro que quieres continuar?`
|
? t("proformas.delete_proforma_dialog.second_confirm_description", { count: total })
|
||||||
: isSingle
|
: isSingle
|
||||||
? "¿Seguro que deseas eliminar esta proforma? Esta acción no se puede deshacer."
|
? t("proformas.delete_proforma_dialog.single_description")
|
||||||
: `¿Seguro que deseas eliminar las ${total} proformas seleccionadas? Esta acción no se puede deshacer.`;
|
: t("proformas.delete_proforma_dialog.multiple_description", { count: total });
|
||||||
|
|
||||||
// Usar teclado para confirmar (Enter / Escape)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
|
|
||||||
const handleKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
onConfirm();
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
onOpenChange(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKey);
|
|
||||||
return () => window.removeEventListener("keydown", handleKey);
|
|
||||||
}, [open, onConfirm, onOpenChange]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog onOpenChange={onOpenChange} open={open}>
|
<AlertDialog
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
onOpenChange(nextOpen);
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
<AlertDialogContent className="max-w-lg">
|
<AlertDialogContent className="max-w-lg">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
{!requireSecondConfirm && total > 1 && (
|
{!isSecondConfirmStep && total > 1 && (
|
||||||
<div className="mt-4 max-h-48 overflow-y-auto rounded-md border p-3 text-sm">
|
<div className="mt-4 max-h-48 overflow-y-auto rounded-md border p-3 text-sm">
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{proformas.map((p) => (
|
{proformas.map((proforma) => (
|
||||||
<li className="flex justify-between text-muted-foreground" key={p.id}>
|
<li className="flex justify-between text-muted-foreground" key={proforma.id}>
|
||||||
<span>Proforma {p.reference ?? `#${p.id}`}</span>
|
<span>
|
||||||
|
{t("proformas.delete_proforma_dialog.list_item", {
|
||||||
|
reference: proforma.reference ?? `#${proforma.id}`,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -95,12 +92,14 @@ export function DeleteProformaDialog({
|
|||||||
<Spinner className="mr-2 size-4" />
|
<Spinner className="mr-2 size-4" />
|
||||||
{t("proformas.delete_proforma_dialog.deleting")}
|
{t("proformas.delete_proforma_dialog.deleting")}
|
||||||
</>
|
</>
|
||||||
) : requireSecondConfirm ? (
|
) : isSecondConfirmStep ? (
|
||||||
<>{t("proformas.delete_proforma_dialog.mass_delete")}</>
|
t("proformas.delete_proforma_dialog.confirm_mass_delete")
|
||||||
) : isSingle ? (
|
) : isSingle ? (
|
||||||
<>{t("proformas.delete_proforma_dialog.delete")}</>
|
t("proformas.delete_proforma_dialog.delete")
|
||||||
|
) : requiresSecondConfirm ? (
|
||||||
|
t("proformas.delete_proforma_dialog.continue")
|
||||||
) : (
|
) : (
|
||||||
<>{t("proformas.delete_proforma_dialog.delete_plural")}</>
|
t("proformas.delete_proforma_dialog.delete_plural")
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./delete-proforma-dialog";
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./delete-proforma-dialog";
|
export * from "./components";
|
||||||
|
|||||||
@ -1,4 +1,2 @@
|
|||||||
export * from "./use-issue-proforma-invoice";
|
export * from "./use-issue-proforma-invoice";
|
||||||
export * from "./use-proforma-query";
|
|
||||||
export * from "./use-proforma-update-mutation";
|
|
||||||
export * from "./use-proformas-query";
|
export * from "./use-proformas-query";
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
11111import
|
import {
|
||||||
{
|
useDataSource
|
||||||
useDataSource;
|
} from ("@erp/core/hooks");
|
||||||
}
|
|
||||||
from;
|
|
||||||
("@erp/core/hooks");
|
|
||||||
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import type { Proforma } from "../types/proforma.api.schema";
|
|
||||||
|
|
||||||
export const PROFORMA_QUERY_KEY = (id: string): QueryKey => ["proforma", id] as const;
|
|
||||||
|
|
||||||
type InvoiceQueryOptions = {
|
|
||||||
enabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useProformaQuery = (proformaId?: string, options?: InvoiceQueryOptions) => {
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
const enabled = (options?.enabled ?? true) && !!proformaId;
|
|
||||||
|
|
||||||
return useQuery<Proforma, DefaultError>({
|
|
||||||
queryKey: PROFORMA_QUERY_KEY(proformaId ?? "unknown"),
|
|
||||||
queryFn: async (context) => {
|
|
||||||
const { signal } = context;
|
|
||||||
if (!proformaId) {
|
|
||||||
if (!proformaId) throw new Error("proformaId is required");
|
|
||||||
}
|
|
||||||
return await dataSource.getOne<Proforma>("proformas", proformaId, {
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
enabled,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
export function useQuery<
|
|
||||||
TQueryFnData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
TData = TQueryFnData,
|
|
||||||
TQueryKey extends QueryKey = QueryKey
|
|
||||||
>
|
|
||||||
|
|
||||||
TQueryFnData: the type returned from the queryFn.
|
|
||||||
TError: the type of Errors to expect from the queryFn.
|
|
||||||
TData: the type our data property will eventually have.
|
|
||||||
Only relevant if you use the select option,
|
|
||||||
because then the data property can be different
|
|
||||||
from what the queryFn returns.
|
|
||||||
Otherwise, it will default to whatever the queryFn returns.
|
|
||||||
TQueryKey: the type of our queryKey, only relevant
|
|
||||||
if you use the queryKey that is passed to your queryFn.
|
|
||||||
|
|
||||||
*/
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import {
|
|
||||||
type UpdateProformaByIdRequestDTO,
|
|
||||||
UpdateProformaByIdRequestSchema,
|
|
||||||
} from "../../../common";
|
|
||||||
import type { InvoiceFormData } from "../../schemas";
|
|
||||||
|
|
||||||
import { PROFORMA_QUERY_KEY } from "./use-proforma-query";
|
|
||||||
import { PROFORMAS_QUERY_KEY } from "./use-proformas-query";
|
|
||||||
|
|
||||||
type UpdateProformaContext = unknown;
|
|
||||||
|
|
||||||
type UpdateProformaPayload = {
|
|
||||||
id: string;
|
|
||||||
data: Partial<InvoiceFormData>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useUpdateProforma() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
const schema = UpdateProformaByIdRequestSchema;
|
|
||||||
|
|
||||||
return useMutation<InvoiceFormData, Error, UpdateProformaPayload, UpdateProformaContext>({
|
|
||||||
mutationKey: ["proforma:update"], //, customerId],
|
|
||||||
|
|
||||||
mutationFn: async (payload) => {
|
|
||||||
const { id: invoiceId, data } = payload;
|
|
||||||
|
|
||||||
if (!invoiceId) {
|
|
||||||
throw new Error("customerInvoiceId is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = schema.safeParse(data);
|
|
||||||
if (!result.success) {
|
|
||||||
// Construye errores detallados
|
|
||||||
const validationErrors = result.error.issues.map((err) => ({
|
|
||||||
field: err.path.join("."),
|
|
||||||
message: err.message,
|
|
||||||
}));
|
|
||||||
|
|
||||||
throw new ValidationErrorCollection("Validation failed", validationErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await dataSource.updateOne("customer-invoices", invoiceId, data);
|
|
||||||
return updated as InvoiceFormData;
|
|
||||||
},
|
|
||||||
onSuccess: (updated: InvoiceFormData, variables) => {
|
|
||||||
const { id: invoiceId } = variables;
|
|
||||||
|
|
||||||
// Refresca inmediatamente el detalle
|
|
||||||
queryClient.setQueryData<UpdateProformaByIdRequestDTO>(
|
|
||||||
PROFORMA_QUERY_KEY(invoiceId),
|
|
||||||
updated
|
|
||||||
);
|
|
||||||
|
|
||||||
// Otra opción es invalidar el detalle para forzar refetch:
|
|
||||||
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
|
|
||||||
|
|
||||||
// Invalida el listado para refrescar desde servidor
|
|
||||||
queryClient.invalidateQueries({ queryKey: PROFORMAS_QUERY_KEY() });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./list";
|
export * from "./list";
|
||||||
|
export * from "./update";
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./proforma-summary-dto.adapter";
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ProformaSummaryData,
|
|
||||||
ProformaSummaryPage,
|
|
||||||
ProformaSummaryPageData,
|
|
||||||
} from "../../types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
|
||||||
*/
|
|
||||||
export const ProformaSummaryDtoAdapter = {
|
|
||||||
fromDto(pageDto: ProformaSummaryPage, context?: unknown): ProformaSummaryPageData {
|
|
||||||
return {
|
|
||||||
...pageDto,
|
|
||||||
items: pageDto.items.map(
|
|
||||||
(summaryDto) =>
|
|
||||||
({
|
|
||||||
...summaryDto,
|
|
||||||
|
|
||||||
subtotal_amount: MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
|
|
||||||
subtotal_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
|
|
||||||
Number(summaryDto.total_amount.scale || 2),
|
|
||||||
summaryDto.currency_code,
|
|
||||||
summaryDto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
|
|
||||||
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
|
|
||||||
summaryDto.discount_percentage
|
|
||||||
),
|
|
||||||
|
|
||||||
discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
|
||||||
discount_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
|
||||||
Number(summaryDto.total_amount.scale || 2),
|
|
||||||
summaryDto.currency_code,
|
|
||||||
summaryDto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
taxable_amount: MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
|
|
||||||
taxable_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
|
|
||||||
Number(summaryDto.total_amount.scale || 2),
|
|
||||||
summaryDto.currency_code,
|
|
||||||
summaryDto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
taxes_amount: MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
|
|
||||||
taxes_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
|
|
||||||
Number(summaryDto.total_amount.scale || 2),
|
|
||||||
summaryDto.currency_code,
|
|
||||||
summaryDto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
total_amount: MoneyDTOHelper.toNumber(summaryDto.total_amount),
|
|
||||||
total_amount_fmt: formatCurrency(
|
|
||||||
MoneyDTOHelper.toNumber(summaryDto.total_amount),
|
|
||||||
Number(summaryDto.total_amount.scale || 2),
|
|
||||||
summaryDto.currency_code,
|
|
||||||
summaryDto.language_code
|
|
||||||
),
|
|
||||||
|
|
||||||
//taxes: dto.taxes,
|
|
||||||
}) as unknown as ProformaSummaryData
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./get-proforma-list.api";
|
|
||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./use-proforma-list.controller.ts";
|
export * from "./use-list-proformas.controller.ts";
|
||||||
export * from "./use-proforma-list-page.controller.ts";
|
export * from "./use-list-proformas-page.controller.ts";
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useChangeProformaStatusDialogController } from "../../change-status";
|
||||||
|
import { useDeleteProformaDialogController } from "../../delete";
|
||||||
|
import { useProformaIssueDialogController } from "../../issue-proforma";
|
||||||
|
import type { ProformaListRow } from "../../shared";
|
||||||
|
import {
|
||||||
|
type PROFORMA_STATUS,
|
||||||
|
PROFORMA_STATUS_TRANSITIONS,
|
||||||
|
} from "../../shared/entities/proforma-status.entity";
|
||||||
|
|
||||||
|
import { useListProformasController } from "./use-list-proformas.controller";
|
||||||
|
|
||||||
|
export function useListProformasPageController() {
|
||||||
|
const listCtrl = useListProformasController();
|
||||||
|
|
||||||
|
// Controlador de diálogos
|
||||||
|
const issueDialogCtrl = useProformaIssueDialogController();
|
||||||
|
const changeStatusDialogCtrl = useChangeProformaStatusDialogController();
|
||||||
|
const deleteDialogCtrl = useDeleteProformaDialogController();
|
||||||
|
|
||||||
|
const handleOpenIssueProformaDialog = React.useCallback(
|
||||||
|
(proforma: ProformaListRow) => {
|
||||||
|
issueDialogCtrl.openDialog(proforma);
|
||||||
|
},
|
||||||
|
[issueDialogCtrl]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenChangeProformaStatusDialog = React.useCallback(
|
||||||
|
(proforma: ProformaListRow, nextStatus: string) => {
|
||||||
|
const proforma_status = proforma.status as PROFORMA_STATUS;
|
||||||
|
const transitions = PROFORMA_STATUS_TRANSITIONS[proforma_status] ?? [];
|
||||||
|
|
||||||
|
if (!transitions.includes(nextStatus as PROFORMA_STATUS)) {
|
||||||
|
console.warn(`Transición inválida: ${proforma.status} → ${nextStatus}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeStatusDialogCtrl.openDialog([proforma], nextStatus as PROFORMA_STATUS);
|
||||||
|
},
|
||||||
|
[changeStatusDialogCtrl]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenDeleteProformaDialog = React.useCallback(
|
||||||
|
(proforma: ProformaListRow) => {
|
||||||
|
deleteDialogCtrl.openDialog([proforma]);
|
||||||
|
},
|
||||||
|
[deleteDialogCtrl]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
listCtrl,
|
||||||
|
|
||||||
|
issueDialogCtrl,
|
||||||
|
changeStatusDialogCtrl,
|
||||||
|
deleteDialogCtrl,
|
||||||
|
|
||||||
|
handleIssueProforma: handleOpenIssueProformaDialog,
|
||||||
|
handleChangeStatusProforma: handleOpenChangeProformaStatusDialog,
|
||||||
|
handleDeleteProforma: handleOpenDeleteProformaDialog,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -2,10 +2,9 @@ import type { CriteriaDTO } from "@erp/core";
|
|||||||
import { useDebounce } from "@repo/rdx-ui/components";
|
import { useDebounce } from "@repo/rdx-ui/components";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { ProformaSummaryDtoAdapter } from "../adapters";
|
import { useListProformasQuery } from "../../shared";
|
||||||
import { useProformaListQuery } from "../hooks";
|
|
||||||
|
|
||||||
export const useProformaListController = () => {
|
export const useListProformasController = () => {
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@ -15,7 +14,7 @@ export const useProformaListController = () => {
|
|||||||
|
|
||||||
const criteria = useMemo<CriteriaDTO>(() => {
|
const criteria = useMemo<CriteriaDTO>(() => {
|
||||||
const baseFilters =
|
const baseFilters =
|
||||||
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
|
status === "all" ? [] : [{ field: "status", operator: "EQUALS", value: status }];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
q: debouncedQ || "",
|
q: debouncedQ || "",
|
||||||
@ -27,25 +26,42 @@ export const useProformaListController = () => {
|
|||||||
};
|
};
|
||||||
}, [pageSize, pageIndex, debouncedQ, status]);
|
}, [pageSize, pageIndex, debouncedQ, status]);
|
||||||
|
|
||||||
const query = useProformaListQuery({ criteria });
|
const query = useListProformasQuery({ criteria });
|
||||||
const data = useMemo(
|
|
||||||
() => (query.data ? ProformaSummaryDtoAdapter.fromDto(query.data) : undefined),
|
|
||||||
[query.data]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
|
const setSearchValue = (value: string) => {
|
||||||
|
setSearch(value.trim().replace(/\s+/g, " "));
|
||||||
|
setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
|
const setPageSizeValue = (value: number) => {
|
||||||
|
setPageSize(value);
|
||||||
|
setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setStatusFilter = (newStatus: string) => {
|
||||||
|
setStatus(newStatus);
|
||||||
|
setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...query,
|
data: query.data,
|
||||||
data,
|
isLoading: query.isLoading,
|
||||||
|
isFetching: query.isFetching,
|
||||||
|
|
||||||
|
isError: query.isError,
|
||||||
|
error: query.error,
|
||||||
|
|
||||||
|
refetch: query.refetch,
|
||||||
|
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
search,
|
|
||||||
setPageIndex,
|
setPageIndex,
|
||||||
setPageSize,
|
setPageSize: setPageSizeValue,
|
||||||
|
|
||||||
|
search,
|
||||||
setSearchValue,
|
setSearchValue,
|
||||||
|
|
||||||
|
status,
|
||||||
setStatusFilter,
|
setStatusFilter,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { useChangeStatusDialogController } from "../../change-status";
|
|
||||||
import { useDeleteProformaDialogController } from "../../delete";
|
|
||||||
import { useProformaIssueDialogController } from "../../issue-proforma";
|
|
||||||
import {
|
|
||||||
type PROFORMA_STATUS,
|
|
||||||
PROFORMA_STATUS_TRANSITIONS,
|
|
||||||
type ProformaSummaryData,
|
|
||||||
} from "../../types";
|
|
||||||
|
|
||||||
import { useProformaListController } from "./use-proforma-list.controller";
|
|
||||||
|
|
||||||
export function useProformaListPageController() {
|
|
||||||
const listCtrl = useProformaListController();
|
|
||||||
|
|
||||||
// Controlador de diálogos
|
|
||||||
const issueDialogCtrl = useProformaIssueDialogController();
|
|
||||||
const changeStatusDialogCtrl = useChangeStatusDialogController();
|
|
||||||
const deleteDialogCtrl = useDeleteProformaDialogController();
|
|
||||||
|
|
||||||
const handleIssueProforma = React.useCallback(
|
|
||||||
(proforma: ProformaSummaryData) => {
|
|
||||||
// Solo si approved → issued
|
|
||||||
issueDialogCtrl.openDialog(proforma);
|
|
||||||
},
|
|
||||||
[issueDialogCtrl]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeStatusProforma = React.useCallback(
|
|
||||||
(proforma: ProformaSummaryData, nextStatus: string) => {
|
|
||||||
const proforma_status = proforma.status as PROFORMA_STATUS;
|
|
||||||
const transitions = PROFORMA_STATUS_TRANSITIONS[proforma_status] ?? [];
|
|
||||||
|
|
||||||
if (!transitions.includes(nextStatus as PROFORMA_STATUS)) {
|
|
||||||
console.warn(`Transición inválida: ${proforma.status} → ${nextStatus}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeStatusDialogCtrl.openDialog([proforma]);
|
|
||||||
},
|
|
||||||
[changeStatusDialogCtrl]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeleteProforma = React.useCallback(
|
|
||||||
(proforma: ProformaSummaryData) => {
|
|
||||||
deleteDialogCtrl.openDialog([proforma]);
|
|
||||||
},
|
|
||||||
[deleteDialogCtrl]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
listCtrl,
|
|
||||||
|
|
||||||
issueDialogCtrl,
|
|
||||||
changeStatusDialogCtrl,
|
|
||||||
deleteDialogCtrl,
|
|
||||||
|
|
||||||
handleIssueProforma,
|
|
||||||
handleChangeStatusProforma,
|
|
||||||
handleDeleteProforma,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./use-proforma-list-query";
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
|
||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
|
||||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import type { ProformaSummaryPage } from "../../types";
|
|
||||||
import { getProformaListApi } from "../api";
|
|
||||||
|
|
||||||
export const PROFORMAS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
|
||||||
"proforma",
|
|
||||||
{
|
|
||||||
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
|
|
||||||
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
|
|
||||||
q: criteria?.q ?? "",
|
|
||||||
filters: criteria?.filters ?? [],
|
|
||||||
orderBy: criteria?.orderBy ?? "",
|
|
||||||
order: criteria?.order ?? "",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type ProformasQueryOptions = {
|
|
||||||
enabled?: boolean;
|
|
||||||
criteria?: CriteriaDTO;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Obtener todas las facturas
|
|
||||||
export const useProformaListQuery = (options?: ProformasQueryOptions) => {
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
const enabled = options?.enabled ?? true;
|
|
||||||
const criteria = options?.criteria ?? {};
|
|
||||||
|
|
||||||
return useQuery<ProformaSummaryPage, DefaultError>({
|
|
||||||
queryKey: PROFORMAS_QUERY_KEY(criteria),
|
|
||||||
queryFn: async ({ signal }) => getProformaListApi(dataSource, signal, criteria),
|
|
||||||
enabled,
|
|
||||||
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./proformas-grid";
|
export * from "./proformas-grid";
|
||||||
|
export * from "./use-proforma-grid-columns";
|
||||||
|
|||||||
@ -13,11 +13,10 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { ChangeStatusDialog } from "../../../change-status";
|
import { ChangeStatusDialog } from "../../../change-status";
|
||||||
import { DeleteProformaDialog } from "../../../delete/ui";
|
import { DeleteProformaDialog } from "../../../delete/ui/components";
|
||||||
import { ProformaIssueDialog } from "../../../issue-proforma";
|
import { ProformaIssueDialog } from "../../../issue-proforma";
|
||||||
import { useProformaListPageController } from "../../controllers";
|
import { useListProformasPageController } from "../../controllers";
|
||||||
import { ProformasGrid } from "../blocks/proformas-grid";
|
import { ProformasGrid, useProformasGridColumns } from "../blocks";
|
||||||
import { useProformasGridColumns } from "../blocks/proformas-grid/use-proforma-grid-columns";
|
|
||||||
|
|
||||||
export const ProformaListPage = () => {
|
export const ProformaListPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -33,7 +32,7 @@ export const ProformaListPage = () => {
|
|||||||
handleChangeStatusProforma,
|
handleChangeStatusProforma,
|
||||||
handleDeleteProforma,
|
handleDeleteProforma,
|
||||||
handleIssueProforma,
|
handleIssueProforma,
|
||||||
} = useProformaListPageController();
|
} = useListProformasPageController();
|
||||||
|
|
||||||
const columns = useProformasGridColumns({
|
const columns = useProformasGridColumns({
|
||||||
onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
|
onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
|
||||||
@ -77,6 +76,7 @@ export const ProformaListPage = () => {
|
|||||||
<SimpleSearchInput
|
<SimpleSearchInput
|
||||||
loading={listCtrl.isLoading}
|
loading={listCtrl.isLoading}
|
||||||
onSearchChange={listCtrl.setSearchValue}
|
onSearchChange={listCtrl.setSearchValue}
|
||||||
|
value={listCtrl.search}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
|
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { ProformaDtoAdapter } from "../../adapters";
|
import { ProformaDtoAdapter } from "../../adapters";
|
||||||
import { useUpdateProforma } from "../../hooks/use-proforma-update-mutation";
|
import { useUpdateProforma } from "../../shared/hooks/use-proforma-update-mutation";
|
||||||
import {
|
import {
|
||||||
type Proforma,
|
type Proforma,
|
||||||
type ProformaFormData,
|
type ProformaFormData,
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-proformas.adapter";
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
||||||
|
|
||||||
|
import type { ListProformasResponseDTO } from "../../../../common";
|
||||||
|
import type { ProformaList, ProformaListRow } from "../entities";
|
||||||
|
|
||||||
|
type ListProformasItemDTO = ListProformasResponseDTO["items"][number];
|
||||||
|
|
||||||
|
export const ListProformasAdapter = {
|
||||||
|
fromDTO(pageDto: ListProformasResponseDTO, context?: unknown): ProformaList {
|
||||||
|
return {
|
||||||
|
//...pageDto,
|
||||||
|
page: pageDto.page,
|
||||||
|
per_page: pageDto.per_page,
|
||||||
|
total_pages: pageDto.total_pages,
|
||||||
|
total_items: pageDto.total_items,
|
||||||
|
items: pageDto.items.map((row) => ListProformasRowAdapter.fromDTO(row, context)),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ListProformasRowAdapter = {
|
||||||
|
fromDTO(rowDto: ListProformasItemDTO, context?: unknown): ProformaListRow {
|
||||||
|
return {
|
||||||
|
//...rowDto,
|
||||||
|
id: rowDto.id,
|
||||||
|
company_id: rowDto.company_id,
|
||||||
|
|
||||||
|
customer_id: rowDto.company_id,
|
||||||
|
|
||||||
|
invoice_number: rowDto.invoice_number,
|
||||||
|
status: rowDto.status,
|
||||||
|
series: rowDto.series,
|
||||||
|
|
||||||
|
invoice_date: rowDto.invoice_date,
|
||||||
|
operation_date: rowDto.operation_date,
|
||||||
|
|
||||||
|
language_code: rowDto.language_code,
|
||||||
|
currency_code: rowDto.currency_code,
|
||||||
|
|
||||||
|
reference: rowDto.reference,
|
||||||
|
description: rowDto.description,
|
||||||
|
|
||||||
|
recipient: {
|
||||||
|
tin: rowDto.recipient.tin,
|
||||||
|
name: rowDto.recipient.name,
|
||||||
|
|
||||||
|
street: rowDto.recipient.street,
|
||||||
|
street2: rowDto.recipient.street2,
|
||||||
|
city: rowDto.recipient.city,
|
||||||
|
province: rowDto.recipient.province,
|
||||||
|
postal_code: rowDto.recipient.postal_code,
|
||||||
|
country: rowDto.recipient.country,
|
||||||
|
},
|
||||||
|
|
||||||
|
subtotal_amount: MoneyDTOHelper.toNumber(rowDto.subtotal_amount),
|
||||||
|
subtotal_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(rowDto.subtotal_amount),
|
||||||
|
Number(rowDto.total_amount.scale || 2),
|
||||||
|
rowDto.currency_code,
|
||||||
|
rowDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
discount_percentage: PercentageDTOHelper.toNumber(rowDto.discount_percentage),
|
||||||
|
discount_percentage_fmt: PercentageDTOHelper.toNumericString(rowDto.discount_percentage),
|
||||||
|
|
||||||
|
discount_amount: MoneyDTOHelper.toNumber(rowDto.discount_amount),
|
||||||
|
discount_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(rowDto.discount_amount),
|
||||||
|
Number(rowDto.total_amount.scale || 2),
|
||||||
|
rowDto.currency_code,
|
||||||
|
rowDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
taxable_amount: MoneyDTOHelper.toNumber(rowDto.taxable_amount),
|
||||||
|
taxable_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(rowDto.taxable_amount),
|
||||||
|
Number(rowDto.total_amount.scale || 2),
|
||||||
|
rowDto.currency_code,
|
||||||
|
rowDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
taxes_amount: MoneyDTOHelper.toNumber(rowDto.taxes_amount),
|
||||||
|
taxes_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(rowDto.taxes_amount),
|
||||||
|
Number(rowDto.total_amount.scale || 2),
|
||||||
|
rowDto.currency_code,
|
||||||
|
rowDto.language_code
|
||||||
|
),
|
||||||
|
|
||||||
|
total_amount: MoneyDTOHelper.toNumber(rowDto.total_amount),
|
||||||
|
total_amount_fmt: formatCurrency(
|
||||||
|
MoneyDTOHelper.toNumber(rowDto.total_amount),
|
||||||
|
Number(rowDto.total_amount.scale || 2),
|
||||||
|
rowDto.currency_code,
|
||||||
|
rowDto.language_code
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -4,7 +4,7 @@ export interface ChangeStatusResponse {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changeProformaStatusApi(
|
export async function changeProformaStatusById(
|
||||||
dataSource: IDataSource,
|
dataSource: IDataSource,
|
||||||
proformaId: string,
|
proformaId: string,
|
||||||
newStatus: string
|
newStatus: string
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { IDataSource } from "@erp/core/client";
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
export async function deleteProformaApi(
|
export async function deleteProformaById(
|
||||||
dataSource: IDataSource,
|
dataSource: IDataSource,
|
||||||
proformaId: string
|
proformaId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { Proforma } from "../entities";
|
||||||
|
|
||||||
|
export async function getProformaById(dataSource: IDataSource, signal: AbortSignal, id?: string) {
|
||||||
|
if (!id) throw new Error("proformaId is required");
|
||||||
|
const response = dataSource.getOne<Proforma>("proformas", id, { signal });
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./change-proforma-status-by-id.api";
|
||||||
|
export * from "./delete-proforma-by-ip.api";
|
||||||
|
export * from "./get-proforma-by-ip.api";
|
||||||
|
export * from "./list-proformas.api";
|
||||||
@ -1,18 +1,17 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
import type { IDataSource } from "@erp/core/client";
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
import type { ProformaSummaryPage } from "../../types";
|
import type { ListProformasResponseDTO } from "../../../../common";
|
||||||
|
|
||||||
export async function getProformaListApi(
|
export async function getListProformas(
|
||||||
dataSource: IDataSource,
|
dataSource: IDataSource,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
criteria: CriteriaDTO
|
criteria: CriteriaDTO
|
||||||
) {
|
) {
|
||||||
const response = dataSource.getList<ProformaSummaryPage>("proformas", {
|
const response = dataSource.getList<ListProformasResponseDTO>("proformas", {
|
||||||
signal,
|
signal,
|
||||||
...criteria,
|
...criteria,
|
||||||
});
|
});
|
||||||
|
|
||||||
//return mapProformaList(raw);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./proforma-list.entity";
|
||||||
|
export * from "./proforma-list-row.entity";
|
||||||
|
export * from "./proforma-status.entity";
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
export interface ProformaListRow {
|
||||||
|
id: string;
|
||||||
|
company_id: string;
|
||||||
|
|
||||||
|
customer_id: string;
|
||||||
|
|
||||||
|
invoice_number: string;
|
||||||
|
status: string;
|
||||||
|
series: string;
|
||||||
|
|
||||||
|
invoice_date: string;
|
||||||
|
operation_date: string;
|
||||||
|
|
||||||
|
language_code: string;
|
||||||
|
currency_code: string;
|
||||||
|
|
||||||
|
reference: string;
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
recipient: {
|
||||||
|
tin: string;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
street: string;
|
||||||
|
street2: string;
|
||||||
|
city: string;
|
||||||
|
province: string;
|
||||||
|
postal_code: string;
|
||||||
|
country: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
subtotal_amount: number;
|
||||||
|
subtotal_amount_fmt: string;
|
||||||
|
|
||||||
|
discount_percentage: number;
|
||||||
|
discount_percentage_fmt: string;
|
||||||
|
|
||||||
|
discount_amount: number;
|
||||||
|
discount_amount_fmt: string;
|
||||||
|
|
||||||
|
taxable_amount: number;
|
||||||
|
taxable_amount_fmt: string;
|
||||||
|
|
||||||
|
taxes_amount: number;
|
||||||
|
taxes_amount_fmt: string;
|
||||||
|
|
||||||
|
total_amount: number;
|
||||||
|
total_amount_fmt: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import type { ProformaListRow } from "./proforma-list-row.entity";
|
||||||
|
|
||||||
|
export interface ProformaList {
|
||||||
|
items: ProformaListRow[];
|
||||||
|
total_pages: number;
|
||||||
|
total_items: number;
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
export enum PROFORMA_STATUS {
|
||||||
|
DRAFT = "draft",
|
||||||
|
SENT = "sent",
|
||||||
|
APPROVED = "approved",
|
||||||
|
REJECTED = "rejected",
|
||||||
|
ISSUED = "issued",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transiciones válidas según reglas del dominio
|
||||||
|
export const PROFORMA_STATUS_TRANSITIONS: Record<PROFORMA_STATUS, PROFORMA_STATUS[]> = {
|
||||||
|
[PROFORMA_STATUS.DRAFT]: [PROFORMA_STATUS.SENT],
|
||||||
|
[PROFORMA_STATUS.SENT]: [PROFORMA_STATUS.APPROVED, PROFORMA_STATUS.REJECTED],
|
||||||
|
[PROFORMA_STATUS.APPROVED]: [PROFORMA_STATUS.ISSUED, PROFORMA_STATUS.DRAFT],
|
||||||
|
[PROFORMA_STATUS.REJECTED]: [PROFORMA_STATUS.DRAFT],
|
||||||
|
[PROFORMA_STATUS.ISSUED]: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProformaStatus = `${PROFORMA_STATUS}`;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./use-change-proforma-status-mutation";
|
||||||
|
export * from "./use-delete-proforma-mutation";
|
||||||
|
export * from "./use-list-proformas-query";
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { changeProformaStatusById } from "../api/change-proforma-status-by-id.api";
|
||||||
|
import type { PROFORMA_STATUS } from "../entities";
|
||||||
|
|
||||||
|
import { LIST_PROFORMAS_QUERY_KEY_PREFIX } from "./use-list-proformas-query";
|
||||||
|
|
||||||
|
interface ChangeProformaStatusPayload {
|
||||||
|
proformaId: string;
|
||||||
|
newStatus: PROFORMA_STATUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChangeProformaStatusMutation() {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: ({ proformaId, newStatus }: ChangeProformaStatusPayload) =>
|
||||||
|
changeProformaStatusById(dataSource, proformaId, newStatus),
|
||||||
|
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
changeProformaStatus: mutation.mutateAsync,
|
||||||
|
isPending: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
reset: mutation.reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { deleteProformaById } from "../api";
|
||||||
|
|
||||||
|
import { LIST_PROFORMAS_QUERY_KEY_PREFIX } from "./use-list-proformas-query";
|
||||||
|
|
||||||
|
interface DeleteProformaPayload {
|
||||||
|
proformaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteProformaMutation() {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation<void, Error, DeleteProformaPayload>({
|
||||||
|
mutationFn: ({ proformaId }: DeleteProformaPayload) =>
|
||||||
|
deleteProformaById(dataSource, proformaId),
|
||||||
|
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteProforma: mutation.mutateAsync,
|
||||||
|
isPending: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
reset: mutation.reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
||||||
|
import {
|
||||||
|
type DefaultError,
|
||||||
|
type QueryClient,
|
||||||
|
type QueryKey,
|
||||||
|
type UseQueryResult,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { ListProformasAdapter } from "../adapters";
|
||||||
|
import { getListProformas } from "../api";
|
||||||
|
import type { ProformaList, ProformaListRow } from "../entities";
|
||||||
|
|
||||||
|
export const LIST_PROFORMAS_QUERY_KEY_PREFIX = ["proformas"] as const;
|
||||||
|
export const LIST_PROFORMAS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
||||||
|
LIST_PROFORMAS_QUERY_KEY_PREFIX,
|
||||||
|
{
|
||||||
|
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
|
||||||
|
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
|
||||||
|
q: criteria?.q ?? "",
|
||||||
|
filters: criteria?.filters ?? [],
|
||||||
|
orderBy: criteria?.orderBy ?? "",
|
||||||
|
order: criteria?.order ?? "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type useListProformasQueryOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
criteria?: Partial<CriteriaDTO>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListProformasQuery = (
|
||||||
|
options?: useListProformasQueryOptions
|
||||||
|
): UseQueryResult<ProformaList, DefaultError> => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const enabled = options?.enabled ?? true;
|
||||||
|
const criteria = options?.criteria ?? {};
|
||||||
|
|
||||||
|
return useQuery<ProformaList, DefaultError>({
|
||||||
|
queryKey: LIST_PROFORMAS_QUERY_KEY(criteria),
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const dto = await getListProformas(dataSource, signal, criteria);
|
||||||
|
return ListProformasAdapter.fromDTO(dto);
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function cancelListProformasQueries(qc: QueryClient) {
|
||||||
|
return qc.cancelQueries({ queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidateListProformasQueries(qc: QueryClient) {
|
||||||
|
return qc.invalidateQueries({ queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllListProformasQueryKeys(qc: QueryClient): QueryKey[] {
|
||||||
|
const entries = qc.getQueriesData<ProformaList>({
|
||||||
|
queryKey: LIST_PROFORMAS_QUERY_KEY_PREFIX,
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries.map(([key]) => key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upsertProformaInListProformasCaches(
|
||||||
|
qc: QueryClient,
|
||||||
|
proforma: Pick<ProformaListRow, "id"> & Partial<ProformaListRow>
|
||||||
|
) {
|
||||||
|
const keys = getAllListProformasQueryKeys(qc);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const page = qc.getQueryData<ProformaList>(key);
|
||||||
|
if (!page) continue;
|
||||||
|
|
||||||
|
const index = page.items.findIndex((row) => row.id === proforma.id);
|
||||||
|
if (index === -1) continue;
|
||||||
|
|
||||||
|
const nextItems = page.items.slice();
|
||||||
|
nextItems[index] = {
|
||||||
|
...page.items[index],
|
||||||
|
...proforma,
|
||||||
|
};
|
||||||
|
|
||||||
|
qc.setQueryData<ProformaList>(key, {
|
||||||
|
...page,
|
||||||
|
items: nextItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeProformaFromListProformasCaches(
|
||||||
|
qc: QueryClient,
|
||||||
|
proformaId: string
|
||||||
|
): Array<{ key: QueryKey; page?: ProformaList }> {
|
||||||
|
const snapshots = getAllListProformasQueryKeys(qc).map((key) => ({
|
||||||
|
key,
|
||||||
|
page: qc.getQueryData<ProformaList>(key),
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const { key, page } of snapshots) {
|
||||||
|
if (!page) continue;
|
||||||
|
|
||||||
|
qc.setQueryData<ProformaList>(key, {
|
||||||
|
...page,
|
||||||
|
items: page.items.filter((row) => row.id !== proformaId),
|
||||||
|
total_items: Math.max(0, page.total_items - 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshots;
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import {
|
||||||
|
type DefaultError,
|
||||||
|
type QueryKey,
|
||||||
|
type UseQueryResult,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { type Proforma, getProformaById } from "../../api";
|
||||||
|
|
||||||
|
export const PROFORMA_QUERY_KEY = (proformaId?: string): QueryKey => [
|
||||||
|
"proformas:detail",
|
||||||
|
{
|
||||||
|
proformaId,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type ProformaQueryOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProformaGetQuery = (
|
||||||
|
proformaId?: string,
|
||||||
|
options?: ProformaQueryOptions
|
||||||
|
): UseQueryResult<Proforma, DefaultError> => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const enabled = options?.enabled ?? Boolean(proformaId);
|
||||||
|
|
||||||
|
return useQuery<Proforma, DefaultError>({
|
||||||
|
queryKey: PROFORMA_QUERY_KEY(proformaId),
|
||||||
|
queryFn: async ({ signal }) => getProformaById(dataSource, signal, proformaId),
|
||||||
|
enabled,
|
||||||
|
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*export function invalidateProformaDetailCache(qc: QueryClient, id: string) {
|
||||||
|
return qc.invalidateQueries({
|
||||||
|
queryKey: getProformaQueryKey(id ?? "unknown"),
|
||||||
|
exact: Boolean(id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setProformaDetailCache(qc: QueryClient, id: string, data: unknown) {
|
||||||
|
qc.setQueryData(getProformaQueryKey(id), data);
|
||||||
|
}
|
||||||
|
*/
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { UpdateProformaByIdRequestSchema } from "../../../../common";
|
||||||
|
import type { ProformaFormData } from "../../update/types";
|
||||||
|
|
||||||
|
export const PROFORMA_UPDATE_KEY = ["proformas", "update"] as const;
|
||||||
|
|
||||||
|
type UpdateProformaContext = {};
|
||||||
|
|
||||||
|
type UpdateProformaPayload = {
|
||||||
|
id: string;
|
||||||
|
data: Partial<ProformaFormData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProformaUpdateMutation = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const schema = UpdateProformaByIdRequestSchema;
|
||||||
|
|
||||||
|
return useMutation<Proforma, DefaultError, UpdateProformaPayload, UpdateProformaContext>({
|
||||||
|
mutationKey: PROFORMA_UPDATE_KEY,
|
||||||
|
|
||||||
|
mutationFn: async (payload) => {
|
||||||
|
const { id: proformaId, data } = payload;
|
||||||
|
if (!proformaId) {
|
||||||
|
throw new Error("proformaId is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = schema.safeParse(data);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await dataSource.updateOne("proformas", proformaId, data);
|
||||||
|
return updated as Proforma;
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccess: (updated: Proforma, variables) => {
|
||||||
|
const { id: proformaId } = updated;
|
||||||
|
|
||||||
|
// Invalida el listado para refrescar desde servidor
|
||||||
|
//invalidateProformaListCache(queryClient);
|
||||||
|
|
||||||
|
// Actualiza detalle
|
||||||
|
//setProformaDetailCache(queryClient, proformaId, updated);
|
||||||
|
|
||||||
|
// Actualiza todas las páginas donde aparezca
|
||||||
|
//upsertProformaIntoListCaches(queryClient, { ...updated });
|
||||||
|
},
|
||||||
|
|
||||||
|
onSettled: () => {
|
||||||
|
// Refresca todos los listados
|
||||||
|
//invalidateProformaListCache(queryClient);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./api";
|
||||||
|
export * from "./entities";
|
||||||
|
export * from "./hooks";
|
||||||
|
export * from "./ui";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./proforma-layout";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./blocks";
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./proforma.api.schema";
|
|
||||||
export * from "./proforma.form.schema";
|
|
||||||
export * from "./proforma-status";
|
|
||||||
export * from "./proforma-summary.web.schema";
|
|
||||||
@ -7,25 +7,6 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export enum PROFORMA_STATUS {
|
|
||||||
DRAFT = "draft",
|
|
||||||
SENT = "sent",
|
|
||||||
APPROVED = "approved",
|
|
||||||
REJECTED = "rejected",
|
|
||||||
ISSUED = "issued",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transiciones válidas según reglas del dominio
|
|
||||||
export const PROFORMA_STATUS_TRANSITIONS: Record<PROFORMA_STATUS, PROFORMA_STATUS[]> = {
|
|
||||||
[PROFORMA_STATUS.DRAFT]: [PROFORMA_STATUS.SENT],
|
|
||||||
[PROFORMA_STATUS.SENT]: [PROFORMA_STATUS.APPROVED, PROFORMA_STATUS.REJECTED],
|
|
||||||
[PROFORMA_STATUS.APPROVED]: [PROFORMA_STATUS.ISSUED, PROFORMA_STATUS.DRAFT],
|
|
||||||
[PROFORMA_STATUS.REJECTED]: [PROFORMA_STATUS.DRAFT],
|
|
||||||
[PROFORMA_STATUS.ISSUED]: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ProformaStatus = `${PROFORMA_STATUS}`;
|
|
||||||
|
|
||||||
export const getProformaStatusButtonVariant = (
|
export const getProformaStatusButtonVariant = (
|
||||||
status: ProformaStatus
|
status: ProformaStatus
|
||||||
): "default" | "secondary" | "outline" | "destructive" => {
|
): "default" | "secondary" | "outline" | "destructive" => {
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
import {
|
|
||||||
CreateProformaRequestSchema,
|
|
||||||
GetProformaByIdResponseSchema,
|
|
||||||
type ListProformasResponseDTO,
|
|
||||||
UpdateProformaByIdRequestSchema,
|
|
||||||
} from "@erp/customer-invoices/common";
|
|
||||||
import type { ArrayElement } from "@repo/rdx-utils";
|
|
||||||
import type { z } from "zod/v4";
|
|
||||||
|
|
||||||
// Proformas
|
|
||||||
export const ProformaSchema = GetProformaByIdResponseSchema.omit({
|
|
||||||
metadata: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Proforma = z.infer<typeof ProformaSchema>;
|
|
||||||
export type ProformaRecipient = Proforma["recipient"];
|
|
||||||
export type ProformaItem = ArrayElement<Proforma["items"]>;
|
|
||||||
|
|
||||||
export const CreateProformaSchema = CreateProformaRequestSchema;
|
|
||||||
export const UpdateProformaSchema = UpdateProformaByIdRequestSchema;
|
|
||||||
|
|
||||||
export type CreateProformaInput = z.infer<typeof CreateProformaSchema>; // Cuerpo para crear
|
|
||||||
export type UpdateProformaInput = z.infer<typeof UpdateProformaSchema>; // Cuerpo para actualizar
|
|
||||||
|
|
||||||
// Resultado de consulta con criteria (paginado, etc.)
|
|
||||||
export type ProformaSummaryPage = Omit<ListProformasResponseDTO, "metadata">;
|
|
||||||
export type ProformaSummary = Omit<ArrayElement<ProformaSummaryPage["items"]>, "metadata">;
|
|
||||||
@ -1,2 +1 @@
|
|||||||
export * from "./proforma-layout";
|
|
||||||
export * from "./proforma-tax-summary";
|
export * from "./proforma-tax-summary";
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-proforma-update-page.controller";
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client";
|
||||||
|
import { useHookForm } from "@erp/core/hooks";
|
||||||
|
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||||
|
import { useEffect, useId, useMemo } from "react";
|
||||||
|
import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
import type { Proforma } from "../../api";
|
||||||
|
import { type ProformaFormData, ProformaFormSchema, defaultProformaFormData } from "../types";
|
||||||
|
|
||||||
|
export interface UseProformaUpdateControllerOptions {
|
||||||
|
onUpdated?(updated: Proforma): void;
|
||||||
|
successToasts?: boolean; // mostrar o no toast automáticcamente
|
||||||
|
|
||||||
|
onError?(error: Error, patchData: ReturnType<typeof pickFormDirtyValues>): void;
|
||||||
|
errorToasts?: boolean; // mostrar o no toast automáticcamente
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProformaUpdateController = (
|
||||||
|
proformaId?: string,
|
||||||
|
options?: UseProformaUpdateControllerOptions
|
||||||
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const formId = useId(); // id único por instancia
|
||||||
|
|
||||||
|
// 1) Estado de carga del cliente (query)
|
||||||
|
const {
|
||||||
|
data: proformaData,
|
||||||
|
isLoading,
|
||||||
|
isError: isLoadError,
|
||||||
|
error: loadError,
|
||||||
|
} = useProformaGetQuery(proformaId, { enabled: Boolean(proformaId) });
|
||||||
|
|
||||||
|
// 2) Estado de creación (mutación)
|
||||||
|
const {
|
||||||
|
mutateAsync,
|
||||||
|
isPending: isUpdating,
|
||||||
|
isError: isUpdateError,
|
||||||
|
error: updateError,
|
||||||
|
} = useProformaUpdateMutation();
|
||||||
|
|
||||||
|
const initialValues = useMemo(() => proformaData ?? defaultProformaFormData, [proformaData]);
|
||||||
|
|
||||||
|
// 3) Form hook
|
||||||
|
const form = useHookForm<ProformaFormData>({
|
||||||
|
resolverSchema: ProformaFormSchema,
|
||||||
|
initialValues,
|
||||||
|
disabled: isLoading || isUpdating,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Reiniciar el form al recibir datos */
|
||||||
|
useEffect(() => {
|
||||||
|
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
||||||
|
if (proformaData) form.reset(proformaData, { keepDirty: false });
|
||||||
|
}, [proformaData, form]);
|
||||||
|
|
||||||
|
/** Handlers */
|
||||||
|
|
||||||
|
const resetForm = () => form.reset(proformaData ?? defaultProformaFormData);
|
||||||
|
|
||||||
|
// Versión sincronizada
|
||||||
|
const submitHandler = form.handleSubmit(
|
||||||
|
async (formData) => {
|
||||||
|
if (!proformaId) {
|
||||||
|
showErrorToast(t("pages.update.error.title"), "Falta el ID de la proforma");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dirtyFields } = form.formState;
|
||||||
|
if (!formHasAnyDirty(dirtyFields)) {
|
||||||
|
showWarningToast(t("pages.update.error.no_changes"), "No hay cambios para guardar");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchData = pickFormDirtyValues(formData, dirtyFields);
|
||||||
|
const previousData = proformaData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Enviamos cambios al servidor
|
||||||
|
const updated = await mutateAsync({ id: proformaId, data: patchData });
|
||||||
|
|
||||||
|
// Ha ido bien -> actualizamos form con datos reales
|
||||||
|
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
||||||
|
form.reset(updated, { keepDirty: false });
|
||||||
|
|
||||||
|
if (options?.successToasts !== false) {
|
||||||
|
showSuccessToast(
|
||||||
|
t("pages.update.success.title", "Proforma modificada"),
|
||||||
|
t("pages.update.success.message", "Se ha modificado correctamente.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
options?.onUpdated?.(updated);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Algo ha fallado -> revertimos cambios
|
||||||
|
form.reset(previousData ?? defaultProformaFormData);
|
||||||
|
if (options?.errorToasts !== false) {
|
||||||
|
showErrorToast(t("pages.update.error.title"), error.message);
|
||||||
|
}
|
||||||
|
options?.onError?.(error, patchData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(errors: FieldErrors<ProformaFormData>) => {
|
||||||
|
const firstKey = Object.keys(errors)[0] as keyof ProformaFormData | undefined;
|
||||||
|
if (firstKey) document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
|
||||||
|
|
||||||
|
showWarningToast(
|
||||||
|
t("forms.validation.title", "Revisa los campos"),
|
||||||
|
t("forms.validation.message", "Hay errores de validación en el formulario.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Evento onSubmit ya preparado para el <form>
|
||||||
|
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM
|
||||||
|
submitHandler(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// form
|
||||||
|
form,
|
||||||
|
formId,
|
||||||
|
|
||||||
|
// handlers del form
|
||||||
|
onSubmit,
|
||||||
|
resetForm,
|
||||||
|
|
||||||
|
// carga de datos
|
||||||
|
proformaData,
|
||||||
|
isLoading,
|
||||||
|
isLoadError,
|
||||||
|
loadError,
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
isUpdating,
|
||||||
|
isUpdateError,
|
||||||
|
updateError,
|
||||||
|
|
||||||
|
// Por comodidad
|
||||||
|
FormProvider,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./ui";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./pages";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./proforma-update-page";
|
||||||
@ -5,33 +5,56 @@ import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useProformaQuery } from "../../hooks";
|
import { useProformaUpdateController } from "../../controllers";
|
||||||
|
|
||||||
import { ProformaProvider } from "./context";
|
import { ProformaProvider } from "./context";
|
||||||
import { ProformaUpdateComp } from "./proforma-update-comp";
|
import { ProformaUpdateComp } from "./proforma-update-comp";
|
||||||
import { ProformaEditorSkeleton } from "./ui/components";
|
import { ProformaEditorSkeleton } from "./ui/components";
|
||||||
|
|
||||||
export const ProformaUpdatePage = () => {
|
export const ProformaUpdatePage = () => {
|
||||||
|
const initialProformaId = useUrlParamId();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const proforma_id = useUrlParamId();
|
|
||||||
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
||||||
|
|
||||||
const proformaQuery = useProformaQuery(proforma_id, { enabled: !!proforma_id });
|
const {
|
||||||
const { data: proformaData, isLoading, isError, error } = proformaQuery;
|
form,
|
||||||
|
formId,
|
||||||
|
onSubmit,
|
||||||
|
resetForm,
|
||||||
|
|
||||||
|
proformaData,
|
||||||
|
isLoading,
|
||||||
|
isLoadError,
|
||||||
|
loadError,
|
||||||
|
|
||||||
|
isUpdating,
|
||||||
|
isUpdateError,
|
||||||
|
updateError,
|
||||||
|
|
||||||
|
FormProvider,
|
||||||
|
} = useProformaUpdateController(initialProformaId, {});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <ProformaEditorSkeleton />;
|
return <ProformaEditorSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !proformaData) {
|
if (isLoadError) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
message={(error as Error)?.message || "Error al cargar la factura"}
|
message={
|
||||||
title={t("pages.update.loadErrorTitle")}
|
(loadError as Error)?.message ??
|
||||||
|
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||||
|
}
|
||||||
|
title={t("pages.update.loadErrorTitle", "No se pudo cargar la proforma")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
<BackHistoryButton />
|
<BackHistoryButton />
|
||||||
|
</div>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ import { useState } from "react";
|
|||||||
import DataTable, { type TableColumn } from "react-data-table-component";
|
import DataTable, { type TableColumn } from "react-data-table-component";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
import type { ListCustomersResponseDTO } from "../../common";
|
import type { ListCustomersResponseDTO } from "../../../common";
|
||||||
import { useCustomerListQuery } from "../hooks";
|
import { useCustomerListQuery } from "../hooks";
|
||||||
|
|
||||||
type Customer = ListCustomersResponseDTO["items"][number];
|
type Customer = ListCustomersResponseDTO["items"][number];
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { useCallback, useId } from "react";
|
import { useCallback, useId } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useCustomerCreateController } from "../../pages/create/use-customer-create-controller";
|
import { useCustomerCreateController } from "../../pages/create/use-customer-create-controller";
|
||||||
import type { CustomerFormData } from "../../schemas";
|
import type { CustomerFormData } from "../../schemas";
|
||||||
import { CustomerEditForm } from "../editor";
|
import { CustomerEditForm } from "../editor";
|
||||||
@ -1,3 +1,3 @@
|
|||||||
//export * from "./client-selector-modal";
|
//export * from "./client-selector-modal";
|
||||||
//export * from "./customer-modal-selector";
|
//export * from "./customer-modal-selector";
|
||||||
export * from "./editor";
|
//export * from "./editor";
|
||||||
@ -2,8 +2,8 @@ import { useDataSource } from "@erp/core/hooks";
|
|||||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { CreateCustomerRequestSchema } from "../../common";
|
import { CreateCustomerRequestSchema } from "../../../common";
|
||||||
import { toValidationErrors } from "../common/hooks/toValidationErrors";
|
import { toValidationErrors } from "../../shared/hooks/toValidationErrors";
|
||||||
import type { Customer, CustomerFormData } from "../schemas";
|
import type { Customer, CustomerFormData } from "../schemas";
|
||||||
|
|
||||||
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
||||||
@ -1,6 +1,12 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { DefaultError, QueryClient, type QueryKey, useQuery } from "@tanstack/react-query";
|
import {
|
||||||
import { Customer } from "../schemas";
|
type DefaultError,
|
||||||
|
type QueryClient,
|
||||||
|
type QueryKey,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import type { Customer } from "../schemas";
|
||||||
|
|
||||||
export const CUSTOMER_DETAIL_SCOPE = "customers:detail" as const;
|
export const CUSTOMER_DETAIL_SCOPE = "customers:detail" as const;
|
||||||
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { CustomersContext, CustomersContextType } from "../context";
|
|
||||||
|
import { CustomersContext, type CustomersContextType } from "../context";
|
||||||
|
|
||||||
export const useCustomersContext = (): CustomersContextType => {
|
export const useCustomersContext = (): CustomersContextType => {
|
||||||
const context = useContext(CustomersContext);
|
const context = useContext(CustomersContext);
|
||||||
@ -1,6 +1,13 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { DefaultError, QueryKey, useMutation, useQueryClient } from "@tanstack/react-query";
|
import {
|
||||||
import { CustomersPage } from "../schemas";
|
type DefaultError,
|
||||||
|
type QueryKey,
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import type { CustomersPage } from "../schemas";
|
||||||
|
|
||||||
import { cancelCustomerListQueries, deleteCustomerIntoListCaches } from "./use-customer-list-query";
|
import { cancelCustomerListQueries, deleteCustomerIntoListCaches } from "./use-customer-list-query";
|
||||||
import { invalidateCustomerDetailCache } from "./use-customer-query";
|
import { invalidateCustomerDetailCache } from "./use-customer-query";
|
||||||
|
|
||||||
@ -2,8 +2,8 @@ import { useDataSource } from "@erp/core/hooks";
|
|||||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { UpdateCustomerByIdRequestSchema } from "../../common";
|
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
||||||
import { toValidationErrors } from "../common/hooks/toValidationErrors";
|
import { toValidationErrors } from "../../shared/hooks/toValidationErrors";
|
||||||
import type { Customer, CustomerFormData } from "../schemas";
|
import type { Customer, CustomerFormData } from "../schemas";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -3,8 +3,8 @@ import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks
|
|||||||
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
import { CustomerEditForm, ErrorAlert } from "../../components";
|
import { CustomerEditForm, ErrorAlert } from "../../components";
|
||||||
import { useTranslation } from "../../i18n";
|
|
||||||
|
|
||||||
import { useCustomerCreateController } from "./use-customer-create-controller";
|
import { useCustomerCreateController } from "./use-customer-create-controller";
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user