feat(portal): descarga del cronograma del proyecto en Excel

El cliente ya veía el roadmap en pantalla pero no tenía cómo pasárselo a
alguien que no entra al portal.

El .xlsx se genera con archive/zip + encoding/xml de la stdlib en vez de
sumar una dependencia de Excel para una sola pantalla. Un CSV renombrado
no servía: en Excel en español el separador y los acentos se rompen, y
esto es un archivo que el cliente le entrega a un tercero.

Detalles que hacen que Excel lo acepte y se lea bien: inline strings (sin
tabla sharedStrings, así ningún índice puede apuntar a la cadena
equivocada), encabezado en negrita y congelado, anchos de columna según
el contenido, y nombre de hoja saneado (Excel rechaza el archivo si pasa
de 31 caracteres o trae : \ / ? * [ ]).

El endpoint repite el chequeo de acceso de PortalProyecto: sin eso,
cualquiera con sesión de portal se bajaba el cronograma de otro cliente
adivinando el slug.

Verificado abriendo el archivo generado con openpyxl: 6 partes, CRC OK,
XML bien formado en todas, acentos y CJK intactos, negrita y panel
congelado donde corresponde.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-14 22:12:45 -05:00
co-authored by Claude Sonnet 5
parent 5182c45d11
commit 62c644b815
5 changed files with 358 additions and 0 deletions
+11
View File
@@ -40,6 +40,17 @@
<!-- ─── ROADMAP ────────────────────────────────────────────────────────────── -->
<div x-show="activeTab==='Roadmap'" x-cloak>
{{ if .fases }}
<div class="flex justify-end mb-4">
<a href="/portal/proyecto/{{ .proyecto.Slug }}/cronograma.xlsx"
class="inline-flex items-center gap-2 text-sm font-medium text-slate-600 hover:text-slate-900 border border-slate-200 hover:border-slate-300 rounded-lg px-3 py-1.5 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>
</svg>
Descargar cronograma (Excel)
</a>
</div>
{{ end }}
<div class="relative">
<!-- Línea vertical -->
<div class="absolute left-6 top-0 bottom-0 w-0.5 bg-slate-200"></div>