Files
whatsapp/scripts/generar_cronograma.py
2026-03-10 18:42:51 -05:00

353 lines
17 KiB
Python
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.
"""
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}")