mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 13:23:17 +00:00
<!-- 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> <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> </div>
180 lines
5.3 KiB
Vue
180 lines
5.3 KiB
Vue
<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>
|