panels-origin/web-panel/components/WorkProgramGantt.vue
Alberto Martinez 17c40c66a6 feat: ver el programa de obra como Gantt (no la grilla de ceros)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Qué pasaba

El Excel que importaste es **por partida** (erogaciones semanales). Eso llena **Partidas** y el **Gantt**, no las barras de concepto. La pestaña que se abría era **Conceptos**, con 0–0 en cada semana.

Además el Gantt que ya existía tenía el encabezado de semanas mal alineado.

## Cambio

- **Gantt es la primera pestaña** y se abre sola si el programa es solo por partida.
- Cada partida es una barra de la primera a la última semana con monto (tooltip con total).
- En Conceptos, si no hay barras, se explica que hay que mirar Gantt o Partidas.

## Cómo probar

1. Mergear y recargar (solo front; no hay Liquibase).
2. Abrir Programa de obra en la obra ya importada: debe entrar en **Gantt** con barras (A001, A002, …) sobre Sem 1–9.
3. Partidas: montos semanales (no ceros).
4. Conceptos: sigue en 0 y el aviso; es normal en este Excel.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?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>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-d8a5d34a-ead9-4912-8296-322cff799783&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>&nbsp;</div>
2026-09-08 22:12:23 +00:00

180 lines
5.3 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="gantt-panel">
<Message v-if="!rows.length" severity="info" :closable="false">
{{ emptyHint }}
</Message>
<div v-else class="gantt" :style="{ '--weeks': String(periods.length || 1) }">
<div class="gantt-head">
<div class="gantt-label-col">{{ drillChapterId ? "Concepto" : "Partida" }}</div>
<div class="gantt-scale">
<span v-for="p in periods" :key="p.id" class="gantt-week">{{ p.label }}</span>
</div>
</div>
<div v-for="row in rows" :key="row.id" class="gantt-row">
<div class="gantt-label-col">
<span class="identifier">{{ row.code }}</span>
<span class="gantt-name">{{ row.label }}</span>
</div>
<div class="gantt-scale gantt-track">
<span v-for="p in periods" :key="`t-${p.id}`" class="gantt-tick" />
<div
v-if="row.span"
class="gantt-bar"
:style="{ left: `${row.span.leftPct}%`, width: `${row.span.widthPct}%` }"
:title="row.span.title"
>
<span v-if="row.span.widthPct > 12" class="gantt-bar-text">{{ row.span.caption }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ganttSpan, periodHasAmount, periodHasConceptBar } from "~/utils/workProgramGantt";
type Period = { id: number; period_index: number; label: string; start_date?: string; end_date?: string };
type ChapterRow = {
id: number;
code: string;
name: string;
periods: { period_id: number; period_index: number; amount: number }[];
};
const props = defineProps<{
periods: Period[];
chapters: ChapterRow[];
drillConcepts?: {
id: number;
code: string;
description: string;
chapter_id: number | null;
periods: { period_index: number; start_pct: number; end_pct: number }[];
}[];
drillChapterId?: number | null;
}>();
const money = new Intl.NumberFormat("es-MX", { style: "currency", currency: "MXN", maximumFractionDigits: 0 });
const emptyHint = computed(() => {
if (props.drillChapterId) {
return "Esta partida no tiene barras de concepto. El Excel por partida programa montos semanales; véalas en Gantt (todas las partidas) o en la pestaña Partidas.";
}
return "Sin partidas con erogación. Importe el Excel por partida o capture montos en Partidas.";
});
const rows = computed(() => {
if (props.drillChapterId && props.drillConcepts?.length) {
return props.drillConcepts
.filter((c) => c.chapter_id === props.drillChapterId)
.map((c) => {
const active = props.periods
.map((p) => {
const bar = periodHasConceptBar(c.periods, p);
return bar ? p.period_index : null;
})
.filter((n): n is number => n != null);
const span = ganttSpan(active, props.periods.length);
return {
id: c.id,
code: c.code,
label: c.description,
span: span
? {
...span,
caption: `${span.first}–${span.last}`,
title: `${c.code} · Sem ${span.first} a ${span.last}`,
}
: null,
};
})
.filter((row) => row.span);
}
return props.chapters
.map((ch) => {
const weeks = props.periods
.map((p) => ({ p, amount: periodHasAmount(ch.periods, p) }))
.filter((x) => x.amount > 0);
const span = ganttSpan(weeks.map((x) => x.p.period_index), props.periods.length);
const total = weeks.reduce((s, x) => s + x.amount, 0);
return {
id: ch.id,
code: ch.code,
label: ch.name,
span: span
? {
...span,
caption: money.format(total),
title: `${ch.code} ${ch.name} · Sem ${span.first}–${span.last} · ${money.format(total)}`,
}
: null,
};
})
.filter((row) => row.span);
});
</script>
<style scoped>
.gantt-panel { display: flex; flex-direction: column; gap: 0.5rem; min-height: 0; }
.gantt { display: flex; flex-direction: column; gap: 0.35rem; overflow: auto; }
.gantt-head,
.gantt-row {
display: grid;
grid-template-columns: minmax(11rem, 16rem) minmax(24rem, 1fr);
gap: 0.75rem;
align-items: center;
}
.gantt-label-col {
display: flex;
flex-direction: column;
min-width: 0;
font-size: 0.8rem;
}
.gantt-name {
color: var(--text-color-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gantt-scale {
display: grid;
grid-template-columns: repeat(var(--weeks), minmax(0, 1fr));
position: relative;
min-height: 1.6rem;
}
.gantt-week {
text-align: center;
font-size: 0.7rem;
color: var(--text-color-secondary);
}
.gantt-track {
background: var(--surface-100);
border-radius: 6px;
overflow: hidden;
}
.gantt-tick {
border-right: 1px solid color-mix(in srgb, var(--surface-border) 70%, transparent);
}
.gantt-tick:last-child { border-right: none; }
.gantt-bar {
position: absolute;
top: 4px;
bottom: 4px;
background: var(--primary-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
min-width: 4px;
}
.gantt-bar-text {
font-size: 0.65rem;
color: var(--primary-color-text, #fff);
padding: 0 0.35rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>