TOOLKIT / static /formatos.html
IA-PLATE's picture
Formatos: permitir subir plantillas (Supabase Storage) y descargarlas directo desde la herramienta
52cee63
Raw
History Blame Contribute Delete
16.4 kB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PLATE · Formatos Institucionales</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap");
:root{--black:#111111;--dark:#1a1a1a;--card:#1f1f1f;--border:#2e2e2e;--white:#f0f0f0;--muted:#888;--amber:#d4932a;--blue:#2b9fd8;--green:#6abf4b;}
*{box-sizing:border-box;margin:0;padding:0;}
body{background:var(--black);color:var(--white);font-family:"IBM Plex Sans",sans-serif;min-height:100vh;display:flex;flex-direction:column;}
header{display:flex;align-items:center;justify-content:space-between;padding:20px 40px;border-bottom:1px solid var(--border);background:var(--dark);}
.logo-wrap{display:flex;align-items:center;gap:14px;}
.logo-text{font-family:"IBM Plex Mono",monospace;font-size:18px;font-weight:600;letter-spacing:0.1em;}
.logo-sep{width:1px;height:28px;background:var(--border);}
.logo-sub{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;}
.btn-back{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--muted);text-decoration:none;border:1px solid var(--border);border-radius:6px;padding:6px 14px;transition:all .15s;}
.btn-back:hover{border-color:var(--amber);color:var(--white);}
main{flex:1;max-width:720px;margin:0 auto;width:100%;padding:48px 24px;}
.page-tag{font-size:11px;font-family:"IBM Plex Mono",monospace;color:var(--amber);letter-spacing:0.2em;text-transform:uppercase;margin-bottom:10px;}
h1{font-size:26px;font-weight:600;margin-bottom:8px;}
.subtitle{font-size:14px;color:var(--muted);margin-bottom:36px;line-height:1.6;}
.search-wrap{position:relative;margin-bottom:12px;}
.search-input{width:100%;background:var(--dark);border:1px solid var(--border);border-radius:12px;color:var(--white);font-family:"IBM Plex Sans",sans-serif;font-size:15px;padding:16px 56px 16px 20px;outline:none;resize:none;line-height:1.5;transition:border-color .15s;}
.search-input:focus{border-color:var(--amber);}
.search-input::placeholder{color:#555;}
.btn-search{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:var(--amber);border:none;border-radius:8px;width:36px;height:36px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s;}
.btn-search:hover:not(:disabled){background:#c47d1e;}
.btn-search:disabled{opacity:0.4;cursor:default;}
.btn-search svg{width:16px;height:16px;color:#fff;}
.hint{font-size:12px;color:#555;margin-bottom:32px;}
.hint span{color:#666;margin-right:12px;cursor:pointer;transition:color .15s;}
.hint span:hover{color:var(--amber);}
.results{display:flex;flex-direction:column;gap:12px;}
.result-card{background:var(--dark);border:1px solid var(--border);border-radius:12px;padding:20px 24px;transition:border-color .15s;}
.result-card:hover{border-color:#444;}
.rc-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px;}
.rc-code{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--amber);font-weight:600;letter-spacing:0.05em;white-space:nowrap;margin-top:2px;}
.rc-name{font-size:15px;font-weight:600;color:var(--white);}
.rc-fase{font-size:11px;font-family:"IBM Plex Mono",monospace;color:var(--muted);margin-bottom:6px;}
.rc-desc{font-size:13px;color:var(--muted);line-height:1.6;margin-bottom:8px;}
.rc-reason{font-size:12px;color:#6abf4b;line-height:1.5;}
.rc-reason::before{content:"→ ";}
.rc-ruta{display:flex;align-items:center;gap:6px;margin-top:10px;padding-top:10px;border-top:1px solid #2a2a2a;font-size:11px;color:#555;font-family:"IBM Plex Mono",monospace;}
.rc-ruta svg{width:12px;height:12px;flex-shrink:0;color:#666;}
.rc-ruta span{color:#666;}
.nota-card{background:rgba(212,147,42,0.08);border:1px solid rgba(212,147,42,0.25);border-radius:10px;padding:14px 18px;font-size:13px;color:#d4a04a;margin-top:4px;}
.rc-archivos{margin-top:10px;padding-top:10px;border-top:1px solid #2a2a2a;display:flex;flex-direction:column;gap:6px;}
.rc-download{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--green);text-decoration:none;width:fit-content;}
.rc-download:hover{text-decoration:underline;}
.rc-upload-link{font-size:12px;color:#666;background:none;border:none;cursor:pointer;font-family:inherit;text-decoration:underline;padding:0;width:fit-content;}
.rc-upload-link:hover{color:var(--amber);}
.upload-toggle{font-size:12px;color:#666;cursor:pointer;text-decoration:underline;margin-bottom:20px;display:inline-block;}
.upload-toggle:hover{color:var(--amber);}
.upload-panel{display:none;background:var(--dark);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin-bottom:24px;flex-direction:column;gap:10px;}
.upload-panel.on{display:flex;}
.upload-panel label{font-size:11px;color:var(--muted);font-family:"IBM Plex Mono",monospace;letter-spacing:0.08em;}
.upload-panel input[type=text]{background:var(--card);border:1px solid var(--border);border-radius:8px;color:var(--white);font-size:13px;padding:8px 12px;width:100%;}
.upload-panel input[type=file]{font-size:12px;color:var(--muted);}
.upload-panel button{background:var(--amber);border:none;color:#fff;border-radius:8px;padding:9px 16px;font-size:12px;font-weight:600;cursor:pointer;width:fit-content;font-family:"IBM Plex Mono",monospace;}
.upload-panel button:hover:not(:disabled){background:#c47d1e;}
.upload-panel button:disabled{opacity:.4;cursor:default;}
.upload-status{font-size:12px;color:var(--muted);}
.upload-status.ok{color:var(--green);}
.upload-status.err{color:#e05252;}
.empty{text-align:center;padding:48px 24px;color:var(--muted);}
.empty svg{width:40px;height:40px;opacity:.3;margin-bottom:12px;}
.loading{display:flex;gap:4px;justify-content:center;padding:40px;}
.loading span{width:8px;height:8px;border-radius:50%;background:var(--amber);animation:ld 1s ease-in-out infinite;}
.loading span:nth-child(2){animation-delay:.15s;}
.loading span:nth-child(3){animation-delay:.3s;}
@keyframes ld{0%,80%,100%{transform:scale(0.6);opacity:0.4;}40%{transform:scale(1);opacity:1;}}
footer{border-top:1px solid var(--border);padding:14px 40px;display:flex;align-items:center;justify-content:space-between;background:var(--dark);}
.ftext{font-size:11px;color:var(--muted);font-family:"IBM Plex Mono",monospace;}
.dot{width:6px;height:6px;border-radius:50%;background:var(--amber);display:inline-block;margin-right:6px;animation:pulse 2s ease-in-out infinite;}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:0.3;}}
@media(max-width:540px){header{padding:16px 20px;}.logo-sub{display:none;}main{padding:32px 16px;}h1{font-size:20px;}}
</style>
</head>
<body>
<header>
<div class="logo-wrap">
<span class="logo-text">PLATE</span>
<div class="logo-sep"></div>
<span class="logo-sub">Formatos Institucionales</span>
</div>
<a class="btn-back" href="/">← Toolkit</a>
</header>
<main>
<p class="page-tag">Buscador de Formatos</p>
<h1>¿Qué necesitas hacer?</h1>
<p class="subtitle">Describe en tus palabras la tarea o situación y te digo qué formato institucional de PLATE usar.</p>
<div class="search-wrap">
<textarea class="search-input" id="queryInput" rows="2" placeholder="Ej: quiero invitar a un contratista a concursar para obra civil…"></textarea>
<button class="btn-search" id="searchBtn">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
<p class="hint">Prueba: <span onclick="setQ('hacer el acta de entrega del proyecto al cliente')">Acta de entrega</span><span onclick="setQ('enviar documentos formalmente a un contratista')">Transmittal</span><span onclick="setQ('hacer una minuta de recorrido de obra')">Minuta de obra</span><span onclick="setQ('controlar los pagos y saldos de contratos')">Control contractual</span></p>
<span class="upload-toggle" id="uploadToggle">+ Subir una plantilla</span>
<div class="upload-panel" id="uploadPanel">
<div>
<label>CÓDIGO DEL FORMATO</label>
<input type="text" id="uploadCodigo" placeholder="Ej: 3.5.9">
</div>
<div>
<label>ARCHIVO</label><br>
<input type="file" id="uploadArchivo">
</div>
<button id="uploadBtn" onclick="subirPlantilla()">Subir</button>
<div class="upload-status" id="uploadStatus"></div>
</div>
<div class="results" id="results"></div>
</main>
<footer>
<span class="ftext"><span class="dot"></span>PLATE &middot; Formatos Institucionales</span>
<span class="ftext">Confidencial &mdash; Solo para uso interno</span>
</footer>
<script>
const BASE = '99. RECURSOS → Formatos Plate → FORMATOS';
const RUTAS = {
'1.1': '1.0 INICIO → 1.1 KICK-OFF',
'2.1': '2.0 PRE-DESARROLLO → 2.1 FORMATOS PARA REUNIONES DE TRABAJO',
'2.2': '2.0 PRE-DESARROLLO → 2.2 FORMATOS PARA REUNIONES DE DEFINICION DEL PROYECTO CON EL CLIENTE',
'2.3': '2.0 PRE-DESARROLLO → 2.3 FORMATOS PARA REUNIONES DE DEFINICIÓN DEL PROYECTO (PLATE)',
'2.4': '2.0 PRE-DESARROLLO → 2.4 FORMATOS PARA CONTROL ADMINISTRATIVO CLIENTE',
'2.5': '2.0 PRE-DESARROLLO → 2.5 FORMATOS PARA CONTROL ADMINISTRATIVO PLATE',
'3.1': '3.0 DESARROLLO → 3.1 FORMATOS PARA REUNIONES DE TRABAJO',
'3.2': '3.0 DESARROLLO → 3.2 FORMATOS PARA REUNIONES DE DEFINICION DEL PROYECTO CLIENTE',
'3.3': '3.0 DESARROLLO → 3.3 FORMATOS PARA REUNIONES DE DEFINICION DEL PROYECTO PLATE',
'3.4': '3.0 DESARROLLO → 3.4 FORMATOS CONTROL DE OBRA CLIENTE',
'3.5': '3.0 DESARROLLO → 3.5 FORMATOS CONTROL DE OBRA PLATE',
'3.6': '3.0 DESARROLLO → 3.6 FORMATOS CONTROL ADMINISTRATIVO CLIENTE',
'3.7': '3.0 DESARROLLO → 3.7 FORMATOS CONTROL ADMINISTRATIVO PLATE',
'4.1': '4.0 CIERRE ADMINISTRATIVO → 4.1 FORMATOS PARA REUNIONES DE TRABAJO',
'4.2': '4.0 CIERRE ADMINISTRATIVO → 4.2 FORMATOS CIERRE DE OBRA CLIENTE',
'4.3': '4.0 CIERRE ADMINISTRATIVO → 4.3 FORMATOS CIERRE DE OBRA PLATE',
'4.4': '4.0 CIERRE ADMINISTRATIVO → 4.4 FORMATOS CIERRE ADMINISTRATIVO CLIENTE',
};
function getRuta(codigo) {
const prefix = codigo.split('.').slice(0,2).join('.');
const r = RUTAS[prefix];
if (!r) return null;
return `Buildpeer → Documentos → ${BASE}${r}`;
}
const input = document.getElementById('queryInput');
const btn = document.getElementById('searchBtn');
const res = document.getElementById('results');
function setQ(text) { input.value = text; input.focus(); }
const uploadToggle = document.getElementById('uploadToggle');
const uploadPanel = document.getElementById('uploadPanel');
uploadToggle.addEventListener('click', () => uploadPanel.classList.toggle('on'));
function abrirSubida(codigo) {
uploadPanel.classList.add('on');
document.getElementById('uploadCodigo').value = codigo;
document.getElementById('uploadCodigo').focus();
uploadPanel.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async function subirPlantilla() {
const codigo = document.getElementById('uploadCodigo').value.trim();
const archivoInput = document.getElementById('uploadArchivo');
const archivo = archivoInput.files[0];
const statusEl = document.getElementById('uploadStatus');
if (!codigo || !archivo) {
statusEl.textContent = 'Falta el código o el archivo.';
statusEl.className = 'upload-status err';
return;
}
const btn = document.getElementById('uploadBtn');
btn.disabled = true;
statusEl.textContent = 'Subiendo...';
statusEl.className = 'upload-status';
try {
const fd = new FormData();
fd.append('codigo', codigo);
fd.append('archivo', archivo);
const r = await fetch('/formatos/subir', { method: 'POST', body: fd });
const data = await r.json();
if (data.ok) {
statusEl.textContent = `Listo — "${data.archivo}" ya está disponible para el código ${data.codigo}.`;
statusEl.className = 'upload-status ok';
archivoInput.value = '';
} else {
statusEl.textContent = 'Error: ' + (data.error || 'desconocido');
statusEl.className = 'upload-status err';
}
} catch (e) {
statusEl.textContent = 'No se pudo conectar con el servidor.';
statusEl.className = 'upload-status err';
}
btn.disabled = false;
}
async function cargarArchivos(codigo, contenedorId) {
try {
const r = await fetch(`/formatos/archivos/${encodeURIComponent(codigo)}`);
const data = await r.json();
const el = document.getElementById(contenedorId);
if (!el) return;
if (data.ok && data.archivos && data.archivos.length) {
el.innerHTML = data.archivos.map(nombre => {
const nombreCorto = nombre.split('/').pop();
return `<a class="rc-download" href="/formatos/descargar/${encodeURIComponent(codigo)}/${encodeURIComponent(nombreCorto)}" download>⬇ Descargar: ${nombreCorto}</a>`;
}).join('');
} else {
el.innerHTML = `<button class="rc-upload-link" onclick="abrirSubida('${codigo}')">Aún no hay plantilla subida — subir una</button>`;
}
} catch (e) {}
}
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); buscar(); }
});
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
});
btn.addEventListener('click', buscar);
async function buscar() {
const q = input.value.trim();
if (!q) return;
btn.disabled = true;
res.innerHTML = '<div class="loading"><span></span><span></span><span></span></div>';
try {
const fd = new FormData();
fd.append('query', q);
const r = await fetch('/buscar-formato', { method: 'POST', body: fd });
const data = await r.json();
if (!data.ok) { res.innerHTML = `<div class="nota-card">Error: ${data.error}</div>`; return; }
const items = data.resultados || [];
if (!items.length) {
res.innerHTML = `<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg><p>No encontré un formato específico para eso.<br>${data.nota || 'Intenta describir de otra forma.'}</p></div>`;
return;
}
const faseLabel = { '1': '1.0 INICIO', '2': '2.0 PRE-DESARROLLO', '3': '3.0 DESARROLLO', '4': '4.0 CIERRE ADMINISTRATIVO' };
let html = items.map((it, i) => {
const ruta = getRuta(it.codigo);
const archivosId = `archivos-${i}`;
return `
<div class="result-card">
<div class="rc-top">
<div>
<div class="rc-name">${it.nombre}</div>
<div class="rc-fase">${it.fase || ''}</div>
</div>
<span class="rc-code">[${it.codigo}]</span>
</div>
<div class="rc-desc">${it.para_que}</div>
${it.por_que ? `<div class="rc-reason">${it.por_que}</div>` : ''}
${ruta ? `<div class="rc-ruta"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg><span>${ruta}</span></div>` : ''}
<div class="rc-archivos" id="${archivosId}"></div>
</div>`;
}).join('');
if (data.nota) html += `<div class="nota-card">${data.nota}</div>`;
res.innerHTML = html;
items.forEach((it, i) => cargarArchivos(it.codigo, `archivos-${i}`));
} catch(e) {
res.innerHTML = `<div class="nota-card">No se pudo conectar. Intenta de nuevo.</div>`;
}
btn.disabled = false;
}
</script>
<script>
window.PAGE_CONTEXT = "El usuario está en el Buscador de Formatos Institucionales de PLATE. Aquí puede describir en lenguaje natural lo que necesita hacer (ej: 'quiero invitar a un contratista a concursar') y el sistema le dice qué formato oficial de PLATE usar, con su código de referencia. Los formatos están organizados en 4 fases: Inicio, Pre-desarrollo, Desarrollo y Cierre Administrativo. Si ya hay una plantilla subida para ese código, aparece un enlace de descarga directa en la tarjeta de resultado; si no, aparece un enlace para subir una. También hay un botón '+ Subir una plantilla' arriba de los resultados para subir un archivo asociado a un código de formato en cualquier momento (queda guardado permanentemente, no solo en esa sesión).";
</script>
<script src="/static/chat-bubble.js"></script>
</body>
</html>