erp-construccion-frontend-v2/web/src/hooks/usePresupuestos.ts
Adrian Flores Cortes 0d0d52ac29 [SPRINT-3] feat: Presupuesto detail page with version management
- Add PresupuestoDetailPage with partidas table
- Add version history sidebar
- Add partida CRUD operations
- Add approval/rejection workflow
- Add PDF/Excel export buttons
- Enhance presupuestos.api.ts with version management APIs
- Add 8 new hooks in usePresupuestos.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 01:15:19 -06:00

560 lines
18 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import toast from 'react-hot-toast';
import { ApiError } from '../services/api';
import {
conceptosApi,
ConceptoFilters,
CreateConceptoDto,
UpdateConceptoDto,
presupuestosApi,
PresupuestoFilters,
CreatePresupuestoDto,
UpdatePresupuestoDto,
CreatePresupuestoPartidaDto,
UpdatePresupuestoPartidaDto,
RejectPresupuestoDto,
estimacionesApi,
EstimacionFilters,
CreateEstimacionDto,
UpdateEstimacionDto,
CreateEstimacionPartidaDto,
UpdateEstimacionPartidaDto,
CreateGeneradorDto,
UpdateGeneradorDto,
} from '../services/presupuestos';
// ==================== QUERY KEYS ====================
export const presupuestosKeys = {
conceptos: {
all: ['presupuestos', 'conceptos'] as const,
list: (filters?: ConceptoFilters) => [...presupuestosKeys.conceptos.all, 'list', filters] as const,
detail: (id: string) => [...presupuestosKeys.conceptos.all, 'detail', id] as const,
tree: (rootId?: string) => [...presupuestosKeys.conceptos.all, 'tree', rootId] as const,
},
presupuestos: {
all: ['presupuestos', 'presupuestos'] as const,
list: (filters?: PresupuestoFilters) => [...presupuestosKeys.presupuestos.all, 'list', filters] as const,
detail: (id: string) => [...presupuestosKeys.presupuestos.all, 'detail', id] as const,
},
estimaciones: {
all: ['presupuestos', 'estimaciones'] as const,
list: (filters?: EstimacionFilters) => [...presupuestosKeys.estimaciones.all, 'list', filters] as const,
detail: (id: string) => [...presupuestosKeys.estimaciones.all, 'detail', id] as const,
},
};
// ==================== ERROR HANDLER ====================
const handleError = (error: AxiosError<ApiError>) => {
const message = error.response?.data?.message || 'Ha ocurrido un error';
toast.error(message);
};
// ==================== CONCEPTOS ====================
export function useConceptos(filters?: ConceptoFilters) {
return useQuery({
queryKey: presupuestosKeys.conceptos.list(filters),
queryFn: () => conceptosApi.list(filters),
});
}
export function useConcepto(id: string) {
return useQuery({
queryKey: presupuestosKeys.conceptos.detail(id),
queryFn: () => conceptosApi.get(id),
enabled: !!id,
});
}
export function useConceptosTree(rootId?: string) {
return useQuery({
queryKey: presupuestosKeys.conceptos.tree(rootId),
queryFn: () => conceptosApi.getTree(rootId),
});
}
export function useCreateConcepto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateConceptoDto) => conceptosApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.conceptos.all });
toast.success('Concepto creado exitosamente');
},
onError: handleError,
});
}
export function useUpdateConcepto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateConceptoDto }) =>
conceptosApi.update(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.conceptos.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.conceptos.detail(id) });
toast.success('Concepto actualizado');
},
onError: handleError,
});
}
export function useDeleteConcepto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => conceptosApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.conceptos.all });
toast.success('Concepto eliminado');
},
onError: handleError,
});
}
// ==================== PRESUPUESTOS ====================
export function usePresupuestos(filters?: PresupuestoFilters) {
return useQuery({
queryKey: presupuestosKeys.presupuestos.list(filters),
queryFn: () => presupuestosApi.list(filters),
});
}
export function usePresupuesto(id: string) {
return useQuery({
queryKey: presupuestosKeys.presupuestos.detail(id),
queryFn: () => presupuestosApi.get(id),
enabled: !!id,
});
}
export function useCreatePresupuesto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreatePresupuestoDto) => presupuestosApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.all });
toast.success('Presupuesto creado exitosamente');
},
onError: handleError,
});
}
export function useUpdatePresupuesto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdatePresupuestoDto }) =>
presupuestosApi.update(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.detail(id) });
toast.success('Presupuesto actualizado');
},
onError: handleError,
});
}
export function useDeletePresupuesto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => presupuestosApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.all });
toast.success('Presupuesto eliminado');
},
onError: handleError,
});
}
export function useApprovePresupuesto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => presupuestosApi.approve(id),
onSuccess: (_, id) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.detail(id) });
toast.success('Presupuesto aprobado');
},
onError: handleError,
});
}
export function useDuplicatePresupuesto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => presupuestosApi.duplicate(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.all });
toast.success('Presupuesto duplicado exitosamente');
},
onError: handleError,
});
}
export function usePresupuestoWithPartidas(id: string) {
return useQuery({
queryKey: [...presupuestosKeys.presupuestos.detail(id), 'partidas'],
queryFn: () => presupuestosApi.getWithPartidas(id),
enabled: !!id,
});
}
export function usePresupuestoVersions(id: string) {
return useQuery({
queryKey: [...presupuestosKeys.presupuestos.detail(id), 'versions'],
queryFn: () => presupuestosApi.getVersions(id),
enabled: !!id,
});
}
export function useCreatePresupuestoVersion() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, notas }: { id: string; notas?: string }) =>
presupuestosApi.createVersion(id, notas),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.detail(id) });
toast.success('Nueva version creada exitosamente');
},
onError: handleError,
});
}
export function useRejectPresupuesto() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: RejectPresupuestoDto }) =>
presupuestosApi.reject(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.detail(id) });
toast.success('Presupuesto rechazado');
},
onError: handleError,
});
}
export function useAddPresupuestoPartida() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ presupuestoId, data }: { presupuestoId: string; data: CreatePresupuestoPartidaDto }) =>
presupuestosApi.addPartida(presupuestoId, data),
onSuccess: (_, { presupuestoId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.detail(presupuestoId) });
toast.success('Partida agregada al presupuesto');
},
onError: handleError,
});
}
export function useUpdatePresupuestoPartida() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
presupuestoId,
partidaId,
data,
}: {
presupuestoId: string;
partidaId: string;
data: UpdatePresupuestoPartidaDto;
}) => presupuestosApi.updatePartida(presupuestoId, partidaId, data),
onSuccess: (_, { presupuestoId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.detail(presupuestoId) });
toast.success('Partida actualizada');
},
onError: handleError,
});
}
export function useDeletePresupuestoPartida() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ presupuestoId, partidaId }: { presupuestoId: string; partidaId: string }) =>
presupuestosApi.deletePartida(presupuestoId, partidaId),
onSuccess: (_, { presupuestoId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.presupuestos.detail(presupuestoId) });
toast.success('Partida eliminada del presupuesto');
},
onError: handleError,
});
}
export function useExportPresupuestoPdf() {
return useMutation({
mutationFn: (id: string) => presupuestosApi.exportPdf(id),
onSuccess: (blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `presupuesto-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('PDF exportado exitosamente');
},
onError: handleError,
});
}
export function useExportPresupuestoExcel() {
return useMutation({
mutationFn: (id: string) => presupuestosApi.exportExcel(id),
onSuccess: (blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `presupuesto-${Date.now()}.xlsx`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('Excel exportado exitosamente');
},
onError: handleError,
});
}
// ==================== ESTIMACIONES ====================
export function useEstimaciones(filters?: EstimacionFilters) {
return useQuery({
queryKey: presupuestosKeys.estimaciones.list(filters),
queryFn: () => estimacionesApi.list(filters),
});
}
export function useEstimacion(id: string) {
return useQuery({
queryKey: presupuestosKeys.estimaciones.detail(id),
queryFn: () => estimacionesApi.get(id),
enabled: !!id,
});
}
export function useCreateEstimacion() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateEstimacionDto) => estimacionesApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.all });
toast.success('Estimacion creada exitosamente');
},
onError: handleError,
});
}
export function useUpdateEstimacion() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateEstimacionDto }) =>
estimacionesApi.update(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(id) });
toast.success('Estimacion actualizada');
},
onError: handleError,
});
}
export function useDeleteEstimacion() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => estimacionesApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.all });
toast.success('Estimacion eliminada');
},
onError: handleError,
});
}
// ==================== ESTIMACIONES WORKFLOW ====================
export function useSubmitEstimacion() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => estimacionesApi.submit(id),
onSuccess: (_, id) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(id) });
toast.success('Estimacion enviada a revision');
},
onError: handleError,
});
}
export function useApproveEstimacion() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, montoAprobado }: { id: string; montoAprobado?: number }) =>
estimacionesApi.approve(id, montoAprobado),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(id) });
toast.success('Estimacion aprobada');
},
onError: handleError,
});
}
export function useRejectEstimacion() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, motivo }: { id: string; motivo: string }) =>
estimacionesApi.reject(id, motivo),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(id) });
toast.success('Estimacion rechazada');
},
onError: handleError,
});
}
export function useMarkEstimacionInvoiced() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, montoFacturado }: { id: string; montoFacturado: number }) =>
estimacionesApi.markAsInvoiced(id, montoFacturado),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(id) });
toast.success('Estimacion marcada como facturada');
},
onError: handleError,
});
}
export function useMarkEstimacionPaid() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, montoCobrado }: { id: string; montoCobrado: number }) =>
estimacionesApi.markAsPaid(id, montoCobrado),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.all });
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(id) });
toast.success('Estimacion marcada como cobrada');
},
onError: handleError,
});
}
// ==================== ESTIMACIONES PARTIDAS ====================
export function useAddEstimacionPartida() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ estimacionId, data }: { estimacionId: string; data: CreateEstimacionPartidaDto }) =>
estimacionesApi.addPartida(estimacionId, data),
onSuccess: (_, { estimacionId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(estimacionId) });
toast.success('Partida agregada a la estimacion');
},
onError: handleError,
});
}
export function useUpdateEstimacionPartida() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
estimacionId,
partidaId,
data,
}: {
estimacionId: string;
partidaId: string;
data: UpdateEstimacionPartidaDto;
}) => estimacionesApi.updatePartida(estimacionId, partidaId, data),
onSuccess: (_, { estimacionId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(estimacionId) });
toast.success('Partida actualizada');
},
onError: handleError,
});
}
export function useRemoveEstimacionPartida() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ estimacionId, partidaId }: { estimacionId: string; partidaId: string }) =>
estimacionesApi.removePartida(estimacionId, partidaId),
onSuccess: (_, { estimacionId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(estimacionId) });
toast.success('Partida eliminada de la estimacion');
},
onError: handleError,
});
}
// ==================== ESTIMACIONES GENERADORES ====================
export function useAddGenerador() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
estimacionId,
partidaId,
data,
}: {
estimacionId: string;
partidaId: string;
data: CreateGeneradorDto;
}) => estimacionesApi.addGenerador(estimacionId, partidaId, data),
onSuccess: (_, { estimacionId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(estimacionId) });
toast.success('Generador agregado');
},
onError: handleError,
});
}
export function useUpdateGenerador() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
estimacionId,
partidaId,
generadorId,
data,
}: {
estimacionId: string;
partidaId: string;
generadorId: string;
data: UpdateGeneradorDto;
}) => estimacionesApi.updateGenerador(estimacionId, partidaId, generadorId, data),
onSuccess: (_, { estimacionId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(estimacionId) });
toast.success('Generador actualizado');
},
onError: handleError,
});
}
export function useRemoveGenerador() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
estimacionId,
partidaId,
generadorId,
}: {
estimacionId: string;
partidaId: string;
generadorId: string;
}) => estimacionesApi.removeGenerador(estimacionId, partidaId, generadorId),
onSuccess: (_, { estimacionId }) => {
queryClient.invalidateQueries({ queryKey: presupuestosKeys.estimaciones.detail(estimacionId) });
toast.success('Generador eliminado');
},
onError: handleError,
});
}