Files
oca-py.org/faq/index.html
T
webmanager ed645942a4 FAQ interna (no indexada) con CRUD sobre el webhook de n8n
Nueva página /faq/ que lee las preguntas del webhook, agrupa por área,
permite buscar y crear/editar/eliminar. Incluye meta robots noindex y
robots.txt con Disallow: /faq.
2026-11-07 17:13:28 +00:00

264 lines
11 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex" />
<meta name="googlebot" content="noindex, nofollow" />
<title>FAQ interna — OCA Paraguay</title>
<link rel="icon" type="image/svg+xml" href="../img/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Sora:wght@600;700;800&display=swap" rel="stylesheet" />
<style>
:root{
--plum:#714B67; --plum-dark:#4b2f43; --violet:#6C4CE0; --violet-soft:#8f7bf0;
--mint:#16C79A; --mint-dark:#0e8f6e; --py-red:#D52B1E; --py-blue:#0038A8;
--ink:#231b2b; --muted:#6b6473; --line:#e7e3ec; --bg:#faf9fc;
}
*{box-sizing:border-box}
body{margin:0;font-family:Inter,system-ui,sans-serif;background:var(--bg);color:var(--ink);line-height:1.55}
a{color:var(--violet)}
header{background:#fff;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20}
.wrap{max-width:980px;margin:0 auto;padding:0 20px}
.head{display:flex;align-items:center;gap:14px;padding:14px 0}
.head img{width:38px;height:38px;border-radius:9px}
.head h1{font-family:Sora,sans-serif;font-size:1.05rem;margin:0}
.head h1 small{display:block;font-family:Inter,sans-serif;font-weight:500;font-size:.72rem;color:var(--muted);letter-spacing:.04em;text-transform:uppercase}
.noidx{margin-left:auto;font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--py-red);border:1px solid currentColor;border-radius:999px;padding:4px 10px}
main{padding:28px 0 60px}
.bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:22px}
.bar input[type=search]{flex:1 1 240px;padding:11px 14px;border:1px solid var(--line);border-radius:12px;font:inherit;background:#fff}
.bar input:focus{outline:2px solid rgba(108,76,224,.25);border-color:var(--violet-soft)}
.btn{font:inherit;font-weight:600;border-radius:12px;border:1px solid transparent;padding:10px 16px;cursor:pointer;display:inline-flex;align-items:center;gap:8px}
.btn-primary{background:var(--violet);color:#fff}
.btn-primary:hover{background:#5b3fd0}
.btn-ghost{background:#fff;border-color:var(--line);color:var(--ink)}
.btn-ghost:hover{border-color:var(--violet-soft)}
.btn-danger{background:#fff;border-color:#f0c9c4;color:var(--py-red)}
.btn-danger:hover{background:#fdf1ef}
.msg{padding:12px 16px;border-radius:12px;margin-bottom:18px;font-size:.9rem;display:none}
.msg.show{display:block}
.msg.err{background:#fdf1ef;color:#8f2117;border:1px solid #f0c9c4}
.msg.ok{background:#eefaf5;color:#0b6b52;border:1px solid #bfeadd}
.area{margin:0 0 28px}
.area h2{font-family:Sora,sans-serif;font-size:.82rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:0 0 10px}
.card{background:#fff;border:1px solid var(--line);border-radius:14px;margin-bottom:10px;overflow:hidden}
.card summary{cursor:pointer;padding:15px 18px;font-weight:600;list-style:none;display:flex;align-items:center;gap:12px}
.card summary::-webkit-details-marker{display:none}
.card summary::after{content:"+";margin-left:auto;color:var(--violet);font-weight:800;font-size:1.1rem}
.card[open] summary::after{content:"–"}
.card .body{padding:0 18px 16px;color:#41394a}
.card .ans{white-space:pre-line;margin:0 0 14px;font-size:.93rem}
.acts{display:flex;gap:8px;flex-wrap:wrap}
.acts .btn{padding:7px 12px;font-size:.8rem;border-radius:9px}
.empty{color:var(--muted);padding:20px 0}
dialog{border:none;border-radius:16px;padding:0;max-width:560px;width:calc(100% - 32px);box-shadow:0 30px 80px rgba(0,0,0,.35)}
dialog::backdrop{background:rgba(35,27,43,.45)}
.dlg-head{padding:18px 22px;border-bottom:1px solid var(--line);display:flex;align-items:center}
.dlg-head h3{font-family:Sora,sans-serif;font-size:1rem;margin:0}
.dlg-head button{margin-left:auto;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--muted)}
form{padding:20px 22px 22px}
.field{margin-bottom:14px}
.field label{display:block;font-size:.78rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);margin-bottom:6px}
.field input,.field textarea{width:100%;padding:11px 13px;border:1px solid var(--line);border-radius:10px;font:inherit;background:#fff}
.field textarea{min-height:130px;resize:vertical}
.field input:focus,.field textarea:focus{outline:2px solid rgba(108,76,224,.22);border-color:var(--violet-soft)}
.dlg-foot{display:flex;gap:10px;justify-content:flex-end;margin-top:6px}
footer{border-top:1px solid var(--line);background:#fff;padding:18px 0;font-size:.78rem;color:var(--muted);text-align:center}
</style>
</head>
<body>
<header>
<div class="wrap head">
<img src="../img/logo.svg" alt="OCA Paraguay" width="38" height="38" />
<h1>Preguntas frecuentes<small>OCA Paraguay · uso interno</small></h1>
<span class="noidx">no indexada</span>
</div>
</header>
<main class="wrap">
<div class="msg" id="msg"></div>
<div class="bar">
<input type="search" id="q" placeholder="Buscar pregunta, área o respuesta…" aria-label="Buscar" />
<button class="btn btn-primary" id="add">+ Nueva pregunta</button>
<button class="btn btn-ghost" id="reload" title="Recargar">↻</button>
</div>
<div id="list"><p class="empty">Cargando…</p></div>
</main>
<footer>Página interna · datos vía webhook · <span id="count">—</span></footer>
<dialog id="dlg">
<div class="dlg-head"><h3 id="dlg-title">Nueva pregunta</h3><button type="button" id="close" aria-label="Cerrar">×</button></div>
<form id="form">
<input type="hidden" id="f-id" />
<div class="field"><label for="f-area">Área</label><input id="f-area" name="area" placeholder="Paraguay Tax" required /></div>
<div class="field"><label for="f-q">Pregunta</label><input id="f-q" name="question" placeholder="¿…?" required /></div>
<div class="field"><label for="f-a">Respuesta</label><textarea id="f-a" name="answer" placeholder="Respuesta…" required></textarea></div>
<div class="dlg-foot">
<button type="button" class="btn btn-ghost" id="cancel">Cancelar</button>
<button type="submit" class="btn btn-primary" id="save">Guardar</button>
</div>
</form>
</dialog>
<script>
const API = 'https://n8n.odoo4projects.com/webhook/oca-py/faq';
const el = (id) => document.getElementById(id);
let items = [];
function flash(text, kind) {
const m = el('msg');
m.textContent = text;
m.className = 'msg show ' + (kind || 'ok');
clearTimeout(flash._t);
flash._t = setTimeout(() => { m.className = 'msg'; }, 6000);
}
function normalize(payload) {
if (Array.isArray(payload)) {
if (payload.length && Array.isArray(payload[0].data)) return payload[0].data;
if (payload.length && payload[0].id !== undefined) return payload;
}
if (payload && Array.isArray(payload.data)) return payload.data;
return [];
}
async function load() {
try {
const res = await fetch(API, { headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error('HTTP ' + res.status);
items = normalize(await res.json());
render();
} catch (err) {
el('list').innerHTML = '';
flash('No se pudo leer el webhook (' + err.message + '). Si es CORS, hay que habilitar el origen https://oca-py.org en n8n.', 'err');
}
}
function render() {
const term = el('q').value.trim().toLowerCase();
const list = el('list');
const shown = items.filter(it => {
if (!term) return true;
return [it.area, it.Question, it.Answer].join(' ').toLowerCase().includes(term);
});
el('count').textContent = shown.length + ' de ' + items.length + ' preguntas';
list.innerHTML = '';
if (!shown.length) { list.innerHTML = '<p class="empty">Sin resultados.</p>'; return; }
const groups = {};
shown.forEach(it => { (groups[it.area || 'General'] = groups[it.area || 'General'] || []).push(it); });
Object.keys(groups).sort().forEach(area => {
const sec = document.createElement('section');
sec.className = 'area';
const h = document.createElement('h2');
h.textContent = area;
sec.appendChild(h);
groups[area].forEach(it => {
const card = document.createElement('details');
card.className = 'card';
const sum = document.createElement('summary');
sum.textContent = it.Question || '(sin pregunta)';
card.appendChild(sum);
const body = document.createElement('div');
body.className = 'body';
const ans = document.createElement('p');
ans.className = 'ans';
ans.textContent = it.Answer || '';
body.appendChild(ans);
const acts = document.createElement('div');
acts.className = 'acts';
const edit = document.createElement('button');
edit.className = 'btn btn-ghost';
edit.textContent = 'Editar';
edit.onclick = () => openForm(it);
const del = document.createElement('button');
del.className = 'btn btn-danger';
del.textContent = 'Eliminar';
del.onclick = () => remove(it);
acts.append(edit, del);
body.appendChild(acts);
card.appendChild(body);
sec.appendChild(card);
});
list.appendChild(sec);
});
}
function openForm(it) {
el('dlg-title').textContent = it ? 'Editar pregunta' : 'Nueva pregunta';
el('f-id').value = it ? it.id : '';
el('f-area').value = it ? (it.area || '') : '';
el('f-q').value = it ? (it.Question || '') : '';
el('f-a').value = it ? (it.Answer || '') : '';
el('dlg').showModal();
el(it ? 'f-q' : 'f-area').focus();
}
async function save(ev) {
ev.preventDefault();
const id = el('f-id').value;
const payload = {
area: el('f-area').value.trim(),
Question: el('f-q').value.trim(),
Answer: el('f-a').value.trim(),
};
if (id) payload.id = Number(id);
const btn = el('save');
btn.disabled = true; btn.textContent = 'Guardando…';
try {
const res = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error('HTTP ' + res.status);
el('dlg').close();
flash(id ? 'Pregunta actualizada.' : 'Pregunta creada.', 'ok');
await load();
} catch (err) {
flash('No se pudo guardar (' + err.message + ').', 'err');
} finally {
btn.disabled = false; btn.textContent = 'Guardar';
}
}
async function remove(it) {
if (!confirm('¿Eliminar «' + (it.Question || '') + '»?')) return;
try {
let res = await fetch(API, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: it.id }),
});
if (!res.ok) {
res = await fetch(API + '?id=' + encodeURIComponent(it.id), { method: 'DELETE' });
}
if (!res.ok) throw new Error('HTTP ' + res.status);
flash('Pregunta eliminada.', 'ok');
await load();
} catch (err) {
flash('No se pudo eliminar (' + err.message + ').', 'err');
}
}
el('add').onclick = () => openForm(null);
el('reload').onclick = load;
el('close').onclick = () => el('dlg').close();
el('cancel').onclick = () => el('dlg').close();
el('form').addEventListener('submit', save);
el('q').addEventListener('input', render);
load();
</script>
</body>
</html>