mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 12:43:18 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Implementa el módulo **Programa de obra** por proyecto, vinculado al presupuesto ya existente. ### Flujo 1. Cargar presupuesto (requisito) 2. Definir fechas de inicio/término en el proyecto 3. Crear programa: **importar Excel Neodata/Opus**, **generar automático** o **crear vacío** 4. Editar en cuadrícula (conceptos con `Barra=` y partidas con erogaciones semanales) 5. Visualizar Gantt resumido y comparativo **programado vs ejecutado** ### Backend - Tablas: `work_programs`, `work_program_periods`, `work_program_concept_slots`, `work_program_partida_amounts`, `work_program_progress` - RPC: list/get, replace (import), generate (wizard), update celdas, recalculate partidas, vs-cost - Parser Excel para formatos **por concepto** (`Barra=`) y **por partida** (montos semanales) - Tests con fixtures reales del usuario ### Frontend - Nueva página `/programa-obra` con pestañas: Conceptos, Partidas, Gantt, Programado vs costos - Diálogos de importación y generación automática - Menú principal, enlace en ficha de proyecto, banner en presupuesto ### Permisos Reutiliza `manage_budget` en MVP. ## Test plan - [x] `deno test api/work_program_excel_test.ts` (5 tests) - [x] `deno check` API modules - [x] `npm run build` web-panel - [ ] Migrar DB (`029`, `030`) en entorno con Postgres - [ ] Importar Excel por concepto y por partida en obra con presupuesto - [ ] Generar automático con estrategia keyword - [ ] Recalcular partidas desde conceptos - [ ] Verificar tab Programado vs costos con gastos registrados <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-3985fe74-d26b-4506-bd4d-f9ad57374141?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-3985fe74-d26b-4506-bd4d-f9ad57374141&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
59 lines
1.9 KiB
Vue
59 lines
1.9 KiB
Vue
<template>
|
|
<Dialog v-model:visible="visibleModel" header="Generar programa automático" modal :style="{ width: '28rem' }">
|
|
<Message severity="info" :closable="false">
|
|
Crea un borrador distribuyendo los conceptos del presupuesto según la estrategia elegida.
|
|
</Message>
|
|
<FormField label="Estrategia" class="mt-3">
|
|
<Select v-model="strategy" :options="options" option-label="label" option-value="value" class="w-full" />
|
|
</FormField>
|
|
<Message v-if="error" severity="error" class="mt-3" :closable="false">{{ error }}</Message>
|
|
<template #footer>
|
|
<Button label="Cancelar" text @click="visibleModel = false" />
|
|
<Button label="Generar" icon="pi pi-bolt" :loading="busy" @click="run" />
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import FormField from "~/components/padron/FormField.vue";
|
|
|
|
const props = defineProps<{ visible: boolean; projectId: number }>();
|
|
const emit = defineEmits<{ "update:visible": [boolean]; generated: [] }>();
|
|
|
|
const { api } = useApi();
|
|
const toast = useToast();
|
|
|
|
const visibleModel = computed({
|
|
get: () => props.visible,
|
|
set: (v) => emit("update:visible", v),
|
|
});
|
|
|
|
const strategy = ref("keyword");
|
|
const busy = ref(false);
|
|
const error = ref("");
|
|
|
|
const options = [
|
|
{ value: "keyword", label: "Por tipo de partida (preliminares → acabados)" },
|
|
{ value: "wbs", label: "Por orden WBS del presupuesto" },
|
|
{ value: "amount", label: "Por peso económico" },
|
|
];
|
|
|
|
async function run() {
|
|
if (!props.projectId) return;
|
|
busy.value = true;
|
|
error.value = "";
|
|
try {
|
|
await api(`/v1/projects/${props.projectId}/work-program/generate`, {
|
|
method: "POST",
|
|
json: { strategy: strategy.value },
|
|
});
|
|
toast.add({ severity: "success", summary: "Programa generado", life: 2500 });
|
|
emit("generated");
|
|
visibleModel.value = false;
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : "No se pudo generar";
|
|
} finally {
|
|
busy.value = false;
|
|
}
|
|
}
|
|
</script>
|