- 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>
560 lines
18 KiB
TypeScript
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,
|
|
});
|
|
}
|