This commit is contained in:
Lizandro Guarnizo
2026-03-10 18:42:51 -05:00
parent b57fc16ab3
commit db469b2134
98 changed files with 16365 additions and 303 deletions
+352
View File
@@ -0,0 +1,352 @@
"""
Genera cronograma.xlsx — Cronograma de Ejecución del Proyecto
Laboratorio Ximena / WhatsApp Bot Manager
Inicio: 2 de marzo de 2026
"""
from datetime import date, timedelta
from openpyxl import Workbook
from openpyxl.styles import (PatternFill, Font, Alignment, Border, Side,
GradientFill)
from openpyxl.utils import get_column_letter
# ─── CONFIGURACIÓN DEL PROYECTO ─────────────────────────────────────────────
INICIO = date(2026, 3, 2)
FASES = [
# (fase, tarea, inicio_offset_dias, duracion_dias, estado)
# Estado: 'done' | 'active' | 'pending'
("Fase 1 Sistema de Roles", "Diseño BD (roles, modules)", 0, 4, "done"),
("Fase 1 Sistema de Roles", "APIs roles y usuarios", 4, 4, "done"),
("Fase 1 Sistema de Roles", "UI lab_usuarios.php", 6, 4, "done"),
("Fase 1 Sistema de Roles", "Unificación panel admin", 8, 3, "done"),
("Fase 2 Portal Enfermero", "Corrección login enfermero", 11, 2, "done"),
("Fase 2 Portal Enfermero", "Fix SyntaxError botón formulario", 11, 2, "done"),
("Fase 2 Portal Enfermero", "Submit idempotente (ya completado)", 13, 2, "done"),
("Fase 2 Portal Enfermero", "Firma digital — canvas", 15, 3, "done"),
("Fase 2 Portal Enfermero", "Firma por foto (opcional)", 18, 2, "active"),
("Fase 3 Formularios Avanz.", "Campos dinámicos tipo fecha/selector", 21, 4, "pending"),
("Fase 3 Formularios Avanz.", "Historial de envíos por paciente", 25, 3, "pending"),
("Fase 3 Formularios Avanz.", "Descarga PDF de formulario enviado", 25, 4, "pending"),
("Fase 4 Agendamiento", "Calendario de domicilios", 32, 5, "pending"),
("Fase 4 Agendamiento", "Mensajes programados WhatsApp", 36, 4, "pending"),
("Fase 4 Agendamiento", "Recordatorios automáticos", 38, 3, "pending"),
("Fase 5 Reportes", "Dashboard métricas laboratorio", 44, 4, "pending"),
("Fase 5 Reportes", "Exportación Excel / CSV", 46, 3, "pending"),
("Fase 5 Reportes", "Reportes por enfermera", 48, 3, "pending"),
("Fase 6 QA y Despliegue", "Pruebas integrales", 53, 4, "pending"),
("Fase 6 QA y Despliegue", "Corrección de bugs", 55, 3, "pending"),
("Fase 6 QA y Despliegue", "Despliegue en producción", 58, 3, "pending"),
("Fase 6 QA y Despliegue", "Capacitación y entrega", 60, 2, "pending"),
]
# ─── COLORES ────────────────────────────────────────────────────────────────
COLORES_FASE = {
"Fase 1 Sistema de Roles": {"header": "1565C0", "barra_done": "42A5F5", "barra_active": "1E88E5", "barra_pending": "90CAF9"},
"Fase 2 Portal Enfermero": {"header": "2E7D32", "barra_done": "66BB6A", "barra_active": "43A047", "barra_pending": "A5D6A7"},
"Fase 3 Formularios Avanz.": {"header": "6A1B9A", "barra_done": "AB47BC", "barra_active": "8E24AA", "barra_pending": "CE93D8"},
"Fase 4 Agendamiento": {"header": "E65100", "barra_done": "FFA726", "barra_active": "FB8C00", "barra_pending": "FFCC80"},
"Fase 5 Reportes": {"header": "00695C", "barra_done": "26A69A", "barra_active": "00897B", "barra_pending": "80CBC4"},
"Fase 6 QA y Despliegue": {"header": "B71C1C", "barra_done": "EF5350", "barra_active": "E53935", "barra_pending": "EF9A9A"},
}
ESTADO_LABEL = {"done": "✅ Completado", "active": "🔄 En curso", "pending": "⏳ Pendiente"}
ESTADO_FILL = {
"done": "D0F0C0",
"active": "FFF9C4",
"pending": "F5F5F5",
}
# ─── HELPERS ────────────────────────────────────────────────────────────────
def fecha(offset): return INICIO + timedelta(days=offset)
def col_for_date(d, week_cols_start, all_weeks):
"""Devuelve columna Excel (1-based) para una fecha dada."""
for i, w in enumerate(all_weeks):
if w <= d < w + timedelta(days=7):
return week_cols_start + i
return None
def thin():
s = Side(style='thin', color='CCCCCC')
return Border(left=s, right=s, top=s, bottom=s)
def fill(hex_color):
return PatternFill("solid", fgColor=hex_color)
# ─── GENERAR SEMANAS ────────────────────────────────────────────────────────
max_offset = max(t[3] + t[2] for t in FASES)
num_weeks = (max_offset // 7) + 2
all_weeks = [INICIO + timedelta(weeks=i) for i in range(num_weeks)]
# ─── WORKBOOK ───────────────────────────────────────────────────────────────
wb = Workbook()
# ════════════════════════════════════════════════════════════════════════════
# HOJA 1 — GANTT
# ════════════════════════════════════════════════════════════════════════════
ws = wb.active
ws.title = "Gantt"
TASK_COLS = 4 # Fase | Tarea | Inicio | Estado
WEEK_COL_START = TASK_COLS + 1 # columna Excel donde empiezan las semanas
# ── Dimensiones ─────────────────────────────────────────────────────────────
ws.column_dimensions['A'].width = 26
ws.column_dimensions['B'].width = 38
ws.column_dimensions['C'].width = 13
ws.column_dimensions['D'].width = 16
for i in range(num_weeks):
cl = get_column_letter(WEEK_COL_START + i)
ws.column_dimensions[cl].width = 5.5
# ── Fila 1 — Título ─────────────────────────────────────────────────────────
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=WEEK_COL_START + num_weeks - 1)
ws['A1'] = "CRONOGRAMA DE EJECUCIÓN — Laboratorio Ximena"
ws['A1'].font = Font(bold=True, size=14, color="FFFFFF")
ws['A1'].fill = fill("0D47A1")
ws['A1'].alignment = Alignment(horizontal="center", vertical="center")
ws.row_dimensions[1].height = 28
# ── Fila 2 — Sub-título ─────────────────────────────────────────────────────
ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=WEEK_COL_START + num_weeks - 1)
ws['A2'] = f"Inicio: {INICIO.strftime('%d de %B de %Y')} | Total estimado: {num_weeks} semanas"
ws['A2'].font = Font(italic=True, size=10, color="444444")
ws['A2'].fill = fill("E3F2FD")
ws['A2'].alignment = Alignment(horizontal="center", vertical="center")
ws.row_dimensions[2].height = 18
# ── Fila 3 — Encabezados columnas texto ─────────────────────────────────────
heads = ["Fase", "Tarea", "Inicio", "Estado"]
for c, h in enumerate(heads, 1):
cell = ws.cell(row=3, column=c, value=h)
cell.font = Font(bold=True, size=10, color="FFFFFF")
cell.fill = fill("37474F")
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = thin()
# ── Fila 3 — Encabezados semanas ────────────────────────────────────────────
for i, w in enumerate(all_weeks):
col = WEEK_COL_START + i
cell = ws.cell(row=3, column=col, value=w.strftime("%-d/%m"))
cell.font = Font(bold=True, size=8, color="FFFFFF")
cell.fill = fill("546E7A")
cell.alignment = Alignment(horizontal="center", vertical="center", text_rotation=60)
cell.border = thin()
ws.row_dimensions[3].height = 44
# ── Filas de datos ───────────────────────────────────────────────────────────
ROW = 4
prev_fase = None
for (fase, tarea, start_off, dur, estado) in FASES:
colores = COLORES_FASE[fase]
# Separador de fase (cuando cambia)
if fase != prev_fase:
ws.merge_cells(start_row=ROW, start_column=1,
end_row=ROW, end_column=WEEK_COL_START + num_weeks - 1)
cell = ws.cell(row=ROW, column=1, value=f" {fase}")
cell.font = Font(bold=True, size=10, color="FFFFFF")
cell.fill = fill(colores["header"])
cell.alignment = Alignment(vertical="center")
cell.border = thin()
ws.row_dimensions[ROW].height = 20
ROW += 1
prev_fase = fase
# Calcular fechas
d_ini = fecha(start_off)
d_fin = fecha(start_off + dur - 1)
row_fill = fill(ESTADO_FILL[estado])
# Columna A — Fase (vacía, ya fue el encabezado)
c_fase = ws.cell(row=ROW, column=1, value="")
c_fase.fill = row_fill
c_fase.border = thin()
# Columna B — Tarea
c_task = ws.cell(row=ROW, column=2, value=tarea)
c_task.font = Font(size=9)
c_task.fill = row_fill
c_task.alignment = Alignment(vertical="center", wrap_text=True)
c_task.border = thin()
# Columna C — Fecha inicio
c_date = ws.cell(row=ROW, column=3, value=d_ini.strftime("%-d %b"))
c_date.font = Font(size=9)
c_date.fill = row_fill
c_date.alignment = Alignment(horizontal="center", vertical="center")
c_date.border = thin()
# Columna D — Estado
c_est = ws.cell(row=ROW, column=4, value=ESTADO_LABEL[estado])
c_est.font = Font(size=9, bold=(estado == "active"))
c_est.fill = row_fill
c_est.alignment = Alignment(horizontal="center", vertical="center")
c_est.border = thin()
# Barras de Gantt
barra_color = colores[f"barra_{estado}"]
w_ini = col_for_date(d_ini, WEEK_COL_START, all_weeks)
w_fin = col_for_date(d_fin, WEEK_COL_START, all_weeks)
if w_ini is None: w_ini = WEEK_COL_START
if w_fin is None: w_fin = WEEK_COL_START + num_weeks - 1
for col in range(WEEK_COL_START, WEEK_COL_START + num_weeks):
cell = ws.cell(row=ROW, column=col, value="")
if w_ini <= col <= w_fin:
cell.fill = fill(barra_color)
else:
cell.fill = fill("F5F5F5")
cell.border = thin()
ws.row_dimensions[ROW].height = 18
ROW += 1
# ── Leyenda ──────────────────────────────────────────────────────────────────
ROW += 1
leyenda = [
("✅ Completado", "D0F0C0"),
("🔄 En curso", "FFF9C4"),
("⏳ Pendiente", "F5F5F5"),
]
ws.cell(row=ROW, column=1, value="Leyenda:").font = Font(bold=True, size=10)
for i, (txt, color) in enumerate(leyenda, 2):
cell = ws.cell(row=ROW, column=i, value=txt)
cell.fill = fill(color)
cell.border = thin()
cell.font = Font(size=9)
cell.alignment = Alignment(horizontal="center")
# Freeze panes
ws.freeze_panes = "E4"
# ════════════════════════════════════════════════════════════════════════════
# HOJA 2 — RESUMEN POR FASE
# ════════════════════════════════════════════════════════════════════════════
ws2 = wb.create_sheet("Resumen por Fase")
ws2.column_dimensions['A'].width = 30
ws2.column_dimensions['B'].width = 14
ws2.column_dimensions['C'].width = 14
ws2.column_dimensions['D'].width = 14
ws2.column_dimensions['E'].width = 16
ws2.column_dimensions['F'].width = 16
# Título
ws2.merge_cells("A1:F1")
ws2['A1'] = "RESUMEN POR FASE"
ws2['A1'].font = Font(bold=True, size=13, color="FFFFFF")
ws2['A1'].fill = fill("0D47A1")
ws2['A1'].alignment = Alignment(horizontal="center", vertical="center")
ws2.row_dimensions[1].height = 26
# Encabezados
enc2 = ["Fase", "Inicio", "Fin", "Duración (días)", "Tareas", "Estado"]
for c, h in enumerate(enc2, 1):
cell = ws2.cell(row=2, column=c, value=h)
cell.font = Font(bold=True, size=10, color="FFFFFF")
cell.fill = fill("37474F")
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = thin()
ws2.row_dimensions[2].height = 22
# Agrupar por fase
from collections import defaultdict
fases_data = defaultdict(list)
for row in FASES:
fases_data[row[0]].append(row)
ROW2 = 3
for fase_name, tareas in fases_data.items():
ini_fase = fecha(min(t[2] for t in tareas))
fin_fase = fecha(max(t[2] + t[3] - 1 for t in tareas))
dur_total = (fin_fase - ini_fase).days + 1
n_tareas = len(tareas)
estados = [t[4] for t in tareas]
if all(e == "done" for e in estados):
estado_fase = "✅ Completada"
fc = "D0F0C0"
elif any(e == "active" for e in estados):
estado_fase = "🔄 En curso"
fc = "FFF9C4"
else:
estado_fase = "⏳ Pendiente"
fc = "F5F5F5"
colores = COLORES_FASE[fase_name]
fondo = fill(colores["header"] if estado_fase == "✅ Completada" else fc)
data = [fase_name, ini_fase.strftime("%-d %b %Y"), fin_fase.strftime("%-d %b %Y"),
dur_total, n_tareas, estado_fase]
for c, v in enumerate(data, 1):
cell = ws2.cell(row=ROW2, column=c, value=v)
cell.alignment = Alignment(horizontal="center" if c > 1 else "left",
vertical="center", wrap_text=True)
cell.border = thin()
cell.font = Font(size=10, bold=(c == 1),
color=("FFFFFF" if estado_fase == "✅ Completada" else "222222"))
cell.fill = PatternFill("solid", fgColor=colores["header"]) if estado_fase == "✅ Completada" \
else fill(fc)
ws2.row_dimensions[ROW2].height = 22
ROW2 += 1
# ════════════════════════════════════════════════════════════════════════════
# HOJA 3 — DETALLE DE TAREAS
# ════════════════════════════════════════════════════════════════════════════
ws3 = wb.create_sheet("Detalle Tareas")
ws3.column_dimensions['A'].width = 28
ws3.column_dimensions['B'].width = 40
ws3.column_dimensions['C'].width = 14
ws3.column_dimensions['D'].width = 14
ws3.column_dimensions['E'].width = 12
ws3.column_dimensions['F'].width = 16
ws3.merge_cells("A1:F1")
ws3['A1'] = "DETALLE DE TAREAS"
ws3['A1'].font = Font(bold=True, size=13, color="FFFFFF")
ws3['A1'].fill = fill("0D47A1")
ws3['A1'].alignment = Alignment(horizontal="center", vertical="center")
ws3.row_dimensions[1].height = 26
enc3 = ["Fase", "Tarea", "Inicio", "Fin", "Días", "Estado"]
for c, h in enumerate(enc3, 1):
cell = ws3.cell(row=2, column=c, value=h)
cell.font = Font(bold=True, size=10, color="FFFFFF")
cell.fill = fill("37474F")
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = thin()
ws3.row_dimensions[2].height = 22
ROW3 = 3
for (fase, tarea, start_off, dur, estado) in FASES:
d_ini = fecha(start_off)
d_fin = fecha(start_off + dur - 1)
colores = COLORES_FASE[fase]
fc = ESTADO_FILL[estado]
data = [fase, tarea, d_ini.strftime("%-d %b %Y"), d_fin.strftime("%-d %b %Y"),
dur, ESTADO_LABEL[estado]]
for c, v in enumerate(data, 1):
cell = ws3.cell(row=ROW3, column=c, value=v)
cell.alignment = Alignment(horizontal="center" if c > 2 else "left",
vertical="center", wrap_text=True)
cell.border = thin()
cell.font = Font(size=9)
cell.fill = fill(fc)
ws3.row_dimensions[ROW3].height = 18
ROW3 += 1
# ─── GUARDAR ─────────────────────────────────────────────────────────────────
out = "/Users/lizandro/Documents/GitHub/whatsapp/cronograma.xlsx"
wb.save(out)
print(f"✅ Archivo generado: {out}")