@thebattlearena/corner 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/ring/agents/cornerman.md +20 -3
- package/ring/agents/wrestler.md +11 -0
- package/ring/hooks/hooks.json +20 -0
- package/ring/scripts/bout-stats-render.mjs +366 -0
- package/ring/scripts/bout-stats.mjs +360 -0
- package/ring/scripts/config.mjs +1 -1
- package/ring/scripts/contract-endpoints.mjs +163 -0
- package/ring/scripts/contract-preview.mjs +34 -14
- package/ring/scripts/fight.mjs +27 -0
- package/ring/scripts/findings.mjs +3 -1
- package/ring/scripts/format-gate.mjs +723 -0
- package/ring/scripts/format-worker.mjs +120 -0
- package/ring/scripts/front-routes.mjs +345 -0
- package/ring/scripts/judges-card.mjs +324 -3
- package/ring/scripts/lookout.mjs +22 -4
- package/ring/scripts/mask-guard.mjs +34 -1
- package/ring/scripts/page-embed.mjs +112 -0
- package/ring/scripts/play-by-play.mjs +155 -7
- package/ring/scripts/preparation-collect.mjs +177 -0
- package/ring/scripts/preparation.mjs +547 -0
- package/ring/scripts/round-bell.mjs +9 -2
- package/ring/scripts/tech-plan-log.mjs +81 -0
- package/ring/scripts/tech-plan.mjs +191 -0
- package/ring/scripts/verdicts-log.mjs +75 -0
- package/ring/scripts/verdicts.mjs +205 -0
- package/ring/skills/play-by-play/SKILL.md +235 -33
- package/ring/skills/play-by-play/templates/blocks.css +196 -0
- package/ring/skills/play-by-play/templates/chronicle.html +358 -55
- package/ring/skills/play-by-play/templates/endpoints-view.js +1153 -0
- package/ring/skills/play-by-play/templates/interview-form.js +516 -0
- package/ring/skills/play-by-play/templates/now-panel.js +719 -0
- package/ring/skills/referee/SKILL.md +136 -42
- package/ring/skills/referee/references/api-diagram.md +56 -0
- package/ring/skills/referee/references/precedents.md +11 -0
- package/ring/skills/referee/references/rulebook.md +65 -4
- package/ring/skills/referee/templates/api-diagram.html +115 -0
- package/ring/skills/referee/templates/escalation.md +7 -1
- package/ring/skills/referee/templates/round-bell-body.md +4 -0
- package/ring/skills/referee/templates/wrestler-brief.md +3 -0
- package/ring/skills/sparring/SKILL.md +10 -2
- package/ring/skills/sparring/templates/contrato.md +29 -0
- package/ring/skills/sparring/templates/rudo-brief.md +7 -0
- package/ring/skills/weigh-in/SKILL.md +6 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
// «La entrevista» answered from the chronicle page. Every `ol.qa[data-escalation="pr<n>-E<k>"]`
|
|
2
|
+
// gets a form per question: each of the referee's options is itself selectable and its text
|
|
3
|
+
// editable (what is saved is the text as left), an «Otro» row with an open field, an optional
|
|
4
|
+
// comment, images to drop or attach, and «Guardar respuesta». The answer goes to the page's `db`
|
|
5
|
+
// (collection `respuestas`, doc `<escalation>-q<n>`: { escalation, question, choice, text, note,
|
|
6
|
+
// attachments, answeredAt, by }) and is painted live. Only who can edit the page (the judge) writes
|
|
7
|
+
// and uploads (capabilities: db rules write "admin", assets for editors); everyone else reads.
|
|
8
|
+
// The referee reads `respuestas` with ArtifactData and logs them (`judges-card.mjs page-answers`).
|
|
9
|
+
// The form is drawn even when the viewer does not serve the capabilities: then it says so on save.
|
|
10
|
+
(() => {
|
|
11
|
+
// The page's bus: what the judge has to do, registered by each script, read by «Qué necesita de
|
|
12
|
+
// ti ahora» (now-panel.js), which answers and judges through the same functions.
|
|
13
|
+
const ring = (window.ringPage ??= {
|
|
14
|
+
questions: new Map(),
|
|
15
|
+
boards: new Map(),
|
|
16
|
+
scales: {},
|
|
17
|
+
changed() {
|
|
18
|
+
document.dispatchEvent(new CustomEvent('ring:change'));
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const lists = [...document.querySelectorAll('ol.qa[data-escalation]')];
|
|
22
|
+
if (!lists.length) return;
|
|
23
|
+
const waitClaude = () =>
|
|
24
|
+
new Promise((resolve) => {
|
|
25
|
+
let waited = 0;
|
|
26
|
+
const tick = () => {
|
|
27
|
+
if (window.claude && window.claude.use) return resolve(window.claude);
|
|
28
|
+
if ((waited += 200) > 10000) return resolve(null);
|
|
29
|
+
setTimeout(tick, 200);
|
|
30
|
+
};
|
|
31
|
+
tick();
|
|
32
|
+
});
|
|
33
|
+
const fmt = (iso) => {
|
|
34
|
+
try {
|
|
35
|
+
return new Date(iso).toLocaleString('es-MX', {
|
|
36
|
+
day: 'numeric',
|
|
37
|
+
month: 'short',
|
|
38
|
+
hour: '2-digit',
|
|
39
|
+
minute: '2-digit',
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const make = (tag, props = {}) =>
|
|
46
|
+
Object.assign(document.createElement(tag), props);
|
|
47
|
+
// Checking a radio from code fires no `change`: emit it so «Guardar respuesta» enables.
|
|
48
|
+
const pick = (radio) => {
|
|
49
|
+
if (radio.checked) return;
|
|
50
|
+
radio.checked = true;
|
|
51
|
+
radio.dispatchEvent(new Event('change', { bubbles: true }));
|
|
52
|
+
};
|
|
53
|
+
const api = { db: null, user: null, assets: null };
|
|
54
|
+
const ready = (async () => {
|
|
55
|
+
const claude = await waitClaude();
|
|
56
|
+
if (!claude) return;
|
|
57
|
+
api.db = await claude.use('db');
|
|
58
|
+
api.user = await claude.use('user');
|
|
59
|
+
api.assets = await claude.use('assets');
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
// A minimal Markdown renderer for the questions' context: headings, paragraphs, lists, code
|
|
63
|
+
// blocks, tables, inline code, bold, italics and links. It escapes the HTML first, so the text
|
|
64
|
+
// can never inject markup.
|
|
65
|
+
const escapeHtml = (text) =>
|
|
66
|
+
String(text)
|
|
67
|
+
.replace(/&/g, '&')
|
|
68
|
+
.replace(/</g, '<')
|
|
69
|
+
.replace(/>/g, '>')
|
|
70
|
+
.replace(/"/g, '"');
|
|
71
|
+
const emphasis = (text) =>
|
|
72
|
+
escapeHtml(text)
|
|
73
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
74
|
+
.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, '$1<em>$2</em>')
|
|
75
|
+
.replace(
|
|
76
|
+
/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
|
|
77
|
+
'<a href="$2" target="_blank" rel="noopener">$1</a>',
|
|
78
|
+
);
|
|
79
|
+
// Code spans first, so a `*` inside code never opens an emphasis.
|
|
80
|
+
const inline = (text) =>
|
|
81
|
+
String(text)
|
|
82
|
+
.split(/(`[^`]+`)/)
|
|
83
|
+
.map((part) =>
|
|
84
|
+
/^`[^`]+`$/.test(part)
|
|
85
|
+
? `<code>${escapeHtml(part.slice(1, -1))}</code>`
|
|
86
|
+
: emphasis(part),
|
|
87
|
+
)
|
|
88
|
+
.join('');
|
|
89
|
+
const cells = (line) =>
|
|
90
|
+
line
|
|
91
|
+
.trim()
|
|
92
|
+
.replace(/^\||\|$/g, '')
|
|
93
|
+
.split('|')
|
|
94
|
+
.map((cell) => cell.trim());
|
|
95
|
+
function markdown(source) {
|
|
96
|
+
const lines = String(source).replace(/\r/g, '').split('\n');
|
|
97
|
+
const out = [];
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < lines.length) {
|
|
100
|
+
const line = lines[i];
|
|
101
|
+
if (/^```/.test(line)) {
|
|
102
|
+
const code = [];
|
|
103
|
+
i += 1;
|
|
104
|
+
while (i < lines.length && !/^```/.test(lines[i]))
|
|
105
|
+
code.push(lines[i++]);
|
|
106
|
+
i += 1;
|
|
107
|
+
out.push(`<pre><code>${escapeHtml(code.join('\n'))}</code></pre>`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
111
|
+
if (heading) {
|
|
112
|
+
const level = Math.min(heading[1].length + 2, 6);
|
|
113
|
+
out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
|
|
114
|
+
i += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
/^\s*\|.*\|\s*$/.test(line) &&
|
|
119
|
+
/^\s*\|?\s*:?-{3,}/.test(lines[i + 1] ?? '')
|
|
120
|
+
) {
|
|
121
|
+
const head = cells(line);
|
|
122
|
+
i += 2;
|
|
123
|
+
const rows = [];
|
|
124
|
+
while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i]))
|
|
125
|
+
rows.push(cells(lines[i++]));
|
|
126
|
+
out.push(
|
|
127
|
+
`<div class="tbl"><table><thead><tr>${head.map((cell) => `<th>${inline(cell)}</th>`).join('')}</tr></thead><tbody>${rows
|
|
128
|
+
.map(
|
|
129
|
+
(row) =>
|
|
130
|
+
`<tr>${row.map((cell) => `<td>${inline(cell)}</td>`).join('')}</tr>`,
|
|
131
|
+
)
|
|
132
|
+
.join('')}</tbody></table></div>`,
|
|
133
|
+
);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const bullet = /^\s*(?:[-*]|\d+\.)\s+/;
|
|
137
|
+
if (bullet.test(line)) {
|
|
138
|
+
const ordered = /^\s*\d+\./.test(line);
|
|
139
|
+
const items = [];
|
|
140
|
+
while (i < lines.length && bullet.test(lines[i]))
|
|
141
|
+
items.push(lines[i++].replace(bullet, ''));
|
|
142
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
143
|
+
out.push(
|
|
144
|
+
`<${tag}>${items.map((item) => `<li>${inline(item)}</li>`).join('')}</${tag}>`,
|
|
145
|
+
);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!line.trim()) {
|
|
149
|
+
i += 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const paragraph = [];
|
|
153
|
+
while (
|
|
154
|
+
i < lines.length &&
|
|
155
|
+
lines[i].trim() &&
|
|
156
|
+
!/^(```|#{1,6}\s|\s*(?:[-*]|\d+\.)\s+|\s*\|)/.test(lines[i])
|
|
157
|
+
)
|
|
158
|
+
paragraph.push(lines[i++]);
|
|
159
|
+
if (!paragraph.length) paragraph.push(lines[i++]);
|
|
160
|
+
out.push(`<p>${inline(paragraph.join(' '))}</p>`);
|
|
161
|
+
}
|
|
162
|
+
return out.join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// One accessible modal for every question: a native <dialog> traps the focus and closes with Esc;
|
|
166
|
+
// a click outside its box closes it too, and the focus goes back to the button that opened it.
|
|
167
|
+
let dialog = null;
|
|
168
|
+
let opener = null;
|
|
169
|
+
function openContext(title, source) {
|
|
170
|
+
if (!dialog) {
|
|
171
|
+
dialog = make('dialog', { className: 'ctx-dialog' });
|
|
172
|
+
dialog.setAttribute('aria-labelledby', 'ctx-title');
|
|
173
|
+
const close = make('button', {
|
|
174
|
+
type: 'button',
|
|
175
|
+
className: 'ctx-close',
|
|
176
|
+
textContent: '×',
|
|
177
|
+
ariaLabel: 'Cerrar',
|
|
178
|
+
});
|
|
179
|
+
close.setAttribute('aria-label', 'Cerrar');
|
|
180
|
+
close.onclick = () => dialog.close();
|
|
181
|
+
dialog.append(
|
|
182
|
+
make('div', { className: 'ctx-head' }),
|
|
183
|
+
make('div', { className: 'ctx-body' }),
|
|
184
|
+
);
|
|
185
|
+
dialog
|
|
186
|
+
.querySelector('.ctx-head')
|
|
187
|
+
.append(make('h3', { id: 'ctx-title' }), close);
|
|
188
|
+
dialog.addEventListener('click', (event) => {
|
|
189
|
+
if (event.target === dialog) dialog.close();
|
|
190
|
+
});
|
|
191
|
+
dialog.addEventListener('close', () => opener?.focus());
|
|
192
|
+
document.body.append(dialog);
|
|
193
|
+
}
|
|
194
|
+
opener = document.activeElement;
|
|
195
|
+
dialog.querySelector('#ctx-title').textContent = title;
|
|
196
|
+
dialog.querySelector('.ctx-body').innerHTML = markdown(source);
|
|
197
|
+
dialog.showModal();
|
|
198
|
+
dialog.querySelector('.ctx-close').focus();
|
|
199
|
+
}
|
|
200
|
+
ring.openContext = openContext;
|
|
201
|
+
|
|
202
|
+
/** Uploads one image for the judge; resolves its asset id and url, or throws a message. */
|
|
203
|
+
ring.upload = async (picked) => {
|
|
204
|
+
await ready;
|
|
205
|
+
if (!api.assets)
|
|
206
|
+
throw new Error('Adjuntar sólo funciona para quien puede editar la página');
|
|
207
|
+
if (!picked.type.startsWith('image/'))
|
|
208
|
+
throw new Error(`${picked.name}: sólo imágenes`);
|
|
209
|
+
try {
|
|
210
|
+
return await api.assets.upload(picked);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`No se pudo subir ${picked.name} (${error.code || 'error'})`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** Saves the judge's answer to one question; throws a message the form can show. */
|
|
219
|
+
async function writeAnswer(esc, n, { choice, text, note = '', attachments = [] }) {
|
|
220
|
+
await ready;
|
|
221
|
+
if (!api.db) throw new Error('Esta vista no puede guardar respuestas');
|
|
222
|
+
try {
|
|
223
|
+
const by = api.user ? await api.user.id() : null;
|
|
224
|
+
await api.db
|
|
225
|
+
.collection('respuestas')
|
|
226
|
+
.doc(`${esc}-q${n}`)
|
|
227
|
+
.set({
|
|
228
|
+
escalation: esc,
|
|
229
|
+
question: Number(n),
|
|
230
|
+
choice,
|
|
231
|
+
text,
|
|
232
|
+
note: String(note).trim(),
|
|
233
|
+
attachments: attachments.slice(),
|
|
234
|
+
answeredAt: new Date().toISOString(),
|
|
235
|
+
by,
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
['permission_denied', 'not_granted'].includes(error.code)
|
|
240
|
+
? 'Sólo el juez (quien edita la página) puede contestar'
|
|
241
|
+
: `No se guardó (${error.code || 'error'})`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const questionsOf = (list) =>
|
|
247
|
+
[...list.children]
|
|
248
|
+
.map((li) => ({
|
|
249
|
+
n: (li.querySelector('.q')?.textContent.match(/^\s*(\d+)/) || [])[1],
|
|
250
|
+
q: li.querySelector('.q'),
|
|
251
|
+
ul: li.querySelector('ul'),
|
|
252
|
+
box: li.querySelector('.answer'),
|
|
253
|
+
}))
|
|
254
|
+
.filter((item) => item.n && item.ul && item.box);
|
|
255
|
+
|
|
256
|
+
function formFor(esc, item, contexts) {
|
|
257
|
+
const name = `q-${esc}-${item.n}`;
|
|
258
|
+
const id = `${esc}-q${item.n}`;
|
|
259
|
+
item.q.closest('li').id = id;
|
|
260
|
+
const time = item.q.querySelector('time')?.textContent.trim() ?? '';
|
|
261
|
+
const title = [...item.q.childNodes]
|
|
262
|
+
.filter((node) => !(node.nodeType === 1 && node.tagName === 'TIME'))
|
|
263
|
+
.map((node) => node.textContent)
|
|
264
|
+
.join('')
|
|
265
|
+
.trim();
|
|
266
|
+
const context = contexts[item.n];
|
|
267
|
+
if (context)
|
|
268
|
+
item.q.append(
|
|
269
|
+
' ',
|
|
270
|
+
make('button', {
|
|
271
|
+
type: 'button',
|
|
272
|
+
className: 'more',
|
|
273
|
+
textContent: 'Ver más información',
|
|
274
|
+
onclick: () =>
|
|
275
|
+
openContext(
|
|
276
|
+
item.q.textContent.replace('Ver más información', '').trim(),
|
|
277
|
+
context,
|
|
278
|
+
),
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
[...item.ul.children].forEach((option) => {
|
|
282
|
+
const key = option.querySelector('i')?.textContent.trim();
|
|
283
|
+
if (!key) return;
|
|
284
|
+
const radio = make('input', {
|
|
285
|
+
type: 'radio',
|
|
286
|
+
name,
|
|
287
|
+
value: key,
|
|
288
|
+
className: 'pick',
|
|
289
|
+
});
|
|
290
|
+
option.prepend(radio);
|
|
291
|
+
option.classList.add('selectable');
|
|
292
|
+
const span = make('span', {
|
|
293
|
+
className: 'otext',
|
|
294
|
+
contentEditable: 'true',
|
|
295
|
+
spellcheck: true,
|
|
296
|
+
title: 'Puedes editar el texto antes de guardar',
|
|
297
|
+
});
|
|
298
|
+
[...option.childNodes]
|
|
299
|
+
.filter(
|
|
300
|
+
(node) =>
|
|
301
|
+
node !== radio && !(node.nodeType === 1 && node.tagName === 'I'),
|
|
302
|
+
)
|
|
303
|
+
.forEach((node) => span.appendChild(node));
|
|
304
|
+
option.appendChild(span);
|
|
305
|
+
option.addEventListener('click', (event) => {
|
|
306
|
+
if (event.target !== span) pick(radio);
|
|
307
|
+
});
|
|
308
|
+
span.addEventListener('focus', () => pick(radio));
|
|
309
|
+
});
|
|
310
|
+
const other = make('li', { className: 'selectable other' });
|
|
311
|
+
const otherRadio = make('input', {
|
|
312
|
+
type: 'radio',
|
|
313
|
+
name,
|
|
314
|
+
value: 'otro',
|
|
315
|
+
className: 'pick',
|
|
316
|
+
});
|
|
317
|
+
const otherText = make('input', {
|
|
318
|
+
type: 'text',
|
|
319
|
+
className: 'otro-text',
|
|
320
|
+
placeholder: 'Otra opción: escríbela aquí',
|
|
321
|
+
});
|
|
322
|
+
otherText.addEventListener('focus', () => pick(otherRadio));
|
|
323
|
+
other.append(otherRadio, make('i', { textContent: '✎' }), otherText);
|
|
324
|
+
item.ul.append(other);
|
|
325
|
+
|
|
326
|
+
const form = make('div', { className: 'ans-form' });
|
|
327
|
+
const note = make('textarea', {
|
|
328
|
+
placeholder: 'Comentario para el réferi (opcional)',
|
|
329
|
+
});
|
|
330
|
+
const drop = make('div', {
|
|
331
|
+
className: 'drop',
|
|
332
|
+
tabIndex: 0,
|
|
333
|
+
textContent: 'Arrastra imágenes aquí o haz clic para adjuntar',
|
|
334
|
+
});
|
|
335
|
+
const file = make('input', {
|
|
336
|
+
type: 'file',
|
|
337
|
+
accept: 'image/*',
|
|
338
|
+
multiple: true,
|
|
339
|
+
hidden: true,
|
|
340
|
+
});
|
|
341
|
+
const thumbs = make('div', { className: 'thumbs' });
|
|
342
|
+
const save = make('button', {
|
|
343
|
+
type: 'button',
|
|
344
|
+
className: 'save',
|
|
345
|
+
textContent: 'Guardar respuesta',
|
|
346
|
+
disabled: true,
|
|
347
|
+
});
|
|
348
|
+
// Nothing comes picked, not even the recommended option: saving needs a choice of the judge.
|
|
349
|
+
item.ul.addEventListener('change', () => {
|
|
350
|
+
save.disabled = !item.ul.querySelector(`input[name="${name}"]:checked`);
|
|
351
|
+
});
|
|
352
|
+
const status = make('span', { className: 'status' });
|
|
353
|
+
const row = make('div', { className: 'row' });
|
|
354
|
+
row.append(save, status);
|
|
355
|
+
form.append(note, drop, file, thumbs, row);
|
|
356
|
+
item.ul.after(form);
|
|
357
|
+
|
|
358
|
+
const attached = [];
|
|
359
|
+
const addFiles = async (files) => {
|
|
360
|
+
await ready;
|
|
361
|
+
if (!api.assets)
|
|
362
|
+
return void (status.textContent =
|
|
363
|
+
'Adjuntar sólo funciona para quien puede editar la página');
|
|
364
|
+
for (const picked of files) {
|
|
365
|
+
if (!picked.type.startsWith('image/')) {
|
|
366
|
+
status.textContent = `${picked.name}: sólo imágenes`;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
status.textContent = `Subiendo ${picked.name}…`;
|
|
370
|
+
try {
|
|
371
|
+
const up = await api.assets.upload(picked);
|
|
372
|
+
attached.push(up.id);
|
|
373
|
+
thumbs.append(make('img', { src: up.url, alt: picked.name }));
|
|
374
|
+
status.textContent = '';
|
|
375
|
+
} catch (error) {
|
|
376
|
+
status.textContent = `No se pudo subir ${picked.name} (${error.code || 'error'})`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
drop.onclick = () => file.click();
|
|
381
|
+
drop.onkeydown = (event) => {
|
|
382
|
+
if (event.key === 'Enter' || event.key === ' ') file.click();
|
|
383
|
+
};
|
|
384
|
+
file.onchange = () => addFiles([...file.files]);
|
|
385
|
+
drop.ondragover = (event) => {
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
drop.classList.add('over');
|
|
388
|
+
};
|
|
389
|
+
drop.ondragleave = () => drop.classList.remove('over');
|
|
390
|
+
drop.ondrop = (event) => {
|
|
391
|
+
event.preventDefault();
|
|
392
|
+
drop.classList.remove('over');
|
|
393
|
+
addFiles([...event.dataTransfer.files]);
|
|
394
|
+
};
|
|
395
|
+
save.onclick = async () => {
|
|
396
|
+
const picked = item.ul.querySelector(`input[name="${name}"]:checked`);
|
|
397
|
+
if (!picked) return void (status.textContent = 'Elige una opción');
|
|
398
|
+
const choice = picked.value;
|
|
399
|
+
const text =
|
|
400
|
+
choice === 'otro'
|
|
401
|
+
? otherText.value.trim()
|
|
402
|
+
: picked.closest('li').querySelector('.otext')?.textContent.trim();
|
|
403
|
+
if (!text) return void (status.textContent = 'Escribe tu opción');
|
|
404
|
+
save.disabled = true;
|
|
405
|
+
status.textContent = 'Guardando…';
|
|
406
|
+
try {
|
|
407
|
+
await writeAnswer(esc, item.n, {
|
|
408
|
+
choice,
|
|
409
|
+
text,
|
|
410
|
+
note: note.value,
|
|
411
|
+
attachments: attached,
|
|
412
|
+
});
|
|
413
|
+
status.textContent = 'Guardada';
|
|
414
|
+
} catch (error) {
|
|
415
|
+
status.textContent = error.message;
|
|
416
|
+
}
|
|
417
|
+
save.disabled = false;
|
|
418
|
+
};
|
|
419
|
+
const options = [...item.ul.querySelectorAll('li.selectable:not(.other)')].map(
|
|
420
|
+
(option) => ({
|
|
421
|
+
key: option.querySelector('input.pick').value,
|
|
422
|
+
text: () => option.querySelector('.otext')?.textContent.trim() ?? '',
|
|
423
|
+
rec: option.classList.contains('rec'),
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
ring.questions.set(id, {
|
|
427
|
+
id,
|
|
428
|
+
esc,
|
|
429
|
+
pr: Number((/^pr(\d+)-/.exec(esc) || [])[1]) || null,
|
|
430
|
+
n: Number(item.n),
|
|
431
|
+
title,
|
|
432
|
+
time,
|
|
433
|
+
hasContext: Boolean(context),
|
|
434
|
+
context: context ?? null,
|
|
435
|
+
options,
|
|
436
|
+
answered: !item.box.classList.contains('pending'),
|
|
437
|
+
save: (payload) => writeAnswer(esc, item.n, payload),
|
|
438
|
+
});
|
|
439
|
+
return { name, otherText, note, id };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function paint(item, ui, answer) {
|
|
443
|
+
item.box.className = 'answer done';
|
|
444
|
+
item.box.textContent = '';
|
|
445
|
+
const choice = answer.choice === 'otro' ? 'Otro' : `(${answer.choice})`;
|
|
446
|
+
item.box.append(
|
|
447
|
+
make('div', {
|
|
448
|
+
textContent: `Juez · ${fmt(answer.answeredAt)} · ${choice} — ${answer.text || ''}${answer.note ? ` · ${answer.note}` : ''}`,
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
(answer.attachments || []).forEach((id) => {
|
|
452
|
+
const image = make('img', {
|
|
453
|
+
src: `/_blob/${id}`,
|
|
454
|
+
alt: 'Adjunto del juez',
|
|
455
|
+
});
|
|
456
|
+
image.onerror = () =>
|
|
457
|
+
image.replaceWith(document.createTextNode(' [adjunto no disponible] '));
|
|
458
|
+
item.box.append(image);
|
|
459
|
+
});
|
|
460
|
+
const radio = item.ul.querySelector(
|
|
461
|
+
`input[name="${ui.name}"][value="${answer.choice}"]`,
|
|
462
|
+
);
|
|
463
|
+
if (radio) radio.checked = true;
|
|
464
|
+
item.ul.dispatchEvent(new Event('change'));
|
|
465
|
+
if (answer.choice === 'otro' && document.activeElement !== ui.otherText)
|
|
466
|
+
ui.otherText.value = answer.text || '';
|
|
467
|
+
if (document.activeElement !== ui.note) ui.note.value = answer.note || '';
|
|
468
|
+
const entry = ring.questions.get(ui.id);
|
|
469
|
+
if (entry && !entry.answered) {
|
|
470
|
+
entry.answered = true;
|
|
471
|
+
ring.changed();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const contextsOf = (esc) => {
|
|
476
|
+
try {
|
|
477
|
+
return JSON.parse(
|
|
478
|
+
document.getElementById(`ring-context-${esc}`)?.textContent ?? '{}',
|
|
479
|
+
);
|
|
480
|
+
} catch {
|
|
481
|
+
return {};
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const escalations = lists.map((list) => {
|
|
485
|
+
const esc = list.dataset.escalation;
|
|
486
|
+
const contexts = contextsOf(esc);
|
|
487
|
+
const items = questionsOf(list).map((item) => ({
|
|
488
|
+
...item,
|
|
489
|
+
ui: formFor(esc, item, contexts),
|
|
490
|
+
}));
|
|
491
|
+
return { esc, items };
|
|
492
|
+
});
|
|
493
|
+
ring.changed();
|
|
494
|
+
|
|
495
|
+
ready.then(() => {
|
|
496
|
+
if (!api.db) return;
|
|
497
|
+
escalations.forEach(({ esc, items }) => {
|
|
498
|
+
api.db
|
|
499
|
+
.collection('respuestas')
|
|
500
|
+
.where('escalation', '==', esc)
|
|
501
|
+
.onSnapshot(
|
|
502
|
+
(snap) => {
|
|
503
|
+
const byQuestion = {};
|
|
504
|
+
snap.docs.forEach((doc) => {
|
|
505
|
+
const value = doc.data();
|
|
506
|
+
if (value) byQuestion[String(value.question)] = value;
|
|
507
|
+
});
|
|
508
|
+
items.forEach((item) => {
|
|
509
|
+
if (byQuestion[item.n]) paint(item, item.ui, byQuestion[item.n]);
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
() => {},
|
|
513
|
+
);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
})();
|