feat(A08): Ámbito dtic-NOTAS creado - Sistema web de notas/informes

Nuevo ámbito A08 para gestión de notas e informes formales:

Arquitectura:
- Separación contenido (archivos .md) de presentación (web HTML)
- Plantillas configurables para diferentes tipos de notas
- Vista previa con membrete institucional UTN-FRLR
- Impresión/PDF via window.print() con CSS @media print

Archivos creados:
- docs/ambito/dtic-NOTAS/A08_dtic-NOTAS.md (manifiesto)
- docs/ambito/dtic-NOTAS/A08.P001_Sistema-Web-Notas.md (plan)
- docs/ambito/dtic-NOTAS/app/index.html (interfaz)
- docs/ambito/dtic-NOTAS/app/styles.css (estilos institucionales)
- docs/ambito/dtic-NOTAS/app/app.js (lógica + marked.js CDN)
- docs/ambito/dtic-NOTAS/plantillas/nota-simple.md
- docs/ambito/dtic-NOTAS/contenido/2026/ejemplo-nota.md

Migración:
- dtic-NOTAs/ movido a docs/ambito/dtic-NOTAS/dtic-NOTAs/
- Histórico 2019-2025 preservado en .Hist/
- Convención de nombres documentada: Nota-<AÑO><NUMERO>_<Asunto>.<ext>

Uso:
1. Abrir docs/ambito/dtic-NOTAS/app/index.html en navegador
2. Seleccionar plantilla o cargar archivo .md
3. Completar datos de nota
4. Generar vista previa e imprimir

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ricardo Monla
2026-04-07 19:18:56 -03:00
co-authored by Claude Opus 4.6
parent 2ab51af7f1
commit 85645ce0b8
131 changed files with 37313 additions and 1 deletions
+318
View File
@@ -0,0 +1,318 @@
/**
* Generador de Notas - DTIC UTN-FRLR
* Lógica de la aplicación web
*/
// Estado de la aplicación
let estado = {
plantillaCargada: null,
contenidoCargado: null,
datosNota: {}
};
// Inicialización
document.addEventListener('DOMContentLoaded', () => {
// Establecer fecha actual por defecto
document.getElementById('fecha').valueAsDate = new Date();
// Cargar último número disponible (simulado)
document.getElementById('numero').placeholder = 'Ej: ' + generarNumeroSugerido();
});
/**
* Genera un número sugerido basado en la fecha
* Formato: AAMM (ej: 2604 para abril 2026)
*/
function generarNumeroSugerido() {
const fecha = new Date();
const año = fecha.getFullYear() % 100;
const mes = String(fecha.getMonth() + 1).padStart(2, '0');
return `${año}${mes}`;
}
/**
* Carga la plantilla seleccionada
*/
function cargarPlantilla() {
const plantilla = document.getElementById('plantilla').value;
if (!plantilla) {
alert('Por favor seleccioná una plantilla');
return;
}
// Plantillas embebidas
const plantillas = {
'nota-simple': `---
tipo: nota-simple
numero: ${document.getElementById('numero').value || 'XXXX'}
fecha: ${document.getElementById('fecha').value || '____'}
destinatario: [Nombre del Destinatario]
remitente: DTIC - UTN FRLR
asunto: [Asunto de la nota]
---
# Nota Simple
Estimado/a [Nombre]:
Por medio de la presente, me dirijo a usted para [propósito de la nota].
[Cuerpo principal de la nota - desarrollar el tema]
Sin otro particular, saludo a usted atentamente.
`,
'informe-tecnico': `---
tipo: informe-tecnico
numero: ${document.getElementById('numero').value || 'XXXX'}
fecha: ${document.getElementById('fecha').value || '____'}
destinatario: [Nombre del Destinatario]
remitente: DTIC - UTN FRLR
asunto: Informe Técnico sobre [tema]
---
# Informe Técnico
## 1. Objeto del Informe
El presente informe tiene por objeto [describir el propósito].
## 2. Antecedentes
[Describir contexto y situación previa]
## 3. Análisis Técnico
[Desarrollar análisis detallado]
## 4. Conclusiones
[Conclusiones y recomendaciones]
## 5. Anexos
- Anexo A: [Descripción]
- Anexo B: [Descripción]
`,
'memorando': `---
tipo: memorando
numero: ${document.getElementById('numero').value || 'XXXX'}
fecha: ${document.getElementById('fecha').value || '____'}
destinatario: [Personal de DTIC / Nombre]
remitente: Dirección DTIC
asunto: [Asunto del memorando]
---
# Memorando
**PARA:** [Destinatario]
**DE:** [Remitente]
**FECHA:** [Fecha]
**ASUNTO:** [Asunto]
---
[Cuerpo del memorando]
Se comunica a todo el personal que [contenido].
Es importante destacar que [puntos clave].
Quedan a disposición para cualquier consulta.
`,
'solicitud': `---
tipo: solicitud
numero: ${document.getElementById('numero').value || 'XXXX'}
fecha: ${document.getElementById('fecha').value || '____'}
destinatario: [Autoridad/Área]
remitente: DTIC - UTN FRLR
asunto: Solicitud de [qué se solicita]
---
# Nota de Solicitud
Por medio de la presente, me dirijo a usted con el fin de solicitar [describir lo que se solicita].
**Fundamentación:**
[Explicar razones y necesidad de lo solicitado]
**Especificaciones:**
- [Ítem 1]
- [Ítem 2]
- [Ítem 3]
Agradeciendo de antemano su atención, saludo a usted atentamente.
`
};
const contenido = plantillas[plantilla];
document.getElementById('editorTexto').value = contenido;
estado.plantillaCargada = plantilla;
// Extraer metadatos del frontmatter
extraerMetadatos(contenido);
alert('Plantilla "' + plantilla + '" cargada. Editá el contenido y generá la vista previa.');
}
/**
* Carga contenido desde archivo .md
*/
function cargarContenido() {
const input = document.getElementById('contenido');
if (!input.files || !input.files[0]) {
alert('Por favor seleccioná un archivo .md');
return;
}
const archivo = input.files[0];
const lector = new FileReader();
lector.onload = (e) => {
document.getElementById('editorTexto').value = e.target.result;
estado.contenidoCargado = archivo.name;
extraerMetadatos(e.target.result);
alert('Contenido cargado desde: ' + archivo.name);
};
lector.readAsText(archivo);
}
/**
* Extrae metadatos del frontmatter (YAML entre ---)
*/
function extraerMetadatos(contenido) {
const match = contenido.match(/^---\n([\s\S]*?)\n---/);
if (match) {
const frontmatter = match[1];
// Extraer campos individuales
const campos = {
numero: frontmatter.match(/numero:\s*(.+)/)?.[1]?.trim(),
fecha: frontmatter.match(/fecha:\s*(.+)/)?.[1]?.trim(),
destinatario: frontmatter.match(/destinatario:\s*(.+)/)?.[1]?.trim(),
asunto: frontmatter.match(/asunto:\s*(.+)/)?.[1]?.trim()
};
// Completar formulario
if (campos.numero && campos.numero !== 'XXXX') {
document.getElementById('numero').value = campos.numero;
}
if (campos.fecha && campos.fecha !== '____') {
document.getElementById('fecha').value = campos.fecha;
}
if (campos.destinatario && !campos.destinatario.includes('[')) {
document.getElementById('destinatario').value = campos.destinatario;
}
if (campos.asunto && !campos.asunto.includes('[')) {
document.getElementById('asunto').value = campos.asunto;
}
estado.datosNota = { ...estado.datosNota, ...campos };
}
}
/**
* Genera la vista previa del documento
*/
function generarVistaPrevia() {
const contenido = document.getElementById('editorTexto').value;
// Obtener datos del formulario
const datos = {
numero: document.getElementById('numero').value,
fecha: formatearFecha(document.getElementById('fecha').value),
destinatario: document.getElementById('destinatario').value,
asunto: document.getElementById('asunto').value
};
// Separar frontmatter del cuerpo
const cuerpo = contenido.replace(/^---\n[\s\S]*?\n---\n/, '');
// Renderizar markdown a HTML
const htmlCuerpo = marked.parse(cuerpo);
// Construir documento
const template = document.getElementById('templateNota');
const clone = template.content.cloneNode(true);
// Completar datos
clone.querySelector('.campo-numero').textContent = datos.numero || '____';
clone.querySelector('.campo-fecha').textContent = datos.fecha || '____';
clone.querySelector('.campo-destinatario').textContent = datos.destinatario || '____';
clone.querySelector('.campo-asunto').textContent = datos.asunto || '____';
clone.querySelector('.cuerpo').innerHTML = htmlCuerpo;
// Actualizar vista previa
const preview = document.getElementById('vistaPrevia');
preview.innerHTML = '';
preview.appendChild(clone);
// Scroll a la vista previa
preview.scrollIntoView({ behavior: 'smooth' });
}
/**
* Formatea fecha de YYYY-MM-DD a DD/MM/YYYY
*/
function formatearFecha(fechaISO) {
if (!fechaISO) return '____';
const [año, mes, dia] = fechaISO.split('-');
return `${dia}/${mes}/${año}`;
}
/**
* Imprime o genera PDF del documento
*/
function imprimir() {
const vistaPrevia = document.getElementById('vistaPrevia');
if (!vistaPrevia.innerHTML.trim() || vistaPrevia.querySelector('.placeholder')) {
alert('Por favor generá primero la vista previa');
return;
}
// Crear ventana de impresión
const contenido = vistaPrevia.innerHTML;
const ventana = window.open('', '_blank');
ventana.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Nota ${document.getElementById('numero').value || '____'}</title>
<style>
${document.querySelector('style').innerHTML}
body { background: white; }
.sidebar, .editor { display: none; }
.container { display: block; padding: 0; }
.preview { position: absolute; width: 100%; }
</style>
</head>
<body>
${contenido}
<script>
window.onload = () => {
window.print();
window.onafterprint = () => window.close();
};
</script>
</body>
</html>
`);
ventana.document.close();
}
+110
View File
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generador de Notas - DTIC UTN-FRLR</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<!-- Panel de Control -->
<aside class="sidebar">
<h2>📝 Panel de Control</h2>
<div class="section">
<label for="plantilla">Plantilla:</label>
<select id="plantilla">
<option value="">-- Seleccionar --</option>
<option value="nota-simple">Nota Simple</option>
<option value="informe-tecnico">Informe Técnico</option>
<option value="memorando">Memorando</option>
<option value="solicitud">Nota de Solicitud</option>
</select>
</div>
<div class="section">
<label for="contenido">Archivo de contenido (.md):</label>
<input type="file" id="contenido" accept=".md,.txt">
</div>
<div class="section">
<h3>Datos de la Nota</h3>
<label>Número:</label>
<input type="text" id="numero" placeholder="Ej: 2601">
<label>Fecha:</label>
<input type="date" id="fecha">
<label>Destinatario:</label>
<input type="text" id="destinatario" placeholder="Nombre/Cargo">
<label>Asunto:</label>
<input type="text" id="asunto" placeholder="Descripción breve">
</div>
<div class="actions">
<button onclick="cargarPlantilla()">📄 Cargar Plantilla</button>
<button onclick="cargarContenido()">📂 Cargar Contenido</button>
<button onclick="generarVistaPrevia()">👁️ Vista Previa</button>
<button onclick="imprimir()">🖨️ Imprimir / PDF</button>
</div>
</aside>
<!-- Área de Edición -->
<main class="editor">
<h2>Editor de Contenido</h2>
<textarea id="editorTexto" placeholder="Cargá una plantilla o escribí el contenido aquí..."></textarea>
</main>
<!-- Vista Previa -->
<aside class="preview">
<h2>Vista Previa</h2>
<div id="vistaPrevia" class="documento">
<div class="placeholder">
<p>La vista previa aparecerá aquí...</p>
</div>
</div>
</aside>
</div>
<!-- Template de Nota (oculto) -->
<template id="templateNota">
<div class="nota">
<header class="membrete">
<div class="logo">
<img src="../logo-utn.png" alt="UTN FRLR" onerror="this.style.display='none'">
</div>
<div class="datos-institucion">
<h1>UNIVERSIDAD TECNOLÓGICA NACIONAL</h1>
<h2>Facultad Regional La Rioja</h2>
<p>Dirección de Tecnologías de la Información y Comunicación</p>
</div>
</header>
<div class="datos-nota">
<p><strong>Nota N°:</strong> <span class="campo-numero"></span></p>
<p><strong>Fecha:</strong> <span class="campo-fecha"></span></p>
</div>
<div class="destinatario">
<p><strong>Destinatario:</strong> <span class="campo-destinatario"></span></p>
<p><strong>Asunto:</strong> <span class="campo-asunto"></span></p>
</div>
<div class="cuerpo">
<!-- Contenido renderizado aquí -->
</div>
<footer class="firma">
<div class="espacio-firma"></div>
<p><strong class="nombre-remitente">DTIC - UTN FRLR</strong></p>
<p class="cargo-remitente">Dirección de Tecnologías de la Información y Comunicación</p>
</footer>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="app.js"></script>
</body>
</html>
+298
View File
@@ -0,0 +1,298 @@
/* =============================================
Estilos - Generador de Notas DTIC UTN-FRLR
============================================= */
:root {
--primary: #1a4d8f;
--secondary: #2c5f2d;
--accent: #97bc09;
--light: #f5f5f5;
--dark: #333;
--border: #ddd;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--light);
color: var(--dark);
line-height: 1.6;
}
.container {
display: grid;
grid-template-columns: 280px 1fr 1fr;
gap: 20px;
padding: 20px;
max-width: 1800px;
margin: 0 auto;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
height: fit-content;
position: sticky;
top: 20px;
}
.sidebar h2 {
color: var(--primary);
margin-bottom: 20px;
font-size: 1.2rem;
}
.section {
margin-bottom: 20px;
}
.section label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: var(--dark);
font-size: 0.9rem;
}
.section select,
.section input[type="text"],
.section input[type="date"],
.section input[type="file"] {
width: 100%;
padding: 8px;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 0.9rem;
margin-bottom: 10px;
}
.section h3 {
color: var(--primary);
font-size: 1rem;
margin-bottom: 10px;
border-bottom: 2px solid var(--primary);
padding-bottom: 5px;
}
.actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.actions button {
padding: 12px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s;
}
.actions button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.actions button:nth(1) { background: var(--primary); color: white; }
.actions button:nth(2) { background: var(--secondary); color: white; }
.actions button:nth(3) { background: var(--accent); color: white; }
.actions button:nth(4) { background: var(--dark); color: white; }
/* Editor */
.editor {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.editor h2 {
color: var(--primary);
margin-bottom: 15px;
}
#editorTexto {
width: 100%;
min-height: 500px;
padding: 15px;
border: 1px solid var(--border);
border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.9rem;
line-height: 1.6;
resize: vertical;
}
/* Preview */
.preview {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow-y: auto;
max-height: calc(100vh - 40px);
}
.preview h2 {
color: var(--primary);
margin-bottom: 15px;
position: sticky;
top: 0;
background: white;
padding-bottom: 10px;
border-bottom: 2px solid var(--border);
}
.placeholder {
text-align: center;
color: #999;
padding: 100px 20px;
}
/* Documento */
.documento {
background: white;
padding: 40px;
border: 1px solid #eee;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.nota {
max-width: 210mm; /* A4 width */
margin: 0 auto;
}
/* Membrete */
.membrete {
display: flex;
gap: 20px;
padding-bottom: 20px;
border-bottom: 3px solid var(--primary);
margin-bottom: 25px;
}
.logo img {
width: 100px;
height: auto;
}
.datos-institucion h1 {
font-size: 1.3rem;
color: var(--primary);
margin-bottom: 5px;
}
.datos-institucion h2 {
font-size: 1rem;
color: var(--secondary);
margin-bottom: 5px;
}
.datos-institucion p {
font-size: 0.85rem;
color: #666;
}
/* Datos de la nota */
.datos-nota,
.destinatario {
margin-bottom: 20px;
}
.datos-nota p,
.destinatario p {
margin-bottom: 5px;
}
/* Cuerpo */
.cuerpo {
min-height: 300px;
line-height: 1.8;
font-size: 11pt;
text-align: justify;
}
.cuerpo p {
margin-bottom: 15px;
}
/* Firma */
.firma {
margin-top: 50px;
text-align: center;
}
.espacio-firma {
height: 60px;
border-bottom: 1px solid var(--dark);
margin-bottom: 10px;
width: 60%;
margin-left: auto;
margin-right: auto;
}
.firma .nombre-remitente {
display: block;
font-size: 1rem;
}
.firma .cargo-remitente {
font-size: 0.85rem;
color: #666;
}
/* =============================================
Estilos para impresión
============================================= */
@media print {
body {
background: white;
}
.sidebar,
.editor,
.preview h2 {
display: none;
}
.container {
display: block;
padding: 0;
}
.preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
max-height: none;
overflow: visible;
box-shadow: none;
padding: 0;
}
.documento {
box-shadow: none;
padding: 0;
}
.nota {
padding: 20px;
}
@page {
margin: 2.5cm;
size: A4;
}
}