[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>
This commit is contained in:
parent
5083292fbd
commit
0d0d52ac29
@ -14,7 +14,7 @@ import {
|
||||
} from './pages/admin/proyectos';
|
||||
import { ManzanasPage } from './pages/admin/proyectos/ManzanasPage';
|
||||
import { DashboardPage } from './pages/admin/dashboard';
|
||||
import { ConceptosPage, PresupuestosPage, EstimacionesPage } from './pages/admin/presupuestos';
|
||||
import { ConceptosPage, PresupuestosPage, PresupuestoDetailPage, EstimacionesPage } from './pages/admin/presupuestos';
|
||||
import { OpportunitiesPage, TendersPage, ProposalsPage, VendorsPage } from './pages/admin/bidding';
|
||||
import { IncidentesPage, CapacitacionesPage, InspeccionesPage, InspeccionDetailPage } from './pages/admin/hse';
|
||||
import { AvancesObraPage, BitacoraObraPage, ProgramaObraPage, ControlAvancePage } from './pages/admin/obras';
|
||||
@ -50,6 +50,7 @@ function App() {
|
||||
<Route index element={<Navigate to="catalogo" replace />} />
|
||||
<Route path="catalogo" element={<ConceptosPage />} />
|
||||
<Route path="presupuestos" element={<PresupuestosPage />} />
|
||||
<Route path="presupuestos/:id" element={<PresupuestoDetailPage />} />
|
||||
<Route path="estimaciones" element={<EstimacionesPage />} />
|
||||
</Route>
|
||||
|
||||
|
||||
@ -11,6 +11,9 @@ import {
|
||||
PresupuestoFilters,
|
||||
CreatePresupuestoDto,
|
||||
UpdatePresupuestoDto,
|
||||
CreatePresupuestoPartidaDto,
|
||||
UpdatePresupuestoPartidaDto,
|
||||
RejectPresupuestoDto,
|
||||
estimacionesApi,
|
||||
EstimacionFilters,
|
||||
CreateEstimacionDto,
|
||||
@ -191,6 +194,132 @@ export function useDuplicatePresupuesto() {
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
1174
web/src/pages/admin/presupuestos/PresupuestoDetailPage.tsx
Normal file
1174
web/src/pages/admin/presupuestos/PresupuestoDetailPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Plus,
|
||||
Pencil,
|
||||
@ -197,12 +198,13 @@ export function PresupuestosPage() {
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button
|
||||
<Link
|
||||
to={`/admin/presupuestos/presupuestos/${item.id}`}
|
||||
className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
|
||||
title="Ver detalle"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
</Link>
|
||||
{item.estado === 'revision' && (
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-green-600 hover:bg-green-50 rounded-lg"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export { ConceptosPage } from './ConceptosPage';
|
||||
export { PresupuestosPage } from './PresupuestosPage';
|
||||
export { PresupuestoDetailPage } from './PresupuestoDetailPage';
|
||||
export { EstimacionesPage } from './EstimacionesPage';
|
||||
|
||||
@ -139,6 +139,50 @@ export const conceptosApi = {
|
||||
// API DE PRESUPUESTOS
|
||||
// ===========================
|
||||
|
||||
// ===========================
|
||||
// TIPOS PARA PARTIDAS DE PRESUPUESTO
|
||||
// ===========================
|
||||
|
||||
export interface CreatePresupuestoPartidaDto {
|
||||
conceptoId: string;
|
||||
cantidad: number;
|
||||
precioUnitario: number;
|
||||
orden?: number;
|
||||
}
|
||||
|
||||
export interface UpdatePresupuestoPartidaDto {
|
||||
cantidad?: number;
|
||||
precioUnitario?: number;
|
||||
orden?: number;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// TIPOS PARA VERSIONES
|
||||
// ===========================
|
||||
|
||||
export interface PresupuestoVersion {
|
||||
id: string;
|
||||
presupuestoId: string;
|
||||
version: number;
|
||||
estado: PresupuestoEstado;
|
||||
montoTotal: number;
|
||||
fechaCreacion: string;
|
||||
creadoPor?: string;
|
||||
notas?: string;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// TIPOS PARA APROBACION/RECHAZO
|
||||
// ===========================
|
||||
|
||||
export interface RejectPresupuestoDto {
|
||||
motivo: string;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// API DE PRESUPUESTOS
|
||||
// ===========================
|
||||
|
||||
export const presupuestosApi = {
|
||||
list: async (filters?: PresupuestoFilters): Promise<PaginatedResponse<Presupuesto>> => {
|
||||
const response = await api.get<PaginatedResponse<Presupuesto>>('/presupuestos', {
|
||||
@ -152,6 +196,11 @@ export const presupuestosApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getWithPartidas: async (id: string): Promise<Presupuesto> => {
|
||||
const response = await api.get<Presupuesto>(`/presupuestos/${id}?include=partidas,partidas.concepto`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: CreatePresupuestoDto): Promise<Presupuesto> => {
|
||||
const response = await api.post<Presupuesto>('/presupuestos', data);
|
||||
return response.data;
|
||||
@ -171,8 +220,59 @@ export const presupuestosApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
reject: async (id: string, data: RejectPresupuestoDto): Promise<Presupuesto> => {
|
||||
const response = await api.post<Presupuesto>(`/presupuestos/${id}/reject`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
duplicate: async (id: string): Promise<Presupuesto> => {
|
||||
const response = await api.post<Presupuesto>(`/presupuestos/${id}/duplicate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createVersion: async (id: string, notas?: string): Promise<Presupuesto> => {
|
||||
const response = await api.post<Presupuesto>(`/presupuestos/${id}/version`, { notas });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getVersions: async (id: string): Promise<PresupuestoVersion[]> => {
|
||||
const response = await api.get<PresupuestoVersion[]>(`/presupuestos/${id}/versions`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
exportPdf: async (id: string): Promise<Blob> => {
|
||||
const response = await api.get(`/presupuestos/${id}/export/pdf`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
exportExcel: async (id: string): Promise<Blob> => {
|
||||
const response = await api.get(`/presupuestos/${id}/export/excel`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Partidas
|
||||
addPartida: async (id: string, data: CreatePresupuestoPartidaDto): Promise<PresupuestoPartida> => {
|
||||
const response = await api.post<PresupuestoPartida>(`/presupuestos/${id}/partidas`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updatePartida: async (
|
||||
presupuestoId: string,
|
||||
partidaId: string,
|
||||
data: UpdatePresupuestoPartidaDto
|
||||
): Promise<PresupuestoPartida> => {
|
||||
const response = await api.patch<PresupuestoPartida>(
|
||||
`/presupuestos/${presupuestoId}/partidas/${partidaId}`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deletePartida: async (presupuestoId: string, partidaId: string): Promise<void> => {
|
||||
await api.delete(`/presupuestos/${presupuestoId}/partidas/${partidaId}`);
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user