""" 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}")