PageSourceSearch

https://www.cognijus.com/js/assessor-core.js?v=7ed0a91c8b0c

js cognijus.com collected 2026-09-24 13:30:15 UTC 205,956 bytes, 4,842 lines download raw bytes

1// funcionalidadeName is intentionally a `var` (not `const`) so this shared
2// script can coexist on the ERP page without a top-level redeclaration error.
3var funcionalidadeName = (window.__COGNIJUS_GP_PAGE__)
4    ? 'Gestão Processual'
5    : ((window.__COGNIJUS_ASSESSOR_LANG__ === 'en') ? 'Legal Advisor' : 'Assessor jurídico');
6const __isEN = (window.__COGNIJUS_ASSESSOR_LANG__ === 'en');
7const __L = {
8    attn: __isEN ? 'Attention' : 'Atenção',
9    choosePdf: __isEN ? 'Please select at least one PDF file.' : 'Escolha pelo menos um arquivo PDF.',
10    invalidPdf: (name) => __isEN ? `The file "${name}" is not a valid PDF.` : `O arquivo "${name}" não é um PDF válido.`,
11    invalidPages: __isEN ? 'Invalid page format. Use numbers separated by commas and hyphens for ranges (e.g.: 1, 3, 5-7)' : 'Formato de páginas inválido. Use números separados por vírgula e hífens para intervalos (Ex: 1, 3, 5-7)',
12    selectPersona: __isEN ? 'Please select a persona before analyzing.' : 'Selecione uma persona antes de analisar.',
13    selectPiece: __isEN ? 'Please select a draft before analyzing.' : 'Selecione uma peça antes de analisar.',
14    queued: __isEN ? 'Your analysis has been queued and will be processed shortly.' : 'Sua análise foi enfileirada e será processada em breve.',
15    queuedTitle: __isEN ? 'Analysis Queued' : 'Análise Enfileirada',
16    enabled: __isEN ? 'Enabled' : 'Ativada',
17    disabled: __isEN ? 'Disabled' : 'Desativada',
18    selectPiece2: __isEN ? 'Select a draft' : 'Selecione uma peça',
19};
20
21function assessorHtmlNewlinesToBr(html) {
22    return String(html || '')
23        .replace(/<\/(p|div|h[1-6]|blockquote|ul|ol|li|table|thead|tbody|tr|hr)>\s*(?:\n|<br\s*\/?\s*>)+\s*/gi, '</$1>')
24        .replace(/(?:\n|<br\s*\/?\s*>)+\s*(<(?:p|div|h[1-6]|blockquote|ul|ol|table|hr)\b)/gi, '$1')
25        .replace(/(?:\n|<br\s*\/?\s*>)+\s*(<blockquote\b)/gi, '$1')
26        .replace(/(<\/blockquote>)\s*(?:\n|<br\s*\/?\s*>)+/gi, '$1')
27        .replace(/\n/g, '<br>');
28}
29
30// Catálogo de peças/personas (config/assessor-config.json via /extension/assessor-config)
31var buttonConfig = [];
32let gestaoAssessorConfigLoaded = false;
33let gestaoUniquePersonas = [];
34
35function getAssessorLimits() {
36    return window.__COGNIJUS_ASSESSOR_LIMITS__ || {
37        maxPdfBytes: 104857600,
38        maxPdfBytesProMax: 262144000,
39        maxPdfMB: 100,
40        maxPdfMBProMax: 250,
41        uploadMaxMbFree: 100,
42        uploadMaxMbPro: 100,
43        uploadMaxMbProMax: 250
44    };
45}
46
47function isAssessorUserPro() {
48    return !!(typeof cognijusUserData !== 'undefined' && cognijusUserData && cognijusUserData.isPro);
49}
50
51function isAssessorUserProMax() {
52    return !!(typeof cognijusUserData !== 'undefined' && cognijusUserData && cognijusUserData.isProMax);
53}
54
55function getAssessorUploadMaxBytes() {
56    const limits = getAssessorLimits();
57    const mb = isAssessorUserProMax() ? limits.uploadMaxMbProMax : (isAssessorUserPro() ? limits.uploadMaxMbPro : limits.uploadMaxMbFree);
58    return mb * 1024 * 1024;
59}
60
61function getAssessorUploadMaxMbLabel() {
62    const limits = getAssessorLimits();
63    return isAssessorUserProMax() ? limits.uploadMaxMbProMax : (isAssessorUserPro() ? limits.uploadMaxMbPro : limits.uploadMaxMbFree);
64}
65
66function formatAssessorFileSizeLabel(bytes) {
67    const n = Number(bytes) || 0;
68    if (n >= 1048576) return (n / 1048576).toFixed(1) + ' MB';
69    return Math.round(n / 1024) + ' KB';
70}
71
72function formatAssessorCharCountLabel(n) {
73    const chars = Number(n);
74    if (!Number.isFinite(chars) || chars <= 0) return '';
75    if (chars < 1000) return chars + ' Chars';
76    return Math.round(chars / 1000) + 'K Chars';
77}
78
79function buildGestaoSizeCellHtml(p) {
80    if (!p || !p.fileSize) return '—';
81    const sizeLabel = formatAssessorFileSizeLabel(p.fileSize);
82    const charsLabel = formatAssessorCharCountLabel(p.charCount);
83    return `<div class="gp-size-stack"><span class="gp-size-bytes">${sizeLabel}</span>${charsLabel ? `<span class="gp-size-chars">${charsLabel}</span>` : ''}</div>`;
84}
85
86function assessorFileTooLargeMessage(fileOrLabel, sizeBytes) {
87    const label = typeof fileOrLabel === 'string' ? fileOrLabel : fileOrLabel.name;
88    const size = typeof sizeBytes === 'number' ? sizeBytes : (fileOrLabel.size || 0);
89    const limitMb = getAssessorUploadMaxMbLabel();
90    return __isEN
91        ? `"${label}" (${formatAssessorFileSizeLabel(size)}) exceeds the ${limitMb} MB limit for your plan. Use Focus Pages or a smaller PDF.`
92        : `"${label}" (${formatAssessorFileSizeLabel(size)}) excede o limite de ${limitMb} MB do seu plano. Use Páginas em Foco ou um PDF menor.`;
93}
94
95function validateAssessorFileForAnalysis(file, options) {
96    if (!file) return false;
97    const uploadMax = getAssessorUploadMaxBytes();
98    const size = file.size || 0;
99    const useFuncMsg = !!(options && options.useFuncMsg);
100
101    if (size > uploadMax) {
102        if (useFuncMsg) {
103            funcMsg('warning', __L.attn, assessorFileTooLargeMessage(file), '', '', undefined, options && options.insight);
104        }
105        return false;
106    }
107
108    return true;
109}
110
111function validateAssessorFilesForAnalysis(files, options) {
112    if (!Array.isArray(files) || files.length === 0) return false;
113    let totalBytes = 0;
114    for (const file of files) {
115        if (!validateAssessorFileForAnalysis(file, options)) return false;
116        totalBytes += file.size || 0;
117    }
118    if (totalBytes > getAssessorUploadMaxBytes()) {
119        const msg = assessorFileTooLargeMessage(
120            __isEN ? 'Selected files (combined)' : 'Arquivos selecionados (total)',
121            totalBytes
122        );
123        if (options && options.useFuncMsg) {
124            funcMsg('warning', __L.attn, msg, '', '', undefined, options && options.insight);
125        }
126        return false;
127    }
128    return true;
129}
130
131function validateAssessorFileForStorage(file, options) {
132    return validateAssessorFileForAnalysis(file, options);
133}
134
135function escapeGestaoHtml(text) {
136    return String(text || '')
137        .replace(/&/g, '&amp;')
138        .replace(/</g, '&lt;')
139        .replace(/>/g, '&gt;')
140        .replace(/"/g, '&quot;');
141}
142
143// Busboy (latin1) + Finder (NFD/UTF-8) grava "Anotações" / "jurisprudência".
144// Reinterpreta code points 0-255 como UTF-8 quando o decode é válido; NFC
145// depois, para o @ bater com o teclado. Espelha normalizeAssessorFilename.
146function normalizeGestaoFilename(name) {
147    const raw = String(name == null ? '' : name);
148    if (!raw) return '';
149    let hasBeyondLatin1 = false;
150    for (let i = 0; i < raw.length; i++) {
151        if (raw.charCodeAt(i) > 255) {
152            hasBeyondLatin1 = true;
153            break;
154        }
155    }
156    if (!hasBeyondLatin1) {
157        try {
158            const bytes = new Uint8Array(raw.length);
159            for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
160            const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
161            return decoded.normalize('NFC');
162        } catch (_) { /* nome já era Unicode NFC (ç/õ), não mojibake */ }
163    }
164    try {
165        return raw.normalize('NFC');
166    } catch (_) {
167        return raw;
168    }
169}
170
171// Arquivos individuais que formaram um "Processo unificado". Uploads antigos só
172// têm originalFileNames (sem fronteiras de página) e caem no fallback.
173// Espelha resolveAssessorMergedParts no servidor.
174function gestaoResolveMergedParts(p) {
175    if (!p) return [];
176    if (Array.isArray(p.mergedParts) && p.mergedParts.length > 1) {
177        return p.mergedParts
178            .filter(part => part && part.name)
179            .map((part, index) => ({
180                index: Number.isInteger(part.index) ? part.index : index,
181                name: normalizeGestaoFilename(part.name),
182                originalName: normalizeGestaoFilename(part.originalName || part.name),
183                convertedFromWord: !!part.convertedFromWord,
184                size: Number(part.size) || 0,
185                startPage: Number(part.startPage) || 0,
186                endPage: Number(part.endPage) || 0
187            }));
188    }
189    const names = Array.isArray(p.originalFileNames) ? p.originalFileNames.filter(Boolean) : [];
190    if (names.length > 1) {
191        return names.map((name, index) => ({
192            index,
193            name: normalizeGestaoFilename(name),
194            originalName: normalizeGestaoFilename(name),
195            convertedFromWord: false,
196            size: 0,
197            startPage: 0,
198            endPage: 0
199        }));
200    }
201    return [];
202}
203
204function gestaoPartPagesLabel(part) {
205    if (!part || !part.startPage || !part.endPage) return '';
206    if (part.startPage === part.endPage) return `${__isEN ? 'p.' : 'fl.'} ${part.startPage}`;
207    return `${__isEN ? 'pp.' : 'fls.'} ${part.startPage}-${part.endPage}`;
208}
209
210function formatGestaoLogField(value) {
211    return typeof formatStringArray === 'function' ? formatStringArray(value) : String(value || '');
212}
213
214function logGestaoFuncMsg(tipo, titulo, msg, id, value) {
215    fetch(base_url + '/funcMsg', {
216        method: 'post',
217        headers: { 'content-type': 'application/json' },
218        body: JSON.stringify({
219            codigoUnico,
220            timestamp,
221            funcionalidadeName,
222            tipo,
223            msg: formatGestaoLogField(msg),
224            id: formatGestaoLogField(id),
225            value: formatGestaoLogField(value),
226            // Success/info toasts (fila e peca pronta) nao precisam de email ao admin.
227            notifyAdmin: tipo === 'error' || tipo === 'warning'
228        })
229    }).catch(() => { });
230}
231
232function ensureGestaoToastStack() {
233    let stack = document.getElementById('gestaoToastStack');
234    if (!stack) {
235        stack = document.createElement('div');
236        stack.id = 'gestaoToastStack';
237        stack.className = 'gestao-toast-stack';
238        stack.setAttribute('aria-live', 'polite');
239        document.body.appendChild(stack);
240    } else if (stack.parentNode !== document.body) {
241        document.body.appendChild(stack);
242    }
243    return stack;
244}
245
246function showGestaoToast(titulo, msg, msOrOpts) {
247    const stack = ensureGestaoToastStack();
248
249    let ms = 6000;
250    let variant = '';
251    if (typeof msOrOpts === 'number') {
252        ms = msOrOpts;
253    } else if (msOrOpts && typeof msOrOpts === 'object') {
254        if (msOrOpts.ms != null) ms = msOrOpts.ms;
255        if (msOrOpts.variant) variant = msOrOpts.variant;
256    }
257
258    const el = document.createElement('div');
259    el.className = 'gestao-toast' + (variant ? ` gestao-toast--${variant}` : '');
260    el.innerHTML = `<div class="gestao-toast-title">${escapeGestaoHtml(titulo)}</div><div class="gestao-toast-body">${escapeGestaoHtml(msg)}</div>`;
261    stack.appendChild(el);
262    requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('show')));
263
264    setTimeout(() => {
265        el.classList.remove('show');
266        setTimeout(() => el.remove(), 320);
267    }, ms);
268}
269
270function gestaoSuccessNotify(titulo, msg, id, value) {
271    showGestaoToast(titulo, msg);
272    logGestaoFuncMsg('success', titulo, msg, id || '', value || '');
273}
274
275function gestaoDisplayFilename(filename) {
276    try {
277        return decodeURIComponent(String(filename || '').replace(/\+/g, ' '));
278    } catch (_) {
279        return filename || '';
280    }
281}
282
283const gestaoWatchedJobs = new Map();
284const gestaoNotifiedJobIds = new Set();
285const GESTAO_WATCHED_JOBS_STORAGE_KEY = 'cognijus_gestao_watched_jobs';
286
287function persistGestaoWatchedJobs() {
288    try {
289        const jobs = [];
290        gestaoWatchedJobs.forEach((value, id) => {
291            jobs.push({
292                id,
293                lastStatus: value.lastStatus != null ? value.lastStatus : null,
294                lastPecaCount: typeof value.lastPecaCount === 'number' ? value.lastPecaCount : -1
295            });
296        });
297        if (jobs.length) {
298            sessionStorage.setItem(GESTAO_WATCHED_JOBS_STORAGE_KEY, JSON.stringify({ jobs }));
299        } else {
300            sessionStorage.removeItem(GESTAO_WATCHED_JOBS_STORAGE_KEY);
301        }
302    } catch (_) { /* quota / private mode */ }
303}
304
305function rehydrateGestaoWatchedJobsFromSession() {
306    try {
307        const raw = sessionStorage.getItem(GESTAO_WATCHED_JOBS_STORAGE_KEY);
308        if (!raw) return;
309        const parsed = JSON.parse(raw);
310        if (!parsed || !Array.isArray(parsed.jobs)) return;
311        parsed.jobs.forEach((entry) => {
312            const key = String(entry.id || '').trim();
313            if (!key) return;
314            gestaoWatchedJobs.set(key, {
315                lastStatus: entry.lastStatus != null ? entry.lastStatus : null,
316                lastPecaCount: typeof entry.lastPecaCount === 'number' ? entry.lastPecaCount : -1
317            });
318        });
319    } catch (_) { /* ignore corrupt storage */ }
320}
321
322function gestaoProcessoId(p) {
323    if (!p || p._id == null) return '';
324    const id = p._id;
325    if (typeof id === 'object') {
326        return String(id.$oid || id.toString?.() || id);
327    }
328    return String(id);
329}
330
331function gestaoPecaCount(p) {
332    return Array.isArray(p.pecas) ? p.pecas.length : (p.hasPeca ? 1 : 0);
333}
334
335function gestaoTrackJob(id, options) {
336    const key = String(id || '').trim();
337    if (!key) return;
338    gestaoWatchedJobs.set(key, {
339        lastStatus: options && options.markPendenteSeen ? 'pendente' : null,
340        lastPecaCount: -1
341    });
342    gestaoNotifiedJobIds.delete(key);
343    persistGestaoWatchedJobs();
344}
345
346function gestaoUntrackJob(id) {
347    const key = String(id || '').trim();
348    if (!key || !gestaoWatchedJobs.has(key)) return;
349    gestaoWatchedJobs.delete(key);
350    persistGestaoWatchedJobs();
351}
352
353function gestaoStatusToastCopy(status, fname) {
354    const name = fname || (__isEN ? 'Your case' : 'Seu processo');
355    if (__isEN) {
356        switch (status) {
357            case 'pendente': return { title: 'Pending', msg: `${name} is queued.` };
358            case 'processando': return { title: 'Processing', msg: `Generating draft for ${name}...` };
359            case 'concluido': return { title: 'Draft ready', msg: `${name} is ready in Batch Analysis.` };
360            case 'erro': return { title: 'Error', msg: `${name} could not be processed.` };
361            default: return { title: 'Update', msg: name };
362        }
363    }
364    switch (status) {
365        case 'pendente': return { title: 'Pendente', msg: `${name} aguardando processamento.` };
366        case 'processando': return { title: 'Processando', msg: `Gerando peça para ${name}...` };
367        case 'concluido': return { title: 'Peça gerada', msg: `${name} disponível em Análise em Lote.` };
368        case 'erro': return { title: 'Erro', msg: `Não foi possível processar ${name}.` };
369        default: return { title: 'Atualização', msg: name };
370    }
371}
372
373function gestaoResolveJobErrorMessage(p) {
374    return String(p?.errorMessage || p?.lastError || '').trim();
375}
376
377function gestaoIsDocumentTooLargeError(p, msg) {
378    const reason = String(p?.lastErrorReason || '').trim();
379    if (reason === 'document_too_large') return true;
380    const text = String(msg || gestaoResolveJobErrorMessage(p)).toLowerCase();
381    return /muito extenso|token_limit|muitas páginas|grande demais|too large|too many pages/i.test(text);
382}
383
384function gestaoNotifyJobError(p, fname) {
385    const name = fname || gestaoDisplayFilename(p?.filename);
386    const detail = gestaoResolveJobErrorMessage(p);
387    const isDocLarge = gestaoIsDocumentTooLargeError(p, detail);
388    const fallback = gestaoStatusToastCopy('erro', name);
389
390    let title;
391    let body;
392    if (detail) {
393        title = isDocLarge
394            ? (__isEN ? 'Document too large' : 'Documento muito extenso')
395            : (__isEN ? 'Processing failed' : 'Falha no processamento');
396        body = detail;
397    } else {
398        title = fallback.title;
399        body = fallback.msg;
400    }
401
402    showGestaoToast(title, body, {
403        ms: detail ? 14000 : 8000,
404        variant: isDocLarge ? 'warning' : 'error'
405    });
406
407    if (isDocLarge && !isAssessorUserPro() && typeof openCogniJUSProModal === 'function') {
408        openCogniJUSProModal(detail || (__isEN
409            ? 'This document exceeds the free plan limit. Upgrade to PRO or specify the most relevant pages.'
410            : 'Este documento excede o limite do plano gratuito. Assine o PRO ou indique as páginas mais relevantes.'));
411    }
412
413    logGestaoFuncMsg(
414        isDocLarge ? 'warning' : 'error',
415        title,
416        body,
417        gestaoProcessoId(p) || '',
418        JSON.stringify({
419            processoId: gestaoProcessoId(p),
420            filename: p?.filename,
421            status: 'erro',
422            errorMessage: detail,
423            lastErrorReason: p?.lastErrorReason || ''
424        })
425    );
426}
427
428function gestaoProcessWatchedJob(p) {
429    const id = gestaoProcessoId(p);
430    if (!id || !gestaoWatchedJobs.has(id)) return;
431
432    const watched = gestaoWatchedJobs.get(id);
433    const status = p.status || 'pendente';
434    const pecaCount = gestaoPecaCount(p);
435    const fname = gestaoDisplayFilename(p.filename);
436
437    if (watched.lastStatus !== status) {
438        const copy = gestaoStatusToastCopy(status, fname);
439        if (status === 'concluido' && pecaCount > 0) {
440            if (!gestaoNotifiedJobIds.has(id)) {
441                gestaoNotifiedJobIds.add(id);
442                gestaoSuccessNotify(copy.title, copy.msg, id, JSON.stringify({ processoId: id, filename: p.filename, pecaCount, status }));
443            }
444            gestaoUntrackJob(id);
445        } else if (status === 'erro') {
446            gestaoNotifyJobError(p, fname);
447            gestaoUntrackJob(id);
448        } else {
449            showGestaoToast(copy.title, copy.msg);
450        }
451        watched.lastStatus = status;
452        persistGestaoWatchedJobs();
453    } else if (status === 'concluido' && pecaCount > watched.lastPecaCount && pecaCount > 0 && !gestaoNotifiedJobIds.has(id)) {
454        gestaoNotifiedJobIds.add(id);
455        const copy = gestaoStatusToastCopy('concluido', fname);
456        gestaoSuccessNotify(copy.title, copy.msg, id, JSON.stringify({ processoId: id, filename: p.filename, pecaCount, status }));
457        gestaoUntrackJob(id);
458    }
459
460    watched.lastPecaCount = pecaCount;
461    if (gestaoWatchedJobs.has(id)) persistGestaoWatchedJobs();
462}
463
464function gestaoNotifyCompleted(p) {
465    const id = gestaoProcessoId(p);
466    if (!id || gestaoNotifiedJobIds.has(id)) return;
467    const pecaCount = gestaoPecaCount(p);
468    if (p.status !== 'concluido' || pecaCount === 0) return;
469
470    gestaoNotifiedJobIds.add(id);
471    gestaoUntrackJob(id);
472
473    const fname = gestaoDisplayFilename(p.filename);
474    const copy = gestaoStatusToastCopy('concluido', fname);
475    gestaoSuccessNotify(
476        copy.title,
477        copy.msg,
478        id,
479        JSON.stringify({ processoId: id, filename: p.filename, pecaCount })
480    );
481    if (typeof window.cjErpRefreshAfterAssessor === 'function') window.cjErpRefreshAfterAssessor();
482}
483
484var selectedFiles = [] // Array de arquivos PDF selecionados (suporta múltiplos)
485var formData = new FormData()
486let isRequestInProgress = false // Flag to prevent multiple simultaneous requests
487
488// Check if user needs to login before uploading
489function checkAuthenticationBeforeAction() {
490    if (!cognijusUserToken) {
491        openCogniJUSModal();
492        return false;
493    }
494    return true;
495}
496
497// Function to toggle the intro dropdown
498function toggleIntroDropdown() {
499    const dropdownContent = document.getElementById('dropdownContent')
500    const dropdownArrow = document.getElementById('dropdownArrow')
501
502    if (dropdownContent.classList.contains('expanded')) {
503        dropdownContent.classList.remove('expanded')
504        dropdownArrow.classList.remove('rotated')
505    } else {
506        dropdownContent.classList.add('expanded')
507        dropdownArrow.classList.add('rotated')
508    }
509}
510
511// Function to validate page input format
512function validatePageInput(input, showError = true) {
513    const value = input.value.trim();
514    const errorDiv = document.getElementById('pageInputError');
515
516    // If empty, it's valid (optional field)
517    if (value === '') {
518        if (showError) {
519            errorDiv.style.display = 'none';
520            input.classList.remove('input-error');
521        }
522        return true;
523    }
524
525    // Regular expression to validate the format: numbers, commas, and hyphens only
526    const validFormat = /^[\d\s,\-]+$/.test(value);
527
528    if (!validFormat) {
529        if (showError) {
530            errorDiv.style.display = 'block';
531            input.classList.add('input-error');
532        }
533        return false;
534    }
535
536    // Additional validation: check if the structure makes sense
537    const parts = value.split(',').map(p => p.trim());
538    let isValid = true;
539
540    for (const part of parts) {
541        if (part.includes('-')) {
542            const rangeParts = part.split('-');
543            if (rangeParts.length !== 2) {
544                isValid = false;
545                break;
546            }
547            const start = parseInt(rangeParts[0].trim());
548            const end = parseInt(rangeParts[1].trim());
549            if (isNaN(start) || isNaN(end) || start > end || start <= 0) {
550                isValid = false;
551                break;
552            }
553        } else {
554            const page = parseInt(part);
555            if (isNaN(page) || page <= 0) {
556                isValid = false;
557                break;
558            }
559        }
560    }
561
562    if (!isValid) {
563        if (showError) {
564            errorDiv.style.display = 'block';
565            input.classList.add('input-error');
566        }
567        return false;
568    }
569
570    // If we get here, the format is valid
571    if (showError) {
572        errorDiv.style.display = 'none';
573        input.classList.remove('input-error');
574    }
575    return true;
576}
577
578// Function to parse page ranges (e.g., "1, 3, 5-7")
579function parsePageRanges(pageString) {
580    if (!pageString || pageString.trim() === '') return [];
581
582    const pages = [];
583    const parts = pageString.split(',').map(p => p.trim());
584
585    for (const part of parts) {
586        if (part.includes('-')) {
587            const [start, end] = part.split('-').map(p => parseInt(p.trim()));
588            if (!isNaN(start) && !isNaN(end) && start <= end) {
589                for (let i = start; i <= end; i++) {
590                    pages.push(i);
591                }
592            }
593        } else {
594            const page = parseInt(part);
595            if (!isNaN(page)) {
596                pages.push(page);
597            }
598        }
599    }
600
601    return [...new Set(pages)].sort((a, b) => a - b);
602}
603
604// Legacy individual analysis upload flow was removed. Gestão Processual uses the batch upload functions below.
605
606// ============================================================================
607// PROMPT BANK (additional instructions history)
608// ============================================================================
609
610let assessorPromptHistory = [];
611let promptHistoryFetchSeq = 0;
612let promptHistoryAuthState = null;
613let promptHistoryLoading = false;
614let promptHistoryLoadedOnce = false;
615let promptHistoryExpanded = false;
616const PROMPT_HISTORY_INITIAL_LIMIT = 3;
617
618function getErpActiveOrgId() {
619    try {
620        return typeof window.cjErpGetActiveOrgId === 'function' ? window.cjErpGetActiveOrgId() : null;
621    } catch (_) {
622        return null;
623    }
624}
625
626function getPromptHistoryCacheKey() {
627    const owner = (cognijusUserData && cognijusUserData.email) || cognijusUserToken || 'anon';
628    const orgId = getErpActiveOrgId();
629    return orgId ? ('assessorPromptBank:' + owner + ':erp:' + orgId) : ('assessorPromptBank:' + owner);
630}
631
632function mapErpPromptsToAssessorHistory(items) {
633    return (items || []).map(item => ({
634        _id: item.id || item._id,
635        prompt: item.content || '',
636        title: item.title || '',
637        timestamp: (item.metadata && item.metadata.assessorUsedAt) || item.updatedAt || item.createdAt || new Date().toISOString()
638    }));
639}
640
641window.cjAssessorResetPromptHistory = function () {
642    promptHistoryLoadedOnce = false;
643    assessorPromptHistory = [];
644    try { sessionStorage.removeItem(getPromptHistoryCacheKey()); } catch (_) { /* noop */ }
645};
646
647window.cjAssessorRefreshPromptBank = function () {
648    window.cjAssessorResetPromptHistory();
649    const modal = document.getElementById('gestaoInstructionModal');
650    if (modal && !modal.hidden && typeof loadGestaoInstructionBank === 'function') {
651        loadGestaoInstructionBank();
652    }
653};
654
655function readPromptHistoryCache() {
656    try {
657        const raw = sessionStorage.getItem(getPromptHistoryCacheKey());
658        if (!raw) return null;
659        const parsed = JSON.parse(raw);
660        return Array.isArray(parsed?.prompts) ? parsed.prompts : null;
661    } catch (_) {
662        return null;
663    }
664}
665
666function writePromptHistoryCache(prompts) {
667    try {
668        sessionStorage.setItem(getPromptHistoryCacheKey(), JSON.stringify({
669            prompts: prompts || [],
670            ts: Date.now()
671        }));
672    } catch (_) { /* quota / private mode */ }
673}
674
675function hidePromptHistorySection() {
676    const section = document.getElementById('promptHistorySection');
677    const content = document.getElementById('promptHistoryContent');
678    const showMore = document.getElementById('promptHistoryShowMore');
679    if (section) {
680        section.style.display = 'none';
681        section.classList.remove('is-loading');
682        section.setAttribute('aria-busy', 'false');
683        delete section.dataset.promptShellReady;
684    }
685    if (content) content.hidden = true;
686    if (showMore) {
687        showMore.innerHTML = '';
688        showMore.hidden = true;
689    }
690    promptHistoryExpanded = false;
691    syncPromptHistoryToggleUi();
692}
693
694function syncPromptHistoryToggleUi() {
695    const content = document.getElementById('promptHistoryContent');
696    const showMore = document.getElementById('promptHistoryShowMore');
697    const toggleIcon = document.getElementById('promptHistoryToggleIcon');
698    const toggleBtn = document.querySelector('.assessor-prompt-history-toggle');
699    const expanded = !!(content && !content.hidden);
700    promptHistoryExpanded = expanded;
701    if (toggleIcon) toggleIcon.className = expanded ? 'fa fa-chevron-up' : 'fa fa-chevron-down';
702    if (toggleBtn) toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
703    if (showMore) showMore.hidden = !(expanded && showMore.innerHTML);
704}
705
706function updatePromptHistoryHeaderCount(count, loading) {
707    const label = document.getElementById('promptHistoryLabel');
708    const header = document.getElementById('promptHistoryHeader');
709    const section = document.getElementById('promptHistorySection');
710    if (!label) return;
711
712    const base = __isEN ? 'Prompt bank' : 'Banco de prompts';
713    let suffix = '';
714    if (loading) {
715        suffix = `<span class="assessor-prompt-history-status">${__isEN ? 'Loading…' : 'Carregando…'}</span>`;
716    } else if (promptHistoryLoadedOnce && typeof count === 'number') {
717        suffix = `<span class="assessor-prompt-history-count">(${count})</span>`;
718    }
719    label.innerHTML = `<i class="fa fa-history" aria-hidden="true"></i> ${base}${suffix ? ' ' + suffix : ''}`;
720
721    if (header) header.classList.toggle('is-loading', !!loading);
722    if (section) {
723        section.classList.toggle('is-loading', !!loading);
724        section.setAttribute('aria-busy', loading ? 'true' : 'false');
725    }
726}
727
728function showPromptHistoryShell(options) {
729    const opts = options || {};
730    const section = document.getElementById('promptHistorySection');
731    if (!section || !isAssessorUserProForPromptHistory()) return;
732
733    section.style.display = '';
734    updatePromptHistoryHeaderCount(
735        typeof opts.count === 'number' ? opts.count : assessorPromptHistory.length,
736        !!opts.loading
737    );
738
739    if (!opts.preserveExpansion && !section.dataset.promptShellReady) {
740        const content = document.getElementById('promptHistoryContent');
741        const showMore = document.getElementById('promptHistoryShowMore');
742        if (content) content.hidden = true;
743        if (showMore) {
744            showMore.innerHTML = '';
745            showMore.hidden = true;
746        }
747        promptHistoryExpanded = false;
748        syncPromptHistoryToggleUi();
749        section.dataset.promptShellReady = '1';
750    }
751}
752
753function isAssessorUserProForPromptHistory() {
754    return !!(cognijusUserData && cognijusUserData.isPro);
755}
756
757function isPromptHistoryAuthReady() {
758    return !!(cognijusUserToken && cognijusUserData && !window.__cognijusAuthInProgress);
759}
760
761function escapePromptHistoryHtml(text) {
762    const div = document.createElement('div');
763    div.textContent = text;
764    return div.innerHTML;
765}
766
767async function fetchPromptHistoryFromDB() {
768    if (!isPromptHistoryAuthReady() || !isAssessorUserProForPromptHistory()) return [];
769
770    const orgId = getErpActiveOrgId();
771    if (orgId && typeof window.cjErpFetchOrgPrompts === 'function') {
772        try {
773            const items = await window.cjErpFetchOrgPrompts();
774            return mapErpPromptsToAssessorHistory(items);
775        } catch (e) {
776            console.error('Error fetching ERP org prompts:', e);
777            return [];
778        }
779    }
780
781    try {
782        const response = await fetch('/assessor_prompt_history', {
783            method: 'POST',
784            headers: {
785                'Content-Type': 'application/json',
786                'Authorization': `Bearer ${cognijusUserToken}`
787            },
788            body: JSON.stringify({
789                funcionalidadeName,
790                codigoUnico: typeof codigoUnico !== 'undefined' ? codigoUnico : undefined
791            })
792        });
793
794        if (response.ok) {
795            const data = await response.json();
796            return data.prompts || [];
797        }
798        return [];
799    } catch (e) {
800        console.error('Error fetching assessor prompt history:', e);
801        return [];
802    }
803}
804
805function formatPromptHistoryShowMoreLabel(remaining) {
806    if (__isEN) {
807        return `Show ${remaining} more prompt${remaining === 1 ? '' : 's'}`;
808    }
809    return `Ver mais ${remaining} prompt${remaining === 1 ? '' : 's'}`;
810}
811
812function buildPromptHistoryItemHTML(item, index) {
813    const date = new Date(item.timestamp);
814    const locale = __isEN ? 'en-US' : 'pt-BR';
815    const formattedDate = date.toLocaleDateString(locale) + ' ' + date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
816    const promptText = item.prompt || '';
817    const titleText = String(item.title || '').trim();
818    const preview = titleText
819        ? escapePromptHistoryHtml(titleText)
820        : escapePromptHistoryHtml(promptText.substring(0, 200)) + (promptText.length > 200 ? '…' : '');
821    const reuseTitle = __isEN ? 'Reuse this prompt' : 'Reutilizar este prompt';
822    return `
823            <div class="assessor-prompt-history-item" data-index="${index}">
824                <div class="assessor-prompt-history-item-body">
825                    <span class="assessor-prompt-history-item-date"><i class="fa fa-clock-o"></i> ${formattedDate}</span>
826                    <div class="assessor-prompt-history-item-text">${preview}</div>
827                </div>
828                <div class="assessor-prompt-history-item-actions">
829                    <button class="assessor-prompt-history-item-btn reuse" onclick="reusePromptHistoryItem(${index})" title="${reuseTitle}" type="button">
830                        <i class="fa fa-refresh"></i>
831                    </button>
832                </div>
833            </div>
834        `;
835}
836
837function upsertLocalPromptHistory(promptText, promptTitle) {
838    const prompt = String(promptText || '').trim();
839    if (!prompt) return;
840    const title = String(promptTitle || '').trim();
841    const existingIdx = assessorPromptHistory.findIndex(item => item.prompt === prompt);
842    if (existingIdx >= 0) assessorPromptHistory.splice(existingIdx, 1);
843    assessorPromptHistory.unshift({
844        _id: 'local-' + Date.now(),
845        prompt,
846        title,
847        timestamp: new Date().toISOString()
848    });
849}
850
851function renderPromptHistory(history, showAll) {
852    const section = document.getElementById('promptHistorySection');
853    const content = document.getElementById('promptHistoryContent');
854    const showMore = document.getElementById('promptHistoryShowMore');
855
856    if (!section || !content) return;
857
858    if (!isAssessorUserProForPromptHistory()) {
859        hidePromptHistorySection();
860        return;
861    }
862
863    showPromptHistoryShell({ count: (history || []).length, loading: false, preserveExpansion: true });
864    const wasExpanded = promptHistoryExpanded;
865
866    if (!history || history.length === 0) {
867        content.innerHTML = `<div class="assessor-prompt-history-empty">${__isEN
868            ? 'No saved prompts yet. Submit an analysis with additional instructions to build your bank.'
869            : 'Nenhum prompt salvo ainda. Envie uma análise com instruções adicionais para montar seu banco.'}</div>`;
870    } else {
871        const displayHistory = showAll ? history : history.slice(0, PROMPT_HISTORY_INITIAL_LIMIT);
872        content.innerHTML = displayHistory.map((item, index) => buildPromptHistoryItemHTML(item, index)).join('');
873
874        if (showMore) {
875            if (!showAll && history.length > PROMPT_HISTORY_INITIAL_LIMIT) {
876                const remaining = history.length - PROMPT_HISTORY_INITIAL_LIMIT;
877                showMore.innerHTML = `
878                        <div class="assessor-prompt-history-show-more">
879                            <button class="assessor-prompt-history-show-more-btn" onclick="renderPromptHistory(assessorPromptHistory, true)" type="button">
880                                <i class="fa fa-chevron-down"></i> ${formatPromptHistoryShowMoreLabel(remaining)}
881                            </button>
882                        </div>
883                    `;
884            } else {
885                showMore.innerHTML = '';
886            }
887        }
888    }
889
890    content.hidden = !wasExpanded;
891    if (showMore) showMore.hidden = !(wasExpanded && showMore.innerHTML);
892    syncPromptHistoryToggleUi();
893}
894
895function togglePromptHistory() {
896    const content = document.getElementById('promptHistoryContent');
897    const showMore = document.getElementById('promptHistoryShowMore');
898    if (!content) return;
899
900    const willExpand = content.hidden;
901    content.hidden = !willExpand;
902    if (showMore) showMore.hidden = !(willExpand && showMore.innerHTML);
903    syncPromptHistoryToggleUi();
904}
905
906function reusePromptHistoryItem(index) {
907    const item = assessorPromptHistory[index];
908    const textarea = document.getElementById('additionalPrompt');
909    if (!item || !textarea) return;
910
911    textarea.value = item.prompt || '';
912    textarea.scrollIntoView({ behavior: 'smooth', block: 'center' });
913    setTimeout(() => textarea.focus(), 400);
914}
915
916async function loadAndRenderPromptHistory(forceReload) {
917    if (!isPromptHistoryAuthReady()) return;
918    if (!isAssessorUserProForPromptHistory()) {
919        assessorPromptHistory = [];
920        hidePromptHistorySection();
921        return;
922    }
923    if (promptHistoryLoading && !forceReload) return;
924
925    const cached = readPromptHistoryCache();
926    if (cached && !promptHistoryLoadedOnce && !getErpActiveOrgId()) {
927        assessorPromptHistory = cached;
928        promptHistoryLoadedOnce = true;
929        showPromptHistoryShell({ count: cached.length, loading: true });
930        renderPromptHistory(assessorPromptHistory);
931    } else {
932        showPromptHistoryShell({ count: assessorPromptHistory.length, loading: true });
933    }
934
935    const seq = ++promptHistoryFetchSeq;
936    promptHistoryLoading = true;
937
938    try {
939        const prompts = await fetchPromptHistoryFromDB();
940        if (seq !== promptHistoryFetchSeq) return;
941
942        assessorPromptHistory = prompts;
943        promptHistoryLoadedOnce = true;
944        writePromptHistoryCache(prompts);
945        renderPromptHistory(assessorPromptHistory);
946    } finally {
947        if (seq === promptHistoryFetchSeq) {
948            promptHistoryLoading = false;
949            updatePromptHistoryHeaderCount(assessorPromptHistory.length, false);
950        }
951    }
952}
953
954function syncPromptHistoryWithAuth() {
955    if (!cognijusUserToken) {
956        promptHistoryAuthState = 'logged-out';
957        assessorPromptHistory = [];
958        hidePromptHistorySection();
959        return;
960    }
961    if (!isPromptHistoryAuthReady()) return;
962
963    const nextState = isAssessorUserProForPromptHistory() ? 'pro' : 'free';
964    if (nextState === promptHistoryAuthState && nextState !== 'pro') return;
965
966    if (nextState === 'pro') {
967        showPromptHistoryShell({
968            count: assessorPromptHistory.length,
969            loading: !promptHistoryLoadedOnce
970        });
971        if (promptHistoryAuthState !== 'pro' || !promptHistoryLoadedOnce) {
972            loadAndRenderPromptHistory(true);
973        } else {
974            renderPromptHistory(assessorPromptHistory);
975        }
976    } else {
977        promptHistoryLoadedOnce = false;
978        assessorPromptHistory = [];
979        hidePromptHistorySection();
980    }
981
982    promptHistoryAuthState = nextState;
983}
984
985// Legacy individual-only prompt/anonymization restrictions were removed.
986
987// ============================================================================
988// TABS NAVIGATION - Assessor Unificado
989// ============================================================================
990
991// Initialize tabs navigation
992function initializeAssessorTabs() {
993    const tabItems = document.querySelectorAll('.assessor-tab-item');
994    const tabPanes = document.querySelectorAll('.assessor-tab-pane');
995
996    tabItems.forEach(item => {
997        item.addEventListener('click', function () {
998            tabItems.forEach(t => t.classList.remove('active'));
999            tabPanes.forEach(p => p.classList.remove('active'));
1000            this.classList.add('active');
1001            const tabId = this.getAttribute('data-tab');
1002            const targetPane = document.getElementById(tabId);
1003            if (targetPane) targetPane.classList.add('active');
1004
1005            if (!window.__cjErpModuleSyncLock && typeof window.cjErpActivateModule === 'function') {
1006                window.cjErpActivateModule(tabId);
1007            }
1008
1009            // Scroll to top of tabs so the content is visible
1010            const tabsNav = document.querySelector('.assessor-tabs-navigation');
1011            if (tabsNav) {
1012                setTimeout(function () {
1013                    tabsNav.scrollIntoView({ behavior: 'smooth', block: 'start' });
1014                }, 50);
1015            }
1016
1017            // Carrega dados quando necessário
1018            if (tabId === 'meus-processos') {
1019                loadProcessosGestao();
1020            }
1021        });
1022    });
1023
1024    // Check URL param for tab selection
1025    const urlParams = new URLSearchParams(window.location.search);
1026    const tabParam = urlParams.get('tab');
1027    if (tabParam) {
1028        const targetTab = document.querySelector(`.assessor-tab-item[data-tab="${tabParam}"]`);
1029        if (targetTab) {
1030            targetTab.click();
1031        }
1032    }
1033}
1034
1035// ============================================================================
1036// GESTÃO PROCESSUAL - Funcionalidades Integradas
1037// ============================================================================
1038
1039const base_url_gestao = window.location.origin + "/assessor";
1040let processosDataGestao = [];
1041let selectedUploadFilesGestao = [];
1042let autoRefreshInterval = null;
1043let gestaoSortState = { field: 'uploadDate', direction: 'desc' };
1044let gestaoNeedsFullRenderAfterInteraction = false;
1045const gestaoRowSelections = new Map();
1046const gestaoRowParamOverrides = new Map();
1047
1048// ── Modelos (aba Modelos do ERP) usados na coluna Modelo da tabela ──
1049// Só existem no contexto ERP (org-scoped). Fora do ERP o botão fica desabilitado.
1050let gestaoModelsCache = [];
1051let gestaoModelsLoadedOrgId = null;
1052let gestaoModelsLoadingPromise = null;
1053
1054function gestaoModelsCanEdit() {
1055    return !!getErpActiveOrgId() && typeof window.cjErpFetchOrgModels === 'function';
1056}
1057
1058async function loadGestaoModels(force) {
1059    const orgId = getErpActiveOrgId();
1060    if (!orgId || typeof window.cjErpFetchOrgModels !== 'function') {
1061        gestaoModelsCache = [];
1062        gestaoModelsLoadedOrgId = null;
1063        return gestaoModelsCache;
1064    }
1065    if (!force && gestaoModelsLoadedOrgId === orgId) return gestaoModelsCache;
1066    if (gestaoModelsLoadingPromise && !force) return gestaoModelsLoadingPromise;
1067    gestaoModelsLoadingPromise = (async () => {
1068        try {
1069            const items = await window.cjErpFetchOrgModels();
1070            gestaoModelsCache = Array.isArray(items) ? items : [];
1071            gestaoModelsLoadedOrgId = orgId;
1072        } catch (_) {
1073            gestaoModelsCache = [];
1074        } finally {
1075            gestaoModelsLoadingPromise = null;
1076        }
1077        return gestaoModelsCache;
1078    })();
1079    return gestaoModelsLoadingPromise;
1080}
1081
1082function upsertGestaoModelCache(item) {
1083    if (!item) return;
1084    const id = String(item.id || item._id || '');
1085    if (!id) return;
1086    const entry = {
1087        id,
1088        title: item.title || '',
1089        modelType: item.modelType || 'general',
1090        sourceFileName: item.sourceFileName || ''
1091    };
1092    const idx = gestaoModelsCache.findIndex(m => String(m.id) === id);
1093    if (idx >= 0) gestaoModelsCache[idx] = Object.assign({}, gestaoModelsCache[idx], entry);
1094    else gestaoModelsCache.unshift(entry);
1095}
1096
1097function getGestaoModelTitle(modelId, fallbackTitle) {
1098    if (!modelId) return '';
1099    const found = gestaoModelsCache.find(m => String(m.id) === String(modelId));
1100    if (found && found.title) return found.title;
1101    return String(fallbackTitle || '').trim();
1102}
1103
1104function getGestaoModelLabel(modelId, modelTitle) {
1105    const title = getGestaoModelTitle(modelId, modelTitle);
1106    if (title) return title.length > 56 ? `${title.slice(0, 56)}...` : title;
1107    if (modelId) return __isEN ? 'Selected template' : 'Modelo selecionado';
1108    return __isEN ? 'Add template' : 'Adicionar modelo';
1109}
1110
1111function updateGestaoModelCell(row) {
1112    if (!row) return;
1113    const idInput = row.querySelector('.row-model-id');
1114    const titleInput = row.querySelector('.row-model-title');
1115    const label = row.querySelector('.row-model-label');
1116    const button = row.querySelector('.row-model-btn');
1117    const modelId = idInput ? String(idInput.value || '').trim() : '';
1118    const modelTitle = titleInput ? String(titleInput.value || '').trim() : '';
1119    const resolvedTitle = getGestaoModelTitle(modelId, modelTitle);
1120    if (titleInput && resolvedTitle && titleInput.value !== resolvedTitle) {
1121        titleInput.value = resolvedTitle;
1122    }
1123    if (label) {
1124        label.textContent = getGestaoModelLabel(modelId, resolvedTitle);
1125        label.title = resolvedTitle;
1126    }
1127    if (button) {
1128        const canEdit = gestaoModelsCanEdit();
1129        button.classList.toggle('has-model', !!modelId);
1130        button.title = canEdit
1131            ? (resolvedTitle || (__isEN ? 'Add or choose a template' : 'Adicionar ou selecionar um modelo'))
1132            : (__isEN
1133                ? 'Templates are available in Gestão Processual after you select a persona.'
1134                : 'Modelos ficam disponíveis na Gestão Processual após selecionar uma persona.');
1135        const icon = button.querySelector('i');
1136        if (icon) icon.className = modelId ? 'fa fa-pencil' : 'fa fa-plus';
1137    }
1138}
1139
1140function syncGestaoModelCellsFromCache() {
1141    const knownIds = new Set((gestaoModelsCache || []).map(m => String(m.id)));
1142    gestaoQueryAll('.row-model-id').forEach(input => {
1143        const row = input.closest('tr');
1144        if (!row) return;
1145        const modelId = String(input.value || '').trim();
1146        if (modelId && knownIds.size && !knownIds.has(modelId)) {
1147            input.value = '';
1148            const titleInput = row.querySelector('.row-model-title');
1149            if (titleInput) titleInput.value = '';
1150            syncGestaoRowParamsFromDom(row);
1151        }
1152        updateGestaoModelCell(row);
1153    });
1154}
1155
1156function populateRowModelSelectsGestao() {
1157    syncGestaoModelCellsFromCache();
1158}
1159
1160async function refreshGestaoModelsFromOrg() {
1161    await loadGestaoModels(true);
1162    syncGestaoModelCellsFromCache();
1163    const modal = document.getElementById('gestaoModelModal');
1164    if (modal && !modal.hidden) renderGestaoModelBank(gestaoModelsCache);
1165}
1166
1167window.cjAssessorRefreshGestaoModels = function () {
1168    return refreshGestaoModelsFromOrg().catch(() => { });
1169};
1170
1171function getErpAssessorRoot() {
1172    const host = document.querySelector('[data-cj-erp-host]');
1173    return host && host.shadowRoot ? host.shadowRoot : null;
1174}
1175
1176function gestaoGetById(id) {
1177    return document.getElementById(id) || getErpAssessorRoot()?.getElementById(id) || null;
1178}
1179
1180function gestaoQuery(selector) {
1181    return document.querySelector(selector) || getErpAssessorRoot()?.querySelector(selector) || null;
1182}
1183
1184function gestaoQueryAll(selector) {
1185    const root = getErpAssessorRoot();
1186    const nodes = [
1187        ...Array.from(document.querySelectorAll(selector)),
1188        ...(root ? Array.from(root.querySelectorAll(selector)) : [])
1189    ];
1190    return Array.from(new Set(nodes));
1191}
1192
1193function createClientOperationIdGestao(prefix) {
1194    const suffix = (window.crypto && typeof window.crypto.randomUUID === 'function')
1195        ? window.crypto.randomUUID()
1196        : `${Date.now()}_${Math.random().toString(36).slice(2)}`;
1197    return `${prefix || 'assessor'}_${suffix}`;
1198}
1199
1200function getGestaoRowId(row) {
1201    const checkbox = row ? row.querySelector('.processo-checkbox') : null;
1202    return checkbox ? String(checkbox.value || '') : '';
1203}
1204
1205function getGestaoRowSelection(id) {
1206    return gestaoRowSelections.get(String(id || '')) || { selected: false, peca: '' };
1207}
1208
1209function setGestaoRowSelection(id, patch) {
1210    const key = String(id || '');
1211    if (!key) return;
1212    const previous = getGestaoRowSelection(key);
1213    const next = Object.assign({}, previous, patch || {});
1214    next.selected = !!(next.selected && next.peca);
1215    if (!next.selected && !next.peca) {
1216        gestaoRowSelections.delete(key);
1217        return;
1218    }
1219    gestaoRowSelections.set(key, next);
1220}
1221
1222function syncGestaoRowSelectionFromDom(row) {
1223    const id = getGestaoRowId(row);
1224    if (!id) return;
1225    const checkbox = row.querySelector('.processo-checkbox');
1226    const pecaSelect = row.querySelector('.row-peca-select');
1227    setGestaoRowSelection(id, {
1228        selected: !!(checkbox && checkbox.checked),
1229        peca: pecaSelect ? pecaSelect.value : ''
1230    });
1231}
1232
1233function syncAllGestaoRowSelectionsFromDom() {
1234    const tbody = gestaoGetById('tabelaProcessosBody');
1235    if (!tbody) return;
1236    tbody.querySelectorAll('tr').forEach(syncGestaoRowSelectionFromDom);
1237}
1238
1239function syncGestaoRowParamsFromDom(row) {
1240    const id = getGestaoRowId(row);
1241    if (!id) return;
1242    const promptInput = row.querySelector('.row-additional-prompt');
1243    const promptTitleInput = row.querySelector('.row-additional-prompt-title');
1244    const pagesInput = row.querySelector('.row-include-pages');
1245    const anonCheckbox = row.querySelector('.row-anonymization');
1246    const modelIdInput = row.querySelector('.row-model-id');
1247    const modelTitleInput = row.querySelector('.row-model-title');
1248    const modelId = modelIdInput ? String(modelIdInput.value || '').trim() : '';
1249    const modelTitle = modelTitleInput ? String(modelTitleInput.value || '').trim() : '';
1250    gestaoRowParamOverrides.set(id, {
1251        additionalPrompt: promptInput ? promptInput.value.trim() : '',
1252        additionalPromptTitle: promptTitleInput ? promptTitleInput.value.trim() : '',
1253        includePages: pagesInput ? pagesInput.value.trim() : '',
1254        enableAnonymization: anonCheckbox ? !!anonCheckbox.checked : false,
1255        modelId: modelId,
1256        modelTitle: modelId ? getGestaoModelTitle(modelId, modelTitle) : ''
1257    });
1258}
1259
1260function syncAllGestaoRowParamsFromDom() {
1261    const tbody = gestaoGetById('tabelaProcessosBody');
1262    if (!tbody) return;
1263    tbody.querySelectorAll('tr').forEach(syncGestaoRowParamsFromDom);
1264}
1265
1266let gestaoInstructionModalRowId = null;
1267let gestaoInstructionModalSelectedIndex = null;
1268let gestaoInstructionModalEscapeHandler = null;
1269
1270function getGestaoPromptLabel(promptText, promptTitle) {
1271    const title = String(promptTitle || '').trim();
1272    if (title) return title.length > 56 ? `${title.slice(0, 56)}...` : title;
1273    const prompt = String(promptText || '').trim();
1274    if (!prompt) return __isEN ? 'Add instruction' : 'Adicionar instrução';
1275    return prompt.length > 56 ? `${prompt.slice(0, 56)}...` : prompt;
1276}
1277
1278function getGestaoPromptTitleFallback(promptText) {
1279    const prompt = String(promptText || '').trim();
1280    if (!prompt) return '';
1281    const firstLine = prompt.split(/\n/)[0].trim();
1282    const base = firstLine || prompt.replace(/\s+/g, ' ').trim();
1283    return base.length > 80 ? `${base.slice(0, 77)}...` : base;
1284}
1285
1286function updateGestaoPromptCell(row) {
1287    if (!row) return;
1288    const input = row.querySelector('.row-additional-prompt');
1289    const titleInput = row.querySelector('.row-additional-prompt-title');
1290    const label = row.querySelector('.row-additional-prompt-label');
1291    const button = row.querySelector('.row-additional-prompt-btn');
1292    const prompt = input ? String(input.value || '').trim() : '';
1293    const promptTitle = titleInput ? String(titleInput.value || '').trim() : '';
1294    if (label) label.textContent = getGestaoPromptLabel(prompt, promptTitle);
1295    if (label) label.title = promptTitle || prompt;
1296    if (button) {
1297        button.classList.toggle('has-prompt', !!prompt);
1298        button.title = promptTitle || prompt || (__isEN ? 'Add or choose additional instructions' : 'Adicionar ou selecionar instruções adicionais');
1299        const icon = button.querySelector('i');
1300        if (icon) icon.className = prompt ? 'fa fa-pencil' : 'fa fa-plus';
1301    }
1302}
1303
1304function gestaoUploadSuccessMessage(count, uploadMode) {
1305    if (uploadMode === 'merged') {
1306        return __isEN ? '1 unified case uploaded successfully.' : '1 processo unificado inserido com sucesso.';
1307    }
1308    if (__isEN) {
1309        return count === 1 ? '1 file uploaded successfully.' : `${count} files uploaded successfully.`;
1310    }
1311    return count === 1 ? '1 arquivo inserido com sucesso.' : `${count} arquivos inseridos com sucesso.`;
1312}
1313
1314function gestaoUploadErrorMessage(count) {
1315    if (__isEN) {
1316        return count === 1 ? '1 file failed to upload.' : `${count} files failed to upload.`;
1317    }
1318    return count === 1 ? '1 arquivo com erro.' : `${count} arquivos com erro.`;
1319}
1320
1321function gestaoDeepActiveElement() {
1322    let active = document.activeElement;
1323    while (active && active.shadowRoot && active.shadowRoot.activeElement) {
1324        active = active.shadowRoot.activeElement;
1325    }
1326    return active;
1327}
1328
1329function gestaoIsTableInteractionActive() {
1330    const modal = document.getElementById('gestaoInstructionModal');
1331    if (modal && !modal.hidden) return true;
1332    const modelModal = document.getElementById('gestaoModelModal');
1333    if (modelModal && !modelModal.hidden) return true;
1334    const active = gestaoDeepActiveElement();
1335    const tbody = gestaoGetById('tabelaProcessosBody');
1336    if (!active || !tbody || !tbody.contains(active)) return false;
1337    return typeof active.matches === 'function'
1338        && active.matches('.row-peca-select, .row-model-btn, .row-include-pages, .row-additional-prompt, .row-anonymization');
1339}
1340
1341function gestaoSearchNeedle(rawTerm) {
1342    return normalizeGestaoFilename(String(rawTerm == null ? '' : rawTerm).trim()).toLowerCase();
1343}
1344
1345function gestaoTextMatchesSearch(text, needle) {
1346    if (!needle) return false;
1347    return normalizeGestaoFilename(text || '').toLowerCase().includes(needle);
1348}
1349
1350function gestaoMergedPartMatchesSearch(p, needle) {
1351    if (!needle) return false;
1352    return gestaoResolveMergedParts(p).some(part =>
1353        gestaoTextMatchesSearch(part.name, needle) || gestaoTextMatchesSearch(part.originalName, needle)
1354    );
1355}
1356
1357function gestaoProcessoMatchesSearch(p, needle) {
1358    if (!needle) return true;
1359    if (gestaoTextMatchesSearch(p && p.filename, needle)) return true;
1360    const origins = Array.isArray(p && p.convertedFromWord) ? p.convertedFromWord : [];
1361    if (origins.some(name => gestaoTextMatchesSearch(name, needle))) return true;
1362    return gestaoMergedPartMatchesSearch(p, needle);
1363}
1364
1365function highlightGestaoFilename(text, termo) {
1366    const display = normalizeGestaoFilename(text == null ? '' : text);
1367    const needle = gestaoSearchNeedle(termo);
1368    if (!needle) return escapeGestaoHtml(display);
1369    const hay = display.toLowerCase();
1370    let out = '';
1371    let from = 0;
1372    let idx = hay.indexOf(needle, from);
1373    if (idx === -1) return escapeGestaoHtml(display);
1374    while (idx !== -1) {
1375        out += escapeGestaoHtml(display.slice(from, idx));
1376        out += `<mark class="gp-autos-search-hit">${escapeGestaoHtml(display.slice(idx, idx + needle.length))}</mark>`;
1377        from = idx + needle.length;
1378        idx = hay.indexOf(needle, from);
1379    }
1380    out += escapeGestaoHtml(display.slice(from));
1381    return out;
1382}
1383
1384function gestaoPartNameHtml(part, termo) {
1385    const name = (part && part.name) || '';
1386    const needle = gestaoSearchNeedle(termo);
1387    if (!needle) return escapeGestaoHtml(normalizeGestaoFilename(name));
1388    if (gestaoTextMatchesSearch(name, needle) || !gestaoTextMatchesSearch(part && part.originalName, needle)) {
1389        return highlightGestaoFilename(name, termo);
1390    }
1391    return `<mark class="gp-autos-search-hit">${escapeGestaoHtml(normalizeGestaoFilename(name))}</mark>`;
1392}
1393
1394function gestaoGetSearchTerm() {
1395    const el = typeof gestaoGetById === 'function' ? gestaoGetById('buscaProcesso') : null;
1396    return el ? String(el.value || '').trim() : '';
1397}
1398
1399function getFilteredSortedProcessosGestao(source) {
1400    const needle = gestaoSearchNeedle(gestaoGetSearchTerm());
1401    return (Array.isArray(source) ? source : [])
1402        .filter(p => gestaoProcessoMatchesSearch(p, needle))
1403        .sort((a, b) => compareProcessosGestao(a, b, gestaoSortState.field, gestaoSortState.direction));
1404}
1405
1406function gestaoExpandUnifiedSearchHits(processos) {
1407    const needle = gestaoSearchNeedle(gestaoGetSearchTerm());
1408    // Só o match em arquivo individual abre o detalhe. O nome do PDF
1409    // unificado (ex.: 11_arquivos_mesclados.pdf) não basta.
1410    gestaoSearchExpandedPartsRows.forEach(id => gestaoExpandedPartsRows.delete(id));
1411    gestaoSearchExpandedPartsRows.clear();
1412    if (!needle) return;
1413    (Array.isArray(processos) ? processos : []).forEach(p => {
1414        if (!gestaoMergedPartMatchesSearch(p, needle)) return;
1415        const id = gestaoProcessoId(p);
1416        gestaoExpandedPartsRows.add(id);
1417        gestaoSearchExpandedPartsRows.add(id);
1418    });
1419}
1420
1421function gestaoProcessoIdsChanged(a, b) {
1422    if (a.length !== b.length) return true;
1423    return a.some((id, index) => id !== b[index]);
1424}
1425
1426function getGestaoPecasForProcesso(p) {
1427    if (Array.isArray(p?.pecas) && p.pecas.length > 0) return p.pecas;
1428    return p?.hasPeca ? [{ selectedPeca: p.selectedPeca || '' }] : [];
1429}
1430
1431function buildGestaoPecaViewCellHtml(p, processoId) {
1432    const pecas = getGestaoPecasForProcesso(p);
1433    const hasPeca = pecas.length > 0;
1434    const pecaCount = pecas.length;
1435    const isBusy = gestaoRowIsBusy(p);
1436    const isOpening = gestaoPecaViewIsOpening(processoId);
1437    const opacity = hasPeca ? '1' : '0.3';
1438    const cursor = isOpening ? 'wait' : ((hasPeca && !isBusy) ? 'pointer' : 'default');
1439    const pecasJson = hasPeca ? escapeGestaoHtml(JSON.stringify(pecas)) : '';
1440    const onclickAttr = (hasPeca && !isBusy && !isOpening)
1441        ? `onclick="verPecaGeradaGestao('${processoId}', JSON.parse(this.dataset.pecas), this)" data-pecas="${pecasJson}"`
1442        : (hasPeca ? `data-pecas="${pecasJson}"` : '');
1443    const title = isOpening
1444        ? (__isEN ? 'Opening draft...' : 'Abrindo a peça...')
1445        : (hasPeca
1446            ? (pecaCount > 1 ? `${pecaCount} peças geradas` : 'Ver Peça Gerada')
1447            : 'Sem peça gerada');
1448    const iconClass = isOpening ? 'fa fa-spinner fa-spin peca-view-icon' : 'fa fa-file-text-o peca-view-icon';
1449    const iconColor = isOpening ? '#1d4ed8' : '#007bff';
1450    const iconOpacity = (isBusy && hasPeca && !isOpening) ? '0.3' : opacity;
1451
1452    return `
1453                    <span class="peca-view-cell" style="display:inline-flex;align-items:center;gap:3px;">
1454                        <i class="${iconClass}" data-loading="${isOpening ? '1' : '0'}" style="font-size: 1.2rem; color: ${iconColor}; opacity: ${iconOpacity}; cursor: ${cursor};" ${onclickAttr} title="${title}"></i>
1455                        ${pecaCount > 1 ? `<span class="peca-view-badge" style="font-size:0.65rem;background:#007bff;color:#fff;border-radius:10px;padding:1px 5px;cursor:${cursor};" ${onclickAttr}>${pecaCount}</span>` : ''}
1456                    </span>
1457                `;
1458}
1459
1460function patchGestaoTableRowsInPlace() {
1461    const tbody = gestaoGetById('tabelaProcessosBody');
1462    if (!tbody) return;
1463    if (gestaoTableHasDataRows(tbody)) setGestaoProcessosTableOverlay('hide');
1464
1465    const filtrados = getFilteredSortedProcessosGestao(processosDataGestao);
1466    const filtradosById = new Map(filtrados.map(p => [gestaoProcessoId(p), p]));
1467
1468    const rows = tbody.querySelectorAll('tr');
1469
1470    rows.forEach(row => {
1471        const id = row.querySelector('.processo-checkbox')?.value;
1472        const p = filtradosById.get(id);
1473        if (!p) return;
1474
1475        const displayStatus = gestaoEffectiveStatus(p);
1476        const statusClass = getStatusClassGestao(displayStatus);
1477        const statusLabel = getStatusLabelGestao(displayStatus);
1478        const isProcessing = gestaoRowIsBusy(p);
1479        const jobErrorMessage = p.status === 'erro' ? gestaoResolveJobErrorMessage(p) : '';
1480        const statusCell = row.querySelector('.gp-status-cell');
1481        if (statusCell) {
1482            const errorTitle = jobErrorMessage
1483                ? `title="${escapeGestaoHtml(jobErrorMessage).replace(/"/g, '&quot;')}"`
1484                : '';
1485            const errorHint = jobErrorMessage
1486                ? `<div class="gp-status-error-hint">${escapeGestaoHtml(jobErrorMessage.length > 140 ? jobErrorMessage.slice(0, 137) + '...' : jobErrorMessage)}</div>`
1487                : '';
1488            statusCell.innerHTML = `<span class="tag-status ${statusClass}" ${errorTitle}>${isProcessing ? '<i class="fa fa-spinner fa-spin"></i>' : ''}${statusLabel}</span>${errorHint}`;
1489        }
1490
1491        const pecaViewCell = row.querySelector('.gp-generated-piece-cell');
1492        if (pecaViewCell) {
1493            pecaViewCell.innerHTML = buildGestaoPecaViewCellHtml(p, id);
1494        }
1495        applyGestaoRowBusyState(row, p);
1496        updateProcessoRowHighlightGestao(row);
1497    });
1498}
1499
1500function ensureGestaoInstructionModal() {
1501    let modal = document.getElementById('gestaoInstructionModal');
1502    if (modal) {
1503        const leftoverClear = modal.querySelector('#gestaoInstructionModalClear');
1504        if (leftoverClear) leftoverClear.remove();
1505        return modal;
1506    }
1507
1508    modal = document.createElement('div');
1509    modal.id = 'gestaoInstructionModal';
1510    modal.className = 'cj-gp-instruction-modal';
1511    modal.hidden = true;
1512    modal.setAttribute('data-cj-gp-instruction-modal', '');
1513
1514    const backdrop = document.createElement('div');
1515    backdrop.className = 'cj-gp-instruction-modal__backdrop';
1516    backdrop.addEventListener('click', closeGestaoPromptModal);
1517
1518    const panel = document.createElement('div');
1519    panel.className = 'cj-gp-instruction-modal__panel';
1520    panel.setAttribute('role', 'dialog');
1521    panel.setAttribute('aria-modal', 'true');
1522    panel.setAttribute('aria-labelledby', 'gestaoInstructionModalTitle');
1523
1524    const header = document.createElement('header');
1525    header.className = 'cj-gp-instruction-modal__header';
1526
1527    const heading = document.createElement('div');
1528    heading.className = 'cj-gp-instruction-modal__heading';
1529
1530    const title = document.createElement('h2');
1531    title.id = 'gestaoInstructionModalTitle';
1532    title.className = 'cj-gp-instruction-modal__title';
1533    title.textContent = __isEN ? 'Additional Instructions' : 'Instruções Adicionais';
1534
1535    const description = document.createElement('p');
1536    description.className = 'cj-gp-instruction-modal__description';
1537    description.textContent = __isEN
1538        ? 'Enter a prompt name and instruction, or choose one from your prompt bank.'
1539        : 'Informe o nome do prompt e a instrução, ou escolha uma do seu banco de prompts.';
1540
1541    heading.appendChild(title);
1542    heading.appendChild(description);
1543
1544    const closeBtn = document.createElement('button');
1545    closeBtn.type = 'button';
1546    closeBtn.className = 'cj-gp-instruction-modal__close';
1547    closeBtn.setAttribute('aria-label', __isEN ? 'Close' : 'Fechar');
1548    closeBtn.innerHTML = '<i class="fa fa-times" aria-hidden="true"></i>';
1549    closeBtn.addEventListener('click', closeGestaoPromptModal);
1550
1551    header.appendChild(heading);
1552    header.appendChild(closeBtn);
1553
1554    const content = document.createElement('div');
1555    content.className = 'cj-gp-instruction-modal__content';
1556
1557    const titleField = document.createElement('label');
1558    titleField.className = 'cj-gp-instruction-modal__field';
1559    const titleFieldLabel = document.createElement('span');
1560    titleFieldLabel.className = 'cj-gp-instruction-modal__field-label';
1561    titleFieldLabel.textContent = __isEN ? 'Prompt name' : 'Nome do prompt';
1562    const titleInput = document.createElement('input');
1563    titleInput.id = 'gestaoInstructionModalTitleInput';
1564    titleInput.type = 'text';
1565    titleInput.className = 'cj-gp-instruction-modal__input';
1566    titleInput.maxLength = 180;
1567    titleInput.placeholder = __isEN
1568        ? 'Example: Detailed statement of facts'
1569        : 'Exemplo: Narrativa detalhada dos fatos';
1570    titleField.appendChild(titleFieldLabel);
1571    titleField.appendChild(titleInput);
1572
1573    const field = document.createElement('label');
1574    field.className = 'cj-gp-instruction-modal__field';
1575    const fieldLabel = document.createElement('span');
1576    fieldLabel.className = 'cj-gp-instruction-modal__field-label';
1577    fieldLabel.textContent = __isEN ? 'Instruction' : 'Instrução';
1578    const textarea = document.createElement('textarea');
1579    textarea.id = 'gestaoInstructionModalTextarea';
1580    textarea.className = 'cj-gp-instruction-modal__textarea';
1581    textarea.rows = 5;
1582    textarea.placeholder = __isEN
1583        ? 'Example: make the statement of facts as detailed as possible.'
1584        : 'Exemplo: quero que a narrativa dos fatos seja o mais detalhada possível.';
1585
1586    // Wrapper posicionado para o popup de menção @arquivo.
1587    const mentionWrapper = document.createElement('div');
1588    mentionWrapper.className = 'cj-gp-instruction-modal__mention-wrapper';
1589    const mentionList = document.createElement('div');
1590    mentionList.id = 'gestaoInstructionModalMentionList';
1591    mentionList.className = 'cj-gp-instruction-modal__mention-list';
1592    mentionList.hidden = true;
1593    mentionWrapper.appendChild(textarea);
1594    mentionWrapper.appendChild(mentionList);
1595
1596    const mentionHint = document.createElement('span');
1597    mentionHint.id = 'gestaoInstructionModalMentionHint';
1598    mentionHint.className = 'cj-gp-instruction-modal__mention-hint';
1599    mentionHint.hidden = true;
1600
1601    field.appendChild(fieldLabel);
1602    field.appendChild(mentionWrapper);
1603    field.appendChild(mentionHint);
1604    bindGestaoMentionEvents(textarea);
1605
1606    const bankSection = document.createElement('section');
1607    bankSection.className = 'cj-gp-instruction-modal__bank';
1608    const bankTitle = document.createElement('h3');
1609    bankTitle.className = 'cj-gp-instruction-modal__bank-title';
1610    bankTitle.textContent = __isEN ? 'Saved prompts' : 'Prompts salvos';
1611    const bank = document.createElement('div');
1612    bank.id = 'gestaoInstructionModalBank';
1613    bank.className = 'cj-gp-instruction-modal__bank-list';
1614    bankSection.appendChild(bankTitle);
1615    bankSection.appendChild(bank);
1616
1617    content.appendChild(titleField);
1618    content.appendChild(field);
1619    content.appendChild(bankSection);
1620
1621    const footer = document.createElement('footer');
1622    footer.className = 'cj-gp-instruction-modal__footer';
1623
1624    const cancelBtn = document.createElement('button');
1625    cancelBtn.type = 'button';
1626    cancelBtn.id = 'gestaoInstructionModalCancel';
1627    cancelBtn.className = 'cj-gp-instruction-modal__btn cj-gp-instruction-modal__btn--ghost';
1628    cancelBtn.textContent = __isEN ? 'Cancel' : 'Cancelar';
1629    cancelBtn.addEventListener('click', closeGestaoPromptModal);
1630
1631    const applyBtn = document.createElement('button');
1632    applyBtn.type = 'button';
1633    applyBtn.id = 'gestaoInstructionModalApply';
1634    applyBtn.className = 'cj-gp-instruction-modal__btn cj-gp-instruction-modal__btn--primary';
1635    applyBtn.textContent = __isEN ? 'Apply' : 'Aplicar';
1636    applyBtn.addEventListener('click', saveGestaoPromptModal);
1637
1638    footer.appendChild(cancelBtn);
1639    footer.appendChild(applyBtn);
1640
1641    panel.appendChild(header);
1642    panel.appendChild(content);
1643    panel.appendChild(footer);
1644    modal.appendChild(backdrop);
1645    modal.appendChild(panel);
1646    document.body.appendChild(modal);
1647    return modal;
1648}
1649
1650// Menção @arquivo: candidatos são os arquivos que formaram o processo unificado
1651// da linha aberta. A menção só contextualiza (o servidor traduz em faixa de
1652// páginas no prompt); não restringe a análise.
1653let gestaoMentionCandidates = [];
1654let gestaoMentionMatches = [];
1655let gestaoMentionActiveIndex = 0;
1656
1657function gestaoMentionTokenAtCaret(textarea) {
1658    if (!textarea) return null;
1659    const caret = textarea.selectionStart;
1660    if (caret !== textarea.selectionEnd) return null;
1661    const before = textarea.value.slice(0, caret);
1662    const match = /(^|[\s(\[])@([^\n@]*)$/.exec(before);
1663    if (!match) return null;
1664    return { query: match[2], start: caret - match[2].length - 1, end: caret };
1665}
1666
1667function closeGestaoMentionList() {
1668    const list = document.getElementById('gestaoInstructionModalMentionList');
1669    if (!list) return;
1670    list.hidden = true;
1671    list.innerHTML = '';
1672    gestaoMentionMatches = [];
1673    gestaoMentionActiveIndex = 0;
1674}
1675
1676function renderGestaoMentionList(textarea) {
1677    const list = document.getElementById('gestaoInstructionModalMentionList');
1678    if (!list) return;
1679    if (!gestaoMentionMatches.length) {
1680        closeGestaoMentionList();
1681        return;
1682    }
1683    list.innerHTML = '';
1684    gestaoMentionMatches.forEach((part, index) => {
1685        const item = document.createElement('button');
1686        item.type = 'button';
1687        item.className = 'cj-gp-instruction-modal__mention-item'
1688            + (index === gestaoMentionActiveIndex ? ' is-active' : '');
1689        const name = document.createElement('span');
1690        name.className = 'cj-gp-instruction-modal__mention-name';
1691        name.textContent = part.name;
1692        item.appendChild(name);
1693        const pagesLabel = gestaoPartPagesLabel(part);
1694        if (pagesLabel) {
1695            const pages = document.createElement('span');
1696            pages.className = 'cj-gp-instruction-modal__mention-pages';
1697            pages.textContent = pagesLabel;
1698            item.appendChild(pages);
1699        }
1700        // mousedown para não perder o caret do textarea antes de inserir.
1701        item.addEventListener('mousedown', (e) => {
1702            e.preventDefault();
1703            applyGestaoMention(textarea, part);
1704        });
1705        list.appendChild(item);
1706    });
1707    list.hidden = false;
1708    const active = list.children[gestaoMentionActiveIndex];
1709    if (active && typeof active.scrollIntoView === 'function') {
1710        active.scrollIntoView({ block: 'nearest' });
1711    }
1712}
1713
1714function updateGestaoMentionList(textarea) {
1715    if (!gestaoMentionCandidates.length) {
1716        closeGestaoMentionList();
1717        return;
1718    }
1719    const token = gestaoMentionTokenAtCaret(textarea);
1720    if (!token) {
1721        closeGestaoMentionList();
1722        return;
1723    }
1724    const query = normalizeGestaoFilename(token.query).trim().toLowerCase();
1725    gestaoMentionMatches = gestaoMentionCandidates
1726        .filter(part => !query || part.name.toLowerCase().includes(query));
1727    gestaoMentionActiveIndex = 0;
1728    renderGestaoMentionList(textarea);
1729}
1730
1731function applyGestaoMention(textarea, part) {
1732    if (!textarea || !part) return;
1733    const token = gestaoMentionTokenAtCaret(textarea);
1734    if (!token) return;
1735    const insertion = '@' + part.name + ' ';
1736    const value = textarea.value;
1737    textarea.value = value.slice(0, token.start) + insertion + value.slice(token.end);
1738    const caret = token.start + insertion.length;
1739    textarea.setSelectionRange(caret, caret);
1740    closeGestaoMentionList();
1741    textarea.focus();
1742}
1743
1744function bindGestaoMentionEvents(textarea) {
1745    if (!textarea || textarea.dataset.mentionBound === 'true') return;
1746    textarea.dataset.mentionBound = 'true';
1747
1748    textarea.addEventListener('input', () => updateGestaoMentionList(textarea));
1749    textarea.addEventListener('click', () => updateGestaoMentionList(textarea));
1750    textarea.addEventListener('blur', () => setTimeout(closeGestaoMentionList, 120));
1751    textarea.addEventListener('keydown', (e) => {
1752        const list = document.getElementById('gestaoInstructionModalMentionList');
1753        if (!list || list.hidden || !gestaoMentionMatches.length) return;
1754        if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
1755            e.preventDefault();
1756            const delta = e.key === 'ArrowDown' ? 1 : -1;
1757            const total = gestaoMentionMatches.length;
1758            gestaoMentionActiveIndex = (gestaoMentionActiveIndex + delta + total) % total;
1759            renderGestaoMentionList(textarea);
1760            return;
1761        }
1762        if (e.key === 'Enter' || e.key === 'Tab') {
1763            e.preventDefault();
1764            applyGestaoMention(textarea, gestaoMentionMatches[gestaoMentionActiveIndex]);
1765            return;
1766        }
1767        if (e.key === 'Escape') {
1768            // Esc fecha só a lista; o modal continua aberto.
1769            e.preventDefault();
1770            e.stopPropagation();
1771            closeGestaoMentionList();
1772        }
1773    });
1774}
1775
1776function setGestaoMentionCandidates(parts) {
1777    gestaoMentionCandidates = Array.isArray(parts) ? parts : [];
1778    closeGestaoMentionList();
1779    const hint = document.getElementById('gestaoInstructionModalMentionHint');
1780    if (!hint) return;
1781    if (!gestaoMentionCandidates.length) {
1782        hint.hidden = true;
1783        hint.textContent = '';
1784        return;
1785    }
1786    hint.hidden = false;
1787    hint.innerHTML = `${__isEN
1788        ? 'Type @ to reference one of the files in this unified case.'
1789        : 'Digite @ para referenciar um dos arquivos deste processo unificado.'}`;
1790}
1791
1792function openGestaoInstructionModal() {
1793    const modal = ensureGestaoInstructionModal();
1794    modal.hidden = false;
1795    modal.classList.add('is-open');
1796    document.body.classList.add('cj-gp-instruction-modal-open');
1797    if (gestaoInstructionModalEscapeHandler) {
1798        document.removeEventListener('keydown', gestaoInstructionModalEscapeHandler);
1799    }
1800    gestaoInstructionModalEscapeHandler = (event) => {
1801        if (event.key === 'Escape') closeGestaoPromptModal();
1802    };
1803    document.addEventListener('keydown', gestaoInstructionModalEscapeHandler);
1804    return modal;
1805}
1806
1807function closeGestaoInstructionModal() {
1808    const modal = document.getElementById('gestaoInstructionModal');
1809    if (!modal) return;
1810    closeGestaoMentionList();
1811    modal.hidden = true;
1812    modal.classList.remove('is-open');
1813    document.body.classList.remove('cj-gp-instruction-modal-open');
1814    if (gestaoInstructionModalEscapeHandler) {
1815        document.removeEventListener('keydown', gestaoInstructionModalEscapeHandler);
1816        gestaoInstructionModalEscapeHandler = null;
1817    }
1818    gestaoInstructionModalRowId = null;
1819    gestaoInstructionModalSelectedIndex = null;
1820}
1821
1822function findGestaoPromptBankIndex(promptText, promptTitle, prompts) {
1823    const list = Array.isArray(prompts) ? prompts.filter(item => item && item.prompt) : [];
1824    const prompt = String(promptText || '').trim();
1825    if (!prompt) return -1;
1826    const title = String(promptTitle || '').trim();
1827    let idx = list.findIndex(item =>
1828        String(item.prompt || '').trim() === prompt
1829        && String(item.title || '').trim() === title
1830    );
1831    if (idx < 0) {
1832        idx = list.findIndex(item => String(item.prompt || '').trim() === prompt);
1833    }
1834    return idx;
1835}
1836
1837function setGestaoPromptModalSelection(index) {
1838    gestaoInstructionModalSelectedIndex = (index == null || index < 0) ? null : Number(index);
1839    const bankEl = document.getElementById('gestaoInstructionModalBank');
1840    if (!bankEl) return;
1841    bankEl.querySelectorAll('.cj-gp-instruction-modal__bank-item').forEach(btn => {
1842        const btnIndex = Number(btn.dataset.promptIndex);
1843        btn.classList.toggle(
1844            'is-selected',
1845            gestaoInstructionModalSelectedIndex != null && btnIndex === gestaoInstructionModalSelectedIndex
1846        );
1847    });
1848}
1849
1850function renderGestaoInstructionBank(prompts) {
1851    const bankEl = document.getElementById('gestaoInstructionModalBank');
1852    if (!bankEl) return;
1853    bankEl.innerHTML = '';
1854    const list = Array.isArray(prompts) ? prompts.filter(item => item && item.prompt) : [];
1855    if (!list.length) {
1856        const empty = document.createElement('p');
1857        empty.className = 'cj-gp-instruction-modal__empty';
1858        empty.textContent = __isEN
1859            ? 'No saved instructions yet. Type a new one above.'
1860            : 'Nenhuma instrução salva ainda. Digite uma nova acima.';
1861        bankEl.appendChild(empty);
1862        return;
1863    }
1864    list.forEach((item, index) => {
1865        const btn = document.createElement('button');
1866        btn.type = 'button';
1867        btn.className = 'cj-gp-instruction-modal__bank-item';
1868        btn.dataset.promptIndex = String(index);
1869        const span = document.createElement('span');
1870        const promptTitle = String(item.title || '').trim();
1871        span.textContent = promptTitle || getGestaoPromptTitleFallback(item.prompt || '');
1872        btn.title = item.prompt || '';
1873        btn.appendChild(span);
1874        if (gestaoInstructionModalSelectedIndex != null && gestaoInstructionModalSelectedIndex === index) {
1875            btn.classList.add('is-selected');
1876        }
1877        btn.addEventListener('click', () => {
1878            const idx = Number(btn.dataset.promptIndex);
1879            const textarea = document.getElementById('gestaoInstructionModalTextarea');
1880            const titleEl = document.getElementById('gestaoInstructionModalTitleInput');
1881            if (gestaoInstructionModalSelectedIndex != null && gestaoInstructionModalSelectedIndex === idx) {
1882                if (textarea) textarea.value = '';
1883                if (titleEl) titleEl.value = '';
1884                setGestaoPromptModalSelection(null);
1885                if (textarea) textarea.focus();
1886                return;
1887            }
1888            if (textarea && list[idx]) {
1889                textarea.value = list[idx].prompt || '';
1890                if (titleEl) {
1891                    titleEl.value = String(list[idx].title || '').trim() || getGestaoPromptTitleFallback(list[idx].prompt || '');
1892                }
1893                textarea.focus();
1894            }
1895            setGestaoPromptModalSelection(idx);
1896        });
1897        bankEl.appendChild(btn);
1898    });
1899}
1900
1901async function loadGestaoInstructionBank() {
1902    ensureGestaoInstructionModal();
1903    const bankEl = document.getElementById('gestaoInstructionModalBank');
1904    if (!bankEl) return;
1905    bankEl.innerHTML = '';
1906    const loading = document.createElement('p');
1907    loading.className = 'cj-gp-instruction-modal__empty';
1908    loading.textContent = __isEN ? 'Loading...' : 'Carregando...';
1909    bankEl.appendChild(loading);
1910    const shouldFetch = !!getErpActiveOrgId() || !promptHistoryLoadedOnce || assessorPromptHistory.length === 0;
1911    if (shouldFetch) {
1912        const prompts = await fetchPromptHistoryFromDB();
1913        assessorPromptHistory = prompts;
1914        promptHistoryLoadedOnce = true;
1915        writePromptHistoryCache(prompts);
1916    }
1917    renderGestaoInstructionBank(assessorPromptHistory);
1918    const textarea = document.getElementById('gestaoInstructionModalTextarea');
1919    const titleEl = document.getElementById('gestaoInstructionModalTitleInput');
1920    const selectedIdx = findGestaoPromptBankIndex(
1921        textarea && textarea.value,
1922        titleEl && titleEl.value,
1923        assessorPromptHistory
1924    );
1925    setGestaoPromptModalSelection(selectedIdx);
1926}
1927
1928function closeGestaoPromptModal() {
1929    closeGestaoInstructionModal();
1930}
1931
1932function openGestaoPromptModal(row) {
1933    const id = getGestaoRowId(row);
1934    if (!id) return;
1935    const pecaSelect = row.querySelector('.row-peca-select');
1936    if (gestaoIsAutomaticPeca(pecaSelect?.value)) {
1937        funcMsg(
1938            'info',
1939            __L.attn,
1940            __isEN
1941                ? 'Additional prompts are disabled for Automatic. Choose Free to write your own instructions.'
1942                : 'Prompts adicionais ficam desativados na opção Automática. Escolha Livre para escrever suas próprias instruções.',
1943            '',
1944            '',
1945            undefined,
1946            gestaoInsightOptions('instruction_prompt_automatic_disabled', {
1947                errorCode: 'validation:prompt_automatic_disabled'
1948            })
1949        );
1950        return;
1951    }
1952    openGestaoInstructionModal();
1953    const textarea = document.getElementById('gestaoInstructionModalTextarea');
1954    const titleInput = document.getElementById('gestaoInstructionModalTitleInput');
1955    const input = row.querySelector('.row-additional-prompt');
1956    const titleHidden = row.querySelector('.row-additional-prompt-title');
1957    if (!textarea || !input) return;
1958    gestaoInstructionModalRowId = id;
1959    textarea.value = input.value || '';
1960    if (titleInput) {
1961        titleInput.value = titleHidden ? (titleHidden.value || '') : '';
1962    }
1963    const processo = processosDataGestao.find(p => gestaoProcessoId(p) === id);
1964    // Nomes homônimos aparecem uma vez na lista de menção (o servidor cita as
1965    // duas faixas de página), mas seguem individuais no detalhe da tabela.
1966    const mentionParts = [];
1967    gestaoResolveMergedParts(processo).forEach(part => {
1968        if (!mentionParts.some(existing => existing.name === part.name)) mentionParts.push(part);
1969    });
1970    setGestaoMentionCandidates(mentionParts);
1971    gestaoInstructionModalSelectedIndex = findGestaoPromptBankIndex(
1972        input.value,
1973        titleHidden && titleHidden.value,
1974        assessorPromptHistory
1975    );
1976    loadGestaoInstructionBank();
1977    setTimeout(() => {
1978        const focusEl = String(textarea.value || '').trim() ? textarea : (titleInput || textarea);
1979        focusEl.focus();
1980    }, 50);
1981}
1982
1983async function persistGestaoPromptBankEntry(title, prompt) {
1984    if (!prompt || !isPromptHistoryAuthReady() || !isAssessorUserProForPromptHistory()) return;
1985    try {
1986        const orgId = getErpActiveOrgId();
1987        if (orgId && typeof window.cjErpSaveOrgPrompt === 'function') {
1988            await window.cjErpSaveOrgPrompt(title, prompt);
1989            upsertLocalPromptHistory(prompt, title);
1990            writePromptHistoryCache(assessorPromptHistory);
1991            if (typeof window.cjErpNotifyPromptsChanged === 'function') {
1992                window.cjErpNotifyPromptsChanged();
1993            }
1994            return;
1995        }
1996
1997        const response = await fetch('/assessor_prompt_bank', {
1998            method: 'POST',
1999            headers: {
2000                'Content-Type': 'application/json',
2001                'Authorization': `Bearer ${cognijusUserToken}`
2002            },
2003            body: JSON.stringify({ title, prompt })
2004        });
2005        if (!response.ok) return;
2006        upsertLocalPromptHistory(prompt, title);
2007        writePromptHistoryCache(assessorPromptHistory);
2008    } catch (err) {
2009        console.error('Error saving assessor prompt bank entry:', err);
2010    }
2011}
2012
2013function saveGestaoPromptModal() {
2014    if (!gestaoInstructionModalRowId) return;
2015    const row = gestaoQuery(`.processo-checkbox[value="${gestaoInstructionModalRowId}"]`)?.closest('tr');
2016    const textarea = document.getElementById('gestaoInstructionModalTextarea');
2017    const titleInput = document.getElementById('gestaoInstructionModalTitleInput');
2018    const input = row ? row.querySelector('.row-additional-prompt') : null;
2019    const titleHidden = row ? row.querySelector('.row-additional-prompt-title') : null;
2020    if (!row || !textarea || !input) return;
2021
2022    const prompt = textarea.value.trim();
2023    const title = titleInput ? titleInput.value.trim() : '';
2024
2025    input.value = prompt;
2026    if (titleHidden) titleHidden.value = title;
2027    syncGestaoRowParamsFromDom(row);
2028    updateGestaoPromptCell(row);
2029    if (prompt && title) {
2030        persistGestaoPromptBankEntry(title, prompt);
2031    }
2032    closeGestaoPromptModal();
2033}
2034
2035let gestaoModelModalRowId = null;
2036let gestaoModelModalSelectedId = null;
2037let gestaoModelModalSourceName = '';
2038let gestaoModelModalSourceMime = '';
2039let gestaoModelModalEscapeHandler = null;
2040
2041function gestaoModelEditorHasContent(editor) {
2042    if (!editor) return false;
2043    return String(editor.innerText || '').trim().length > 0;
2044}
2045
2046function syncGestaoModelEditorEmptyState() {
2047    const editor = document.getElementById('gestaoModelModalEditor');
2048    if (!editor) return;
2049    editor.classList.toggle('is-empty', !gestaoModelEditorHasContent(editor));
2050}
2051
2052function setGestaoModelModalSelection(modelId) {
2053    gestaoModelModalSelectedId = modelId ? String(modelId) : null;
2054    const bankEl = document.getElementById('gestaoModelModalBank');
2055    if (!bankEl) return;
2056    bankEl.querySelectorAll('.cj-gp-instruction-modal__bank-item').forEach(btn => {
2057        btn.classList.toggle('is-selected', !!(gestaoModelModalSelectedId && btn.dataset.modelId === gestaoModelModalSelectedId));
2058    });
2059}
2060
2061function clearGestaoModelModalEditor(meta) {
2062    const editor = document.getElementById('gestaoModelModalEditor');
2063    const titleInput = document.getElementById('gestaoModelModalTitleInput');
2064    const fileInput = document.getElementById('gestaoModelModalFile');
2065    const fileLabel = document.getElementById('gestaoModelModalFileLabel');
2066    const status = document.getElementById('gestaoModelModalImportStatus');
2067    const uploader = document.getElementById('gestaoModelModalUploader');
2068    if (editor) editor.innerHTML = '';
2069    gestaoModelModalSourceName = (meta && meta.sourceFileName) || '';
2070    gestaoModelModalSourceMime = (meta && meta.sourceMimeType) || '';
2071    if (titleInput && meta && meta.title && !String(titleInput.value || '').trim()) {
2072        titleInput.value = meta.title;
2073    }
2074    if (fileInput) fileInput.value = '';
2075    if (fileLabel) fileLabel.textContent = gestaoModelModalSourceName;
2076    if (status) {
2077        status.textContent = gestaoModelModalSourceName
2078            ? (__isEN ? 'File loaded in the editor. Review and apply.' : 'Arquivo carregado no editor. Revise e aplique.')
2079            : '';
2080    }
2081    if (uploader) uploader.classList.toggle('has-file', !!gestaoModelModalSourceName);
2082    syncGestaoModelEditorEmptyState();
2083}
2084
2085function applyGestaoModelToRow(row, modelId, modelTitle) {
2086    if (!row) return;
2087    const idInput = row.querySelector('.row-model-id');
2088    const titleInput = row.querySelector('.row-model-title');
2089    const resolvedTitle = getGestaoModelTitle(modelId, modelTitle);
2090    if (idInput) idInput.value = modelId || '';
2091    if (titleInput) titleInput.value = modelId ? resolvedTitle : '';
2092    syncGestaoRowParamsFromDom(row);
2093    updateGestaoModelCell(row);
2094}
2095
2096function ensureGestaoModelModal() {
2097    let modal = document.getElementById('gestaoModelModal');
2098    if (modal) {
2099        const leftoverClear = modal.querySelector('#gestaoModelModalClear');
2100        if (leftoverClear) leftoverClear.remove();
2101        return modal;
2102    }
2103
2104    modal = document.createElement('div');
2105    modal.id = 'gestaoModelModal';
2106    modal.className = 'cj-gp-instruction-modal cj-gp-model-modal';
2107    modal.hidden = true;
2108    modal.setAttribute('data-cj-gp-model-modal', '');
2109
2110    const backdrop = document.createElement('div');
2111    backdrop.className = 'cj-gp-instruction-modal__backdrop';
2112    backdrop.addEventListener('click', closeGestaoModelModal);
2113
2114    const panel = document.createElement('div');
2115    panel.className = 'cj-gp-instruction-modal__panel cj-gp-model-modal__panel';
2116    panel.setAttribute('role', 'dialog');
2117    panel.setAttribute('aria-modal', 'true');
2118    panel.setAttribute('aria-labelledby', 'gestaoModelModalTitle');
2119
2120    const header = document.createElement('header');
2121    header.className = 'cj-gp-instruction-modal__header';
2122
2123    const heading = document.createElement('div');
2124    heading.className = 'cj-gp-instruction-modal__heading';
2125
2126    const title = document.createElement('h2');
2127    title.id = 'gestaoModelModalTitle';
2128    title.className = 'cj-gp-instruction-modal__title';
2129    title.textContent = __isEN ? 'Draft template' : 'Modelo da peça';
2130
2131    const description = document.createElement('p');
2132    description.className = 'cj-gp-instruction-modal__description';
2133    description.textContent = __isEN
2134        ? 'Choose a saved template or add a new one for this case.'
2135        : 'Escolha um modelo salvo ou adicione um novo para este processo.';
2136
2137    heading.appendChild(title);
2138    heading.appendChild(description);
2139
2140    const closeBtn = document.createElement('button');
2141    closeBtn.type = 'button';
2142    closeBtn.className = 'cj-gp-instruction-modal__close';
2143    closeBtn.setAttribute('aria-label', __isEN ? 'Close' : 'Fechar');
2144    closeBtn.innerHTML = '<i class="fa fa-times" aria-hidden="true"></i>';
2145    closeBtn.addEventListener('click', closeGestaoModelModal);
2146
2147    header.appendChild(heading);
2148    header.appendChild(closeBtn);
2149
2150    const content = document.createElement('div');
2151    content.className = 'cj-gp-instruction-modal__content';
2152
2153    const titleField = document.createElement('label');
2154    titleField.className = 'cj-gp-instruction-modal__field';
2155    const titleFieldLabel = document.createElement('span');
2156    titleFieldLabel.className = 'cj-gp-instruction-modal__field-label';
2157    titleFieldLabel.textContent = __isEN ? 'Template name' : 'Nome do modelo';
2158    const titleInput = document.createElement('input');
2159    titleInput.id = 'gestaoModelModalTitleInput';
2160    titleInput.type = 'text';
2161    titleInput.className = 'cj-gp-instruction-modal__input';
2162    titleInput.maxLength = 180;
2163    titleInput.placeholder = __isEN ? 'Example: Initial petition' : 'Exemplo: Petição inicial';
2164    titleField.appendChild(titleFieldLabel);
2165    titleField.appendChild(titleInput);
2166
2167    const uploader = document.createElement('label');
2168    uploader.id = 'gestaoModelModalUploader';
2169    uploader.className = 'cj-gp-model-modal__uploader';
2170    uploader.innerHTML = `
2171        <span class="cj-gp-model-modal__uploader-icon"><i class="fa fa-cloud-upload" aria-hidden="true"></i></span>
2172        <span class="cj-gp-model-modal__uploader-title">${__isEN ? 'Upload Word or PDF' : 'Enviar Word ou PDF'}</span>
2173        <span class="cj-gp-model-modal__uploader-hint">${__isEN ? '.docx or .pdf · up to 20 MB' : '.docx ou .pdf · até 20 MB'}</span>
2174        <input id="gestaoModelModalFile" type="file" accept=".docx,.doc,.pdf,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document">
2175        <span id="gestaoModelModalFileLabel" class="cj-gp-model-modal__uploader-file"></span>
2176    `;
2177    const importStatus = document.createElement('p');
2178    importStatus.id = 'gestaoModelModalImportStatus';
2179    importStatus.className = 'cj-gp-model-modal__import-status';
2180
2181    const editorWrap = document.createElement('div');
2182    editorWrap.className = 'cj-gp-model-modal__editor-wrap';
2183    const editorLabel = document.createElement('div');
2184    editorLabel.className = 'cj-gp-instruction-modal__field-label';
2185    editorLabel.textContent = __isEN ? 'Template content' : 'Conteúdo do modelo';
2186    const toolbar = document.createElement('div');
2187    toolbar.className = 'cj-gp-model-modal__toolbar';
2188    [
2189        ['bold', 'fa-bold', __isEN ? 'Bold' : 'Negrito'],
2190        ['italic', 'fa-italic', __isEN ? 'Italic' : 'Itálico'],
2191        ['underline', 'fa-underline', __isEN ? 'Underline' : 'Sublinhado'],
2192        ['insertUnorderedList', 'fa-list-ul', __isEN ? 'List' : 'Lista'],
2193        ['insertOrderedList', 'fa-list-ol', __isEN ? 'Numbered list' : 'Lista numerada'],
2194        ['removeFormat', 'fa-eraser', __isEN ? 'Clear formatting' : 'Limpar formatação']
2195    ].forEach(([cmd, icon, label]) => {
2196        const btn = document.createElement('button');
2197        btn.type = 'button';
2198        btn.dataset.modelCmd = cmd;
2199        btn.title = label;
2200        btn.innerHTML = `<i class="fa ${icon}" aria-hidden="true"></i>`;
2201        toolbar.appendChild(btn);
2202    });
2203    const editor = document.createElement('div');
2204    editor.id = 'gestaoModelModalEditor';
2205    editor.className = 'cj-gp-model-modal__editor is-empty';
2206    editor.contentEditable = 'true';
2207    editor.setAttribute('role', 'textbox');
2208    editor.setAttribute('aria-multiline', 'true');
2209    editor.dataset.placeholder = __isEN
2210        ? 'Paste from Word or type the template…'
2211        : 'Cole do Word ou digite o conteúdo…';
2212    editor.addEventListener('input', () => {
2213        setGestaoModelModalSelection(null);
2214        syncGestaoModelEditorEmptyState();
2215    });
2216    editor.addEventListener('paste', () => {
2217        setTimeout(() => {
2218            setGestaoModelModalSelection(null);
2219            syncGestaoModelEditorEmptyState();
2220        }, 0);
2221    });
2222    toolbar.addEventListener('click', (event) => {
2223        const btn = event.target.closest('[data-model-cmd]');
2224        if (!btn) return;
2225        event.preventDefault();
2226        document.execCommand(btn.dataset.modelCmd, false, null);
2227        editor.focus();
2228        setGestaoModelModalSelection(null);
2229        syncGestaoModelEditorEmptyState();
2230    });
2231    editorWrap.appendChild(editorLabel);
2232    editorWrap.appendChild(toolbar);
2233    editorWrap.appendChild(editor);
2234
2235    const bankSection = document.createElement('section');
2236    bankSection.className = 'cj-gp-instruction-modal__bank';
2237    const bankTitle = document.createElement('h3');
2238    bankTitle.className = 'cj-gp-instruction-modal__bank-title';
2239    bankTitle.textContent = __isEN ? 'Saved templates' : 'Modelos salvos';
2240    const bank = document.createElement('div');
2241    bank.id = 'gestaoModelModalBank';
2242    bank.className = 'cj-gp-instruction-modal__bank-list';
2243    bankSection.appendChild(bankTitle);
2244    bankSection.appendChild(bank);
2245
2246    content.appendChild(titleField);
2247    content.appendChild(uploader);
2248    content.appendChild(importStatus);
2249    content.appendChild(editorWrap);
2250    content.appendChild(bankSection);
2251
2252    const footer = document.createElement('footer');
2253    footer.className = 'cj-gp-instruction-modal__footer';
2254
2255    const cancelBtn = document.createElement('button');
2256    cancelBtn.type = 'button';
2257    cancelBtn.className = 'cj-gp-instruction-modal__btn cj-gp-instruction-modal__btn--ghost';
2258    cancelBtn.textContent = __isEN ? 'Cancel' : 'Cancelar';
2259    cancelBtn.addEventListener('click', closeGestaoModelModal);
2260
2261    const applyBtn = document.createElement('button');
2262    applyBtn.type = 'button';
2263    applyBtn.id = 'gestaoModelModalApply';
2264    applyBtn.className = 'cj-gp-instruction-modal__btn cj-gp-instruction-modal__btn--primary';
2265    applyBtn.textContent = __isEN ? 'Apply' : 'Aplicar';
2266    applyBtn.addEventListener('click', () => {
2267        saveGestaoModelModal().catch(() => { });
2268    });
2269
2270    footer.appendChild(cancelBtn);
2271    footer.appendChild(applyBtn);
2272
2273    panel.appendChild(header);
2274    panel.appendChild(content);
2275    panel.appendChild(footer);
2276    modal.appendChild(backdrop);
2277    modal.appendChild(panel);
2278    document.body.appendChild(modal);
2279
2280    const fileInput = document.getElementById('gestaoModelModalFile');
2281    if (fileInput) {
2282        fileInput.addEventListener('change', () => {
2283            const file = fileInput.files && fileInput.files[0];
2284            if (file) importGestaoModelModalFile(file);
2285        });
2286    }
2287    ['dragenter', 'dragover'].forEach(type => {
2288        uploader.addEventListener(type, (event) => {
2289            event.preventDefault();
2290            uploader.classList.add('is-dragover');
2291        });
2292    });
2293    ['dragleave', 'drop'].forEach(type => {
2294        uploader.addEventListener(type, (event) => {
2295            event.preventDefault();
2296            uploader.classList.remove('is-dragover');
2297        });
2298    });
2299    uploader.addEventListener('drop', (event) => {
2300        const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
2301        if (file) importGestaoModelModalFile(file);
2302    });
2303
2304    return modal;
2305}
2306
2307function renderGestaoModelBank(models) {
2308    const bankEl = document.getElementById('gestaoModelModalBank');
2309    if (!bankEl) return;
2310    bankEl.innerHTML = '';
2311    const list = Array.isArray(models) ? models.filter(item => item && (item.id || item._id)) : [];
2312    if (!list.length) {
2313        const empty = document.createElement('p');
2314        empty.className = 'cj-gp-instruction-modal__empty';
2315        empty.textContent = __isEN
2316            ? 'No saved templates yet. Add a new one above.'
2317            : 'Nenhum modelo salvo ainda. Adicione um novo acima.';
2318        bankEl.appendChild(empty);
2319        return;
2320    }
2321    list.forEach((item) => {
2322        const id = String(item.id || item._id || '');
2323        const btn = document.createElement('button');
2324        btn.type = 'button';
2325        btn.className = 'cj-gp-instruction-modal__bank-item';
2326        btn.dataset.modelId = id;
2327        const span = document.createElement('span');
2328        span.textContent = item.title || (__isEN ? 'Untitled template' : 'Modelo sem título');
2329        btn.title = item.sourceFileName || span.textContent;
2330        btn.appendChild(span);
2331        if (gestaoModelModalSelectedId && gestaoModelModalSelectedId === id) {
2332            btn.classList.add('is-selected');
2333        }
2334        btn.addEventListener('click', () => {
2335            if (gestaoModelModalSelectedId && gestaoModelModalSelectedId === id) {
2336                const titleEl = document.getElementById('gestaoModelModalTitleInput');
2337                if (titleEl) titleEl.value = '';
2338                setGestaoModelModalSelection(null);
2339                return;
2340            }
2341            const titleEl = document.getElementById('gestaoModelModalTitleInput');
2342            if (titleEl) titleEl.value = item.title || '';
2343            const editor = document.getElementById('gestaoModelModalEditor');
2344            if (editor) editor.innerHTML = '';
2345            gestaoModelModalSourceName = '';
2346            gestaoModelModalSourceMime = '';
2347            const fileLabel = document.getElementById('gestaoModelModalFileLabel');
2348            const status = document.getElementById('gestaoModelModalImportStatus');
2349            const fileInput = document.getElementById('gestaoModelModalFile');
2350            const uploader = document.getElementById('gestaoModelModalUploader');
2351            if (fileLabel) fileLabel.textContent = '';
2352            if (status) status.textContent = '';
2353            if (fileInput) fileInput.value = '';
2354            if (uploader) uploader.classList.remove('has-file');
2355            syncGestaoModelEditorEmptyState();
2356            setGestaoModelModalSelection(id);
2357        });
2358        bankEl.appendChild(btn);
2359    });
2360}
2361
2362async function loadGestaoModelBank() {
2363    ensureGestaoModelModal();
2364    const bankEl = document.getElementById('gestaoModelModalBank');
2365    if (!bankEl) return;
2366    bankEl.innerHTML = '';
2367    const loading = document.createElement('p');
2368    loading.className = 'cj-gp-instruction-modal__empty';
2369    loading.textContent = __isEN ? 'Loading...' : 'Carregando...';
2370    bankEl.appendChild(loading);
2371    const models = await loadGestaoModels(true);
2372    renderGestaoModelBank(models);
2373    setGestaoModelModalSelection(gestaoModelModalSelectedId);
2374}
2375
2376function openGestaoModelModalDialog() {
2377    const modal = ensureGestaoModelModal();
2378    modal.hidden = false;
2379    modal.classList.add('is-open');
2380    document.body.classList.add('cj-gp-instruction-modal-open');
2381    if (gestaoModelModalEscapeHandler) {
2382        document.removeEventListener('keydown', gestaoModelModalEscapeHandler);
2383    }
2384    gestaoModelModalEscapeHandler = (event) => {
2385        if (event.key === 'Escape') closeGestaoModelModal();
2386    };
2387    document.addEventListener('keydown', gestaoModelModalEscapeHandler);
2388    return modal;
2389}
2390
2391function closeGestaoModelModal() {
2392    const modal = document.getElementById('gestaoModelModal');
2393    if (!modal) return;
2394    modal.hidden = true;
2395    modal.classList.remove('is-open');
2396    document.body.classList.remove('cj-gp-instruction-modal-open');
2397    if (gestaoModelModalEscapeHandler) {
2398        document.removeEventListener('keydown', gestaoModelModalEscapeHandler);
2399        gestaoModelModalEscapeHandler = null;
2400    }
2401    gestaoModelModalRowId = null;
2402    gestaoModelModalSelectedId = null;
2403    gestaoModelModalSourceName = '';
2404    gestaoModelModalSourceMime = '';
2405}
2406
2407function openGestaoModelModal(row) {
2408    const id = getGestaoRowId(row);
2409    if (!id) return;
2410    if (!gestaoModelsCanEdit()) {
2411        funcMsg(
2412            'info',
2413            __L.attn,
2414            __isEN
2415                ? 'Templates are available in Gestão Processual after you select a persona.'
2416                : 'Modelos ficam disponíveis na Gestão Processual após selecionar uma persona.',
2417            '',
2418            '',
2419            undefined,
2420            gestaoInsightOptions('instruction_model_org_required', {
2421                errorCode: 'validation:model_org_required'
2422            })
2423        );
2424        return;
2425    }
2426    openGestaoModelModalDialog();
2427    const titleInput = document.getElementById('gestaoModelModalTitleInput');
2428    const idHidden = row.querySelector('.row-model-id');
2429    const titleHidden = row.querySelector('.row-model-title');
2430    gestaoModelModalRowId = id;
2431    const currentId = idHidden ? String(idHidden.value || '').trim() : '';
2432    const currentTitle = titleHidden ? String(titleHidden.value || '').trim() : '';
2433    if (titleInput) titleInput.value = getGestaoModelTitle(currentId, currentTitle);
2434    clearGestaoModelModalEditor(null);
2435    setGestaoModelModalSelection(currentId || null);
2436    loadGestaoModelBank();
2437    setTimeout(() => {
2438        const focusEl = currentId ? titleInput : titleInput;
2439        if (focusEl) focusEl.focus();
2440    }, 50);
2441}
2442
2443async function importGestaoModelModalFile(file) {
2444    const status = document.getElementById('gestaoModelModalImportStatus');
2445    const applyBtn = document.getElementById('gestaoModelModalApply');
2446    if (typeof window.cjErpImportOrgModel !== 'function') return;
2447    if (status) status.textContent = __isEN ? 'Importing...' : 'Importando...';
2448    if (applyBtn) applyBtn.disabled = true;
2449    try {
2450        const item = await window.cjErpImportOrgModel(file);
2451        const editor = document.getElementById('gestaoModelModalEditor');
2452        const titleInput = document.getElementById('gestaoModelModalTitleInput');
2453        if (editor) editor.innerHTML = (item && item.content) || '';
2454        if (titleInput && item && item.title && !String(titleInput.value || '').trim()) {
2455            titleInput.value = item.title;
2456        }
2457        gestaoModelModalSourceName = (item && item.sourceFileName) || (file && file.name) || '';
2458        gestaoModelModalSourceMime = (item && item.sourceMimeType) || (file && file.type) || '';
2459        const fileLabel = document.getElementById('gestaoModelModalFileLabel');
2460        const uploader = document.getElementById('gestaoModelModalUploader');
2461        if (fileLabel) fileLabel.textContent = gestaoModelModalSourceName;
2462        if (status) {
2463            status.textContent = __isEN
2464                ? 'File loaded in the editor. Review and apply.'
2465                : 'Arquivo carregado no editor. Revise e aplique.';
2466        }
2467        if (uploader) uploader.classList.add('has-file');
2468        setGestaoModelModalSelection(null);
2469        syncGestaoModelEditorEmptyState();
2470    } catch (err) {
2471        if (status) status.textContent = '';
2472        funcMsg(
2473            'error',
2474            __L.attn,
2475            err && err.message
2476                ? err.message
2477                : (__isEN ? 'Could not import this file.' : 'Não foi possível importar este arquivo.'),
2478            '',
2479            '',
2480            undefined,
2481            gestaoInsightOptions('instruction_model_import_failed', {
2482                errorCode: 'model:import_failed'
2483            })
2484        );
2485    } finally {
2486        if (applyBtn) applyBtn.disabled = false;
2487    }
2488}
2489
2490async function persistGestaoModelBankEntry(title, content) {
2491    if (!title || !content || typeof window.cjErpSaveOrgModel !== 'function') return null;
2492    try {
2493        const item = await window.cjErpSaveOrgModel({
2494            title,
2495            content,
2496            contentFormat: 'html',
2497            sourceFileName: gestaoModelModalSourceName,
2498            sourceMimeType: gestaoModelModalSourceMime
2499        });
2500        if (!item) return null;
2501        upsertGestaoModelCache(item);
2502        if (typeof window.cjErpNotifyModelsChanged === 'function') {
2503            window.cjErpNotifyModelsChanged();
2504        }
2505        return {
2506            id: String(item.id || item._id || ''),
2507            title: item.title || title
2508        };
2509    } catch (err) {
2510        console.error('Error saving assessor model bank entry:', err);
2511        throw err;
2512    }
2513}
2514
2515async function saveGestaoModelModal() {
2516    if (!gestaoModelModalRowId) return;
2517    const row = gestaoQuery(`.processo-checkbox[value="${gestaoModelModalRowId}"]`)?.closest('tr');
2518    const titleInput = document.getElementById('gestaoModelModalTitleInput');
2519    const editor = document.getElementById('gestaoModelModalEditor');
2520    const applyBtn = document.getElementById('gestaoModelModalApply');
2521    if (!row || !titleInput || !editor) return;
2522
2523    const selectedId = gestaoModelModalSelectedId;
2524    const title = titleInput.value.trim();
2525    const html = String(editor.innerHTML || '').trim();
2526    const hasContent = gestaoModelEditorHasContent(editor);
2527
2528    if (selectedId) {
2529        applyGestaoModelToRow(row, selectedId, title);
2530        closeGestaoModelModal();
2531        return;
2532    }
2533
2534    if (title && hasContent) {
2535        if (applyBtn) {
2536            applyBtn.disabled = true;
2537            applyBtn.textContent = __isEN ? 'Saving...' : 'Salvando...';
2538        }
2539        try {
2540            const saved = await persistGestaoModelBankEntry(title, html);
2541            if (!saved || !saved.id) {
2542                throw new Error(__isEN ? 'Could not save this template.' : 'Não foi possível salvar este modelo.');
2543            }
2544            applyGestaoModelToRow(row, saved.id, saved.title);
2545            closeGestaoModelModal();
2546        } catch (err) {
2547            funcMsg(
2548                'error',
2549                __L.attn,
2550                err && err.message
2551                    ? err.message
2552                    : (__isEN ? 'Could not save this template.' : 'Não foi possível salvar este modelo.'),
2553                '',
2554                '',
2555                undefined,
2556                gestaoInsightOptions('instruction_model_save_failed', {
2557                    errorCode: 'model:save_failed'
2558                })
2559            );
2560        } finally {
2561            if (applyBtn) {
2562                applyBtn.disabled = false;
2563                applyBtn.textContent = __isEN ? 'Apply' : 'Aplicar';
2564            }
2565        }
2566        return;
2567    }
2568
2569    applyGestaoModelToRow(row, '', '');
2570    closeGestaoModelModal();
2571}
2572
2573function gestaoRowIsBusy(p) {
2574    return gestaoEffectiveStatus(p) === 'processando';
2575}
2576
2577function applyGestaoRowBusyState(row, p) {
2578    if (!row || !p) return;
2579    const busy = gestaoRowIsBusy(p);
2580    const checkbox = row.querySelector('.processo-checkbox');
2581    const pecaSelect = row.querySelector('.row-peca-select');
2582    const deleteBtn = row.querySelector('.btn-delete-processo');
2583    const pecaIcon = row.querySelector('.peca-view-icon');
2584    const pecaBadge = row.querySelector('.peca-view-badge');
2585
2586    const promptInput = row.querySelector('.row-additional-prompt');
2587    const promptBtn = row.querySelector('.row-additional-prompt-btn');
2588    const pagesInput = row.querySelector('.row-include-pages');
2589    const anonCheckbox = row.querySelector('.row-anonymization');
2590    const modelBtn = row.querySelector('.row-model-btn');
2591
2592    if (checkbox) checkbox.disabled = busy;
2593    if (pecaSelect) pecaSelect.disabled = busy;
2594    if (promptInput) promptInput.disabled = busy;
2595    if (promptBtn) promptBtn.disabled = busy;
2596    if (pagesInput) pagesInput.disabled = busy;
2597    if (anonCheckbox) anonCheckbox.disabled = busy;
2598    if (modelBtn) {
2599        modelBtn.disabled = busy || !gestaoModelsCanEdit();
2600        updateGestaoModelCell(row);
2601    }
2602    if (deleteBtn) {
2603        deleteBtn.disabled = busy;
2604        deleteBtn.style.opacity = busy ? '0.5' : '';
2605        deleteBtn.style.cursor = busy ? 'not-allowed' : 'pointer';
2606    }
2607    if (pecaIcon) {
2608        if (busy) {
2609            pecaIcon.style.pointerEvents = 'none';
2610            pecaIcon.style.opacity = '0.3';
2611            pecaIcon.removeAttribute('onclick');
2612        }
2613    }
2614    if (pecaBadge && busy) {
2615        pecaBadge.style.pointerEvents = 'none';
2616        pecaBadge.removeAttribute('onclick');
2617    }
2618    applyGestaoPromptAvailability(row);
2619}
2620
2621function applyGestaoRowSelections(processos) {
2622    const currentIds = new Set((processos || []).map(gestaoProcessoI
2622d).filter(Boolean));
2623    Array.from(gestaoRowSelections.keys()).forEach(id => {
2624        if (!currentIds.has(id)) gestaoRowSelections.delete(id);
2625    });
2626    Array.from(gestaoRowParamOverrides.keys()).forEach(id => {
2627        if (!currentIds.has(id)) gestaoRowParamOverrides.delete(id);
2628    });
2629    (processos || []).forEach(p => {
2630        const id = gestaoProcessoId(p);
2631        const selection = getGestaoRowSelection(id);
2632        p._selected = !!(selection.selected && selection.peca);
2633        p._selectedPeca = selection.peca || '';
2634    });
2635}
2636
2637function getGestaoPieceStatus() {
2638    const rows = gestaoQueryAll('#tabelaProcessosBody tr').filter(row => !!getGestaoRowId(row));
2639    const withPiece = rows.filter(row => rowHasPecaGestao(row)).length;
2640    return {
2641        withPiece,
2642        withoutPiece: Math.max(0, rows.length - withPiece)
2643    };
2644}
2645
2646function buildGestaoInsightContext(action, extra) {
2647    const personaSelect = gestaoGetById('personaSelectGestao');
2648    const checked = gestaoQueryAll('.processo-checkbox:checked');
2649    const context = {
2650        flowStep: 'gestao_processual',
2651        action,
2652        personaSelected: !!(personaSelect && personaSelect.value),
2653        selectedCount: checked.length,
2654        processCount: processosDataGestao.length,
2655        fileCount: selectedUploadFilesGestao.length,
2656        mainUploadFileCount: Array.isArray(selectedFiles) ? selectedFiles.length : 0,
2657        pieceStatus: getGestaoPieceStatus()
2658    };
2659    return Object.assign(context, extra || {});
2660}
2661
2662function gestaoInsightOptions(action, extra) {
2663    const opts = extra || {};
2664    return {
2665        notifyAdmin: opts.notifyAdmin !== false,
2666        source: opts.source || 'assessor-ui',
2667        category: opts.category || 'validation',
2668        flowStep: opts.flowStep || 'gestao_processual',
2669        action,
2670        errorCode: opts.errorCode,
2671        requestId: opts.requestId,
2672        operationId: opts.operationId,
2673        context: buildGestaoInsightContext(action, opts.context)
2674    };
2675}
2676
2677function gestaoTableHasDataRows(tbody) {
2678    if (!tbody) return false;
2679    return !!tbody.querySelector('tr');
2680}
2681
2682function gestaoProcessosTableContainer() {
2683    const tbody = gestaoGetById('tabelaProcessosBody');
2684    return tbody && tbody.closest('.gestao-processual-table-container');
2685}
2686
2687function gestaoProcessosTableScrollEl() {
2688    return gestaoQuery('.gestao-processual-table-scroll');
2689}
2690
2691function gestaoProcessosTableHTrack() {
2692    return gestaoQuery('.gp-table-htrack');
2693}
2694
2695function updateGestaoProcessosTableOverflow() {
2696    const scroller = gestaoProcessosTableScrollEl();
2697    const wrap = scroller && scroller.closest('.gestao-processual-table-scroll-wrap');
2698    const track = gestaoProcessosTableHTrack();
2699    const thumb = track && track.querySelector('.gp-table-hthumb');
2700    if (!scroller || !wrap) return;
2701    const maxScroll = scroller.scrollWidth - scroller.clientWidth;
2702    const overflowing = maxScroll > 1;
2703    wrap.classList.toggle('can-scroll-right', overflowing && scroller.scrollLeft < maxScroll - 1);
2704    wrap.classList.toggle('can-scroll-left', overflowing && scroller.scrollLeft > 1);
2705    if (!track) return;
2706    const overlayOn = wrap.closest('.gestao-processual-table-container');
2707    const hideTrack = !overflowing || (overlayOn && (overlayOn.classList.contains('is-loading') || overlayOn.classList.contains('is-empty')));
2708    track.hidden = hideTrack;
2709    if (hideTrack || !thumb) return;
2710    const vBar = Math.max(0, scroller.offsetWidth - scroller.clientWidth);
2711    track.style.right = vBar ? vBar + 'px' : '0px';
2712    const trackW = track.clientWidth;
2713    const thumbW = Math.max(32, Math.round(trackW * (scroller.clientWidth / scroller.scrollWidth)));
2714    const maxThumbLeft = Math.max(0, trackW - thumbW);
2715    const left = maxScroll > 0 ? Math.round((scroller.scrollLeft / maxScroll) * maxThumbLeft) : 0;
2716    thumb.style.width = thumbW + 'px';
2717    thumb.style.transform = 'translateX(' + left + 'px)';
2718    thumb.setAttribute('aria-valuemin', '0');
2719    thumb.setAttribute('aria-valuemax', String(Math.round(maxScroll)));
2720    thumb.setAttribute('aria-valuenow', String(Math.round(scroller.scrollLeft)));
2721}
2722
2723function bindGestaoProcessosTableHTrack() {
2724    const scroller = gestaoProcessosTableScrollEl();
2725    const track = gestaoProcessosTableHTrack();
2726    const thumb = track && track.querySelector('.gp-table-hthumb');
2727    if (!scroller || !track || !thumb || track.dataset.gpHtrackBound === 'true') return;
2728    track.dataset.gpHtrackBound = 'true';
2729    let dragging = false;
2730    let dragStartX = 0;
2731    let dragStartScroll = 0;
2732
2733    function maxScrollLeft() {
2734        return Math.max(0, scroller.scrollWidth - scroller.clientWidth);
2735    }
2736
2737    function maxThumbLeft() {
2738        return Math.max(0, track.clientWidth - thumb.offsetWidth);
2739    }
2740
2741    track.addEventListener('pointerdown', function (event) {
2742        if (event.button != null && event.button !== 0) return;
2743        const maxScroll = maxScrollLeft();
2744        if (maxScroll <= 0) return;
2745        if (event.target !== thumb) {
2746            const rect = track.getBoundingClientRect();
2747            const ratio = (event.clientX - rect.left - (thumb.offsetWidth / 2)) / Math.max(1, track.clientWidth - thumb.offsetWidth);
2748            scroller.scrollLeft = Math.max(0, Math.min(maxScroll, ratio * maxScroll));
2749        }
2750        dragging = true;
2751        dragStartX = event.clientX;
2752        dragStartScroll = scroller.scrollLeft;
2753        thumb.classList.add('is-dragging');
2754        try { track.setPointerCapture(event.pointerId); } catch (e) { /* ignore */ }
2755        event.preventDefault();
2756    });
2757
2758    track.addEventListener('pointermove', function (event) {
2759        if (!dragging) return;
2760        const span = maxThumbLeft();
2761        if (span <= 0) return;
2762        const delta = event.clientX - dragStartX;
2763        scroller.scrollLeft = dragStartScroll + (delta / span) * maxScrollLeft();
2764    });
2765
2766    function stopDrag(event) {
2767        if (!dragging) return;
2768        dragging = false;
2769        thumb.classList.remove('is-dragging');
2770        if (event && event.pointerId != null) {
2771            try { track.releasePointerCapture(event.pointerId); } catch (e) { /* ignore */ }
2772        }
2773    }
2774
2775    track.addEventListener('pointerup', stopDrag);
2776    track.addEventListener('pointercancel', stopDrag);
2777}
2778
2779function bindGestaoProcessosTableOverflow() {
2780    const scroller = gestaoProcessosTableScrollEl();
2781    if (!scroller) return;
2782    bindGestaoProcessosTableHTrack();
2783    if (scroller.dataset.gpOverflowBound === 'true') {
2784        updateGestaoProcessosTableOverflow();
2785        return;
2786    }
2787    scroller.dataset.gpOverflowBound = 'true';
2788    scroller.addEventListener('scroll', updateGestaoProcessosTableOverflow, { passive: true });
2789    if (typeof ResizeObserver === 'function') {
2790        const observer = new ResizeObserver(updateGestaoProcessosTableOverflow);
2791        observer.observe(scroller);
2792        const table = scroller.querySelector('.gestao-processual-table');
2793        if (table) observer.observe(table);
2794    }
2795    window.addEventListener('resize', updateGestaoProcessosTableOverflow);
2796    updateGestaoProcessosTableOverflow();
2797}
2798
2799function scheduleGestaoProcessosTableOverflowUpdate() {
2800    bindGestaoProcessosTableOverflow();
2801    if (typeof requestAnimationFrame === 'function') {
2802        requestAnimationFrame(updateGestaoProcessosTableOverflow);
2803    } else {
2804        updateGestaoProcessosTableOverflow();
2805    }
2806}
2807
2808function setGestaoProcessosTableOverlay(mode, text) {
2809    const container = gestaoProcessosTableContainer();
2810    const overlay = gestaoGetById('gestaoProcessosTableOverlay');
2811    if (!container || !overlay) return;
2812    container.classList.toggle('is-loading', mode === 'loading');
2813    container.classList.toggle('is-empty', mode === 'empty');
2814    if (mode === 'hide') {
2815        overlay.innerHTML = '';
2816        overlay.setAttribute('aria-hidden', 'true');
2817        scheduleGestaoProcessosTableOverflowUpdate();
2818        return;
2819    }
2820    overlay.removeAttribute('aria-hidden');
2821    if (mode === 'loading') {
2822        overlay.innerHTML = '<span class="gp-table-loading"><i class="fa fa-spinner fa-spin" aria-hidden="true"></i> '
2823            + (__isEN ? 'Loading cases...' : 'Carregando processos...')
2824            + '</span>';
2825        scheduleGestaoProcessosTableOverflowUpdate();
2826        return;
2827    }
2828    overlay.innerHTML = '<span class="gp-table-empty">' + escapeGestaoHtml(text || '') + '</span>';
2829    scheduleGestaoProcessosTableOverflowUpdate();
2830}
2831
2832function showGestaoProcessosTableLoading() {
2833    const tbody = gestaoGetById('tabelaProcessosBody');
2834    if (!tbody || gestaoTableHasDataRows(tbody)) return;
2835    setGestaoProcessosTableOverlay('loading');
2836}
2837
2838function showGestaoProcessosTableMessage(text) {
2839    const tbody = gestaoGetById('tabelaProcessosBody');
2840    if (!tbody || gestaoTableHasDataRows(tbody)) return;
2841    tbody.innerHTML = '';
2842    setGestaoProcessosTableOverlay('empty', text);
2843}
2844
2845// Load processos from API
2846async function loadProcessosGestao() {
2847    const token = localStorage.getItem('CogniJUS_User_Token');
2848    if (!token) return;
2849
2850    const tbody = gestaoGetById('tabelaProcessosBody');
2851    if (tbody && !gestaoTableHasDataRows(tbody)) showGestaoProcessosTableLoading();
2852
2853    try {
2854        const orgId = getErpActiveOrgId();
2855        const response = await fetch(base_url_gestao + '/processos/list', {
2856            method: 'POST',
2857            headers: { 'x-access-token': token, 'Content-Type': 'application/json' },
2858            body: JSON.stringify(orgId ? { organizationId: orgId } : {})
2859        });
2860        const data = await response.json();
2861
2862        if (!response.ok || !Array.isArray(data.processos)) {
2863            showGestaoProcessosTableMessage(
2864                __isEN ? 'Could not load the cases.' : 'Não foi possível carregar os processos.'
2865            );
2866            return;
2867        }
2868
2869        if (data.processos) {
2870            const prevFilteredIds = getFilteredSortedProcessosGestao(processosDataGestao).map(gestaoProcessoI
2870d);
2871            const prevSnapshot = new Map(
2872                processosDataGestao.map(p => [
2873                    gestaoProcessoId(p),
2874                    {
2875                        status: p.status,
2876                        pecaCount: gestaoPecaCount(p)
2877                    }
2878                ])
2879            );
2880
2881            data.processos.forEach(p => {
2882                const id = gestaoProcessoId(p);
2883                const prev = prevSnapshot.get(id);
2884                const pecaCount = gestaoPecaCount(p);
2885
2886                if (gestaoWatchedJobs.has(id)) {
2887                    gestaoProcessWatchedJob(p);
2888                    return;
2889                }
2890
2891                if (!prev) return;
2892
2893                const status = p.status || 'pendente';
2894                const wasActive = prev.status === 'pendente' || prev.status === 'processando';
2895                if (prev.status !== status && wasActive) {
2896                    const copy = gestaoStatusToastCopy(status, gestaoDisplayFilename(p.filename));
2897                    if (status === 'concluido' && pecaCount > prev.pecaCount && pecaCount > 0) {
2898                        gestaoNotifyCompleted(p);
2899                    } else if (status === 'erro') {
2900                        gestaoNotifyJobError(p, gestaoDisplayFilename(p.filename));
2901                    } else if (status !== 'concluido') {
2902                        showGestaoToast(copy.title, copy.msg);
2903                    }
2904                    return;
2905                }
2906
2907                if (wasActive && status === 'concluido' && pecaCount > prev.pecaCount) {
2908                    gestaoNotifyCompleted(p);
2909                }
2910            });
2911
2912            applyGestaoRowSelections(data.processos);
2913            processosDataGestao = data.processos;
2914            const nextFilteredIds = getFilteredSortedProcessosGestao(processosDataGestao).map(gestaoProcessoId);
2915            const tbody = gestaoGetById('tabelaProcessosBody');
2916            const tbodyMissingDataRows = !!(tbody && !gestaoTableHasDataRows(tbody));
2917            const needsFullRender = gestaoNeedsFullRenderAfterInteraction
2918                || gestaoProcessoIdsChanged(prevFilteredIds, nextFilteredIds)
2919                || tbodyMissingDataRows;
2920            const tableInteractionActive = gestaoIsTableInteractionActive();
2921            if (needsFullRender && !tableInteractionActive) {
2922                applyFiltersAndRenderGestao();
2923                gestaoNeedsFullRenderAfterInteraction = false;
2924            } else {
2925                if (needsFullRender) gestaoNeedsFullRenderAfterInteraction = true;
2926                patchGestaoTableRowsInPlace();
2927            }
2928
2929            const hasPendingJobs = data.processos.some(
2930                p => p.status === 'pendente' || p.status === 'processando'
2931            );
2932            if (hasPendingJobs && !autoRefreshInterval) {
2933                autoRefreshInterval = setInterval(() => {
2934                    loadProcessosGestao();
2935                }, 5000);
2936            } else if (!hasPendingJobs && autoRefreshInterval) {
2937                clearInterval(autoRefreshInterval);
2938                autoRefreshInterval = null;
2939            }
2940        }
2941    } catch (error) {
2942        console.error('Erro ao listar processos:', error);
2943        showGestaoProcessosTableMessage(
2944            __isEN ? 'Could not load the cases.' : 'Não foi possível carregar os processos.'
2945        );
2946    }
2947}
2948
2949function compareProcessosGestao(a, b, field, direction) {
2950    let result = 0;
2951    switch (field) {
2952        case 'filename':
2953            result = (a.filename || '').localeCompare(b.filename || '');
2954            break;
2955        case 'fileSize':
2956            result = (a.fileSize || 0) - (b.fileSize || 0);
2957            break;
2958        case 'status':
2959            result = (a.status || '').localeCompare(b.status || '');
2960            break;
2961        case 'uploadDate':
2962            result = new Date(a.uploadDate) - new Date(b.uploadDate);
2963            break;
2964    }
2965    return direction === 'desc' ? -result : result;
2966}
2967
2968function updateGestaoSortIndicators() {
2969    gestaoQueryAll('.gestao-processual-table th.sortable').forEach(th => {
2970        const field = th.dataset.sortField;
2971        const icon = th.querySelector('.sort-icon');
2972        if (!icon) return;
2973        icon.classList.remove('fa-sort', 'fa-sort-asc', 'fa-sort-desc');
2974        if (field === gestaoSortState.field) {
2975            icon.classList.add(gestaoSortState.direction === 'asc' ? 'fa-sort-asc' : 'fa-sort-desc');
2976        } else {
2977            icon.classList.add('fa-sort');
2978        }
2979    });
2980}
2981
2982function handleGestaoColumnSort(field) {
2983    if (gestaoSortState.field === field) {
2984        gestaoSortState.direction = gestaoSortState.direction === 'asc' ? 'desc' : 'asc';
2985    } else {
2986        gestaoSortState.field = field;
2987        gestaoSortState.direction = 'asc';
2988    }
2989    applyFiltersAndRenderGestao();
2990}
2991
2992function applyFiltersAndRenderGestao(fromSearch) {
2993    if (!gestaoGetById('buscaProcesso')) return;
2994    const filtrados = getFilteredSortedProcessosGestao(processosDataGestao);
2995    if (fromSearch) gestaoExpandUnifiedSearchHits(filtrados);
2996
2997    syncAllGestaoRowSelectionsFromDom();
2998    syncAllGestaoRowParamsFromDom();
2999    renderTabelaProcessosGestao(filtrados);
3000    updateGestaoSortIndicators();
3001}
3002
3003// Estado do caret de "Processo unificado". A tabela é recriada a cada refresh
3004// (polling de status), então o que está aberto precisa sobreviver ao render.
3005const gestaoExpandedPartsRows = new Set();
3006const gestaoSearchExpandedPartsRows = new Set();
3007
3008function buildGestaoPartsDetailRow(processoId, parts) {
3009    const row = document.createElement('tr');
3010    row.className = 'gp-autos-detail-row';
3011    row.dataset.partsFor = processoId;
3012    const searchTerm = gestaoGetSearchTerm();
3013
3014    const items = parts.map((part, position) => {
3015        const pagesLabel = gestaoPartPagesLabel(part);
3016        const sizeLabel = part.size
3017            ? (part.size >= 1048576 ? (part.size / 1048576).toFixed(1) + ' MB' : (part.size / 1024).toFixed(0) + ' KB')
3018            : '';
3019        const wordBadge = part.convertedFromWord
3020            ? `<span class="gp-autos-detail-badge" title="${escapeGestaoHtml(__isEN ? 'Some component files were converted from Word to PDF.' : 'Há arquivos componentes convertidos de Word para PDF.')}">Word</span>`
3021            : '';
3022        const meta = [pagesLabel, sizeLabel].filter(Boolean)
3023            .map(text => `<span class="gp-autos-detail-meta">${escapeGestaoHtml(text)}</span>`)
3024            .join('');
3025        return `<li class="gp-autos-detail-item">
3026                    <span class="gp-autos-detail-order">${position + 1}</span>
3027                    <span class="gp-autos-detail-name" title="${escapeGestaoHtml(part.originalName)}">${gestaoPartNameHtml(part, searchTerm)}</span>
3028                    ${wordBadge}${meta}
3029                </li>`;
3030    }).join('');
3031
3032    const hint = __isEN
3033        ? 'Use @ in Instructions to point the AI at a specific file.'
3034        : 'Use @ em Instruções para direcionar a IA a um arquivo específico.';
3035
3036    row.innerHTML = `<td colspan="12" class="gp-autos-detail-cell">
3037            <div class="gp-autos-detail-title">${__isEN ? 'Files in this unified case' : 'Arquivos que formam este processo unificado'}</div>
3038            <ul class="gp-autos-detail-list">${items}</ul>
3039            <div class="gp-autos-detail-hint">${hint}</div>
3040        </td>`;
3041    return row;
3042}
3043
3044function toggleGestaoPartsRow(processoId, row) {
3045    if (!processoId || !row) return;
3046    const toggle = row.querySelector('.gp-autos-toggle');
3047    const existing = row.nextElementSibling;
3048    const isOpen = existing && existing.classList.contains('gp-autos-detail-row');
3049
3050    if (isOpen) {
3051        existing.remove();
3052        gestaoExpandedPartsRows.delete(processoId);
3053        gestaoSearchExpandedPartsRows.delete(processoId);
3054        if (toggle) {
3055            toggle.classList.remove('is-open');
3056            toggle.setAttribute('aria-expanded', 'false');
3057        }
3058        return;
3059    }
3060
3061    const processo = processosDataGestao.find(p => gestaoProcessoId(p) === processoId);
3062    const parts = gestaoResolveMergedParts(processo);
3063    if (!parts.length) return;
3064    row.parentNode.insertBefore(buildGestaoPartsDetailRow(processoId, parts), row.nextSibling);
3065    gestaoExpandedPartsRows.add(processoId);
3066    if (toggle) {
3067        toggle.classList.add('is-open');
3068        toggle.setAttribute('aria-expanded', 'true');
3069    }
3070}
3071
3072function renderTabelaProcessosGestao(processos) {
3073    const tbody = gestaoGetById('tabelaProcessosBody');
3074    if (!tbody) return;
3075
3076    if (processos.length === 0) {
3077        tbody.innerHTML = '';
3078        setGestaoProcessosTableOverlay(
3079            'empty',
3080            __isEN ? 'No cases found.' : 'Nenhum processo encontrado.'
3081        );
3082        return;
3083    }
3084
3085    setGestaoProcessosTableOverlay('hide');
3086
3087    const fragment = document.createDocumentFragment();
3088    const searchTerm = gestaoGetSearchTerm();
3089
3090    processos.forEach(p => {
3091        const row = document.createElement('tr');
3092        const processoId = gestaoProcessoId(p);
3093        const dataUpload = new Date(p.uploadDate).toLocaleDateString(__isEN ? 'en-US' : 'pt-BR');
3094        const displayStatus = gestaoEffectiveStatus(p);
3095        const statusClass = getStatusClassGestao(displayStatus);
3096        const statusLabel = getStatusLabelGestao(displayStatus);
3097        const selection = getGestaoRowSelection(processoId);
3098        const isChecked = selection.selected && selection.peca ? 'checked' : '';
3099
3100        const isBusy = gestaoRowIsBusy(p);
3101        const isProcessing = isBusy;
3102        const jobErrorMessage = p.status === 'erro' ? gestaoResolveJobErrorMessage(p) : '';
3103        const errorTitle = jobErrorMessage ? `title="${escapeGestaoHtml(jobErrorMessage).replace(/"/g, '&quot;')}"` : '';
3104        const errorHint = jobErrorMessage
3105            ? `<div class="gp-status-error-hint">${escapeGestaoHtml(jobErrorMessage.length > 140 ? jobErrorMessage.slice(0, 137) + '...' : jobErrorMessage)}</div>`
3106            : '';
3107        const busyAttr = isBusy ? 'disabled' : '';
3108        const deleteBusyStyle = isBusy ? 'opacity: 0.5; cursor: not-allowed;' : '';
3109
3110        // Processo compartilhado pelo dono da Persona: colaborador vê e gera peças,
3111        // mas não pode excluir o que não subiu.
3112        const ownedByMe = p.ownedByMe !== false;
3113        const sharedBadge = !ownedByMe
3114            ? `<span class="gp-shared-badge" title="${__isEN ? 'Shared by the Persona owner' : 'Compartilhado pelo dono da Persona'}"><i class="fa fa-users" aria-hidden="true"></i></span>`
3115            : '';
3116        const deleteCell = ownedByMe
3117            ? `<button class="btn-icon btn-delete-processo" style="color: #d32f2f; background: none; border: 1px solid #e2e8f0; padding: 6px; border-radius: 4px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; ${deleteBusyStyle}" title="Excluir Processo" onclick="deleteProcessGestao('${processoId}')" ${busyAttr}>
3118                        <i class="fa fa-trash"></i>
3119                    </button>`
3120            : `<span class="btn-icon" style="color:#cbd5e1; background:none; border:1px solid #eef2f7; padding:6px; border-radius:4px; display:inline-flex; align-items:center; justify-content:center; cursor:not-allowed;" title="${__isEN ? 'Only the owner can delete this case' : 'Somente o dono pode excluir este processo'}">
3121                        <i class="fa fa-lock"></i>
3122                    </span>`;
3123
3124        // Verifica se veio da Chrome Extension
3125        const isFromExtension = p.source === 'chrome-extension';
3126        const hasRealFile = p.fileId && !p.filename?.endsWith('.txt');
3127        const canDownload = !!hasRealFile;
3128        const fileIcon = isFromExtension ? 'fa-chrome' : 'fa-file-pdf-o';
3129        const fileColor = canDownload ? '#007bff' : '#64748b';
3130        const fileCursor = canDownload ? 'pointer' : 'default';
3131        const fileOnclick = canDownload ? `onclick="verProcessoGestao('${p.fileId}')"` : '';
3132        const fileTitle = canDownload ? (isFromExtension ? 'Ver PDF (via PJe Extension)' : 'Ver PDF') : 'Sem PDF disponível';
3133        const extensionBadge = isFromExtension ? '<span style="font-size: 0.7rem; color: #94a3b8; margin-left: 4px;">(Ext)</span>' : '';
3134
3135        // Parâmetros herdados (ex.: da análise individual do mesmo PDF).
3136        // Exibidos como colunas editáveis para o usuário ajustar ou remover
3137        // antes de gerar a peça em lote.
3138        const escGpAttr = (s) => String(s == null ? '' : s)
3139            .replace(/&/g, '&amp;').replace(/"/g, '&quot;')
3140            .replace(/</g, '&lt;').replace(/>/g, '&gt;');
3141        const paramOverride = gestaoRowParamOverrides.get(processoId);
3142        const inheritedPrompt = escGpAttr(paramOverride ? paramOverride.additionalPrompt : (p.additionalPrompt || ''));
3143        const inheritedPromptTitle = escGpAttr(paramOverride ? paramOverride.additionalPromptTitle : (p.additionalPromptTitle || ''));
3144        const inheritedPages = escGpAttr(paramOverride ? paramOverride.includePages : (p.includePages || ''));
3145        const isAnon = paramOverride ? !!paramOverride.enableAnonymization : !!p.enableAnonymization;
3146        const rawPrompt = paramOverride ? paramOverride.additionalPrompt : (p.additionalPrompt || '');
3147        const rawPromptTitle = paramOverride ? paramOverride.additionalPromptTitle : (p.additionalPromptTitle || '');
3148        const promptSummary = escGpAttr(getGestaoPromptLabel(rawPrompt, rawPromptTitle));
3149        const promptIcon = String(rawPrompt || '').trim() ? 'fa-pencil' : 'fa-plus';
3150        const promptClass = String(rawPrompt || '').trim() ? ' has-prompt' : '';
3151        const inheritedModelId = escGpAttr(paramOverride ? (paramOverride.modelId || '') : (p.modelId || ''));
3152        const inheritedModelTitleRaw = paramOverride ? (paramOverride.modelTitle || '') : (p.modelTitle || '');
3153        const resolvedModelTitle = getGestaoModelTitle(inheritedModelId, inheritedModelTitleRaw);
3154        const inheritedModelTitle = escGpAttr(resolvedModelTitle);
3155        const modelSummary = escGpAttr(getGestaoModelLabel(inheritedModelId, resolvedModelTitle));
3156        const modelIcon = inheritedModelId ? 'fa-pencil' : 'fa-plus';
3157        const modelClass = inheritedModelId ? ' has-model' : '';
3158        const modelBusyAttr = (isBusy || !gestaoModelsCanEdit()) ? 'disabled' : '';
3159
3160        // Origem Word em upload não unificado: o nome exibido é o do PDF convert
3160ido.
3161        const wordOrigins = Array.isArray(p.convertedFromWord) ? p.convertedFromWord.filter(Boolean) : [];
3162        const wordBadge = wordOrigins.length
3163            ? `<span class="gp-autos-detail-badge" style="margin-left:4px;" title="${escapeGestaoHtml(__isEN ? 'Some component files were converted from Word to PDF.' : 'Há arquivos componentes convertidos de Word para PDF.')}">Word</span>`
3164            : '';
3165
3166        // Processo unificado: caret abre a linha de detalhe com cada arquivo de origem.
3167        const mergedParts = gestaoResolveMergedParts(p);
3168        const partsExpanded = gestaoExpandedPartsRows.has(processoId);
3169        const partsToggle = mergedParts.length
3170            ? `<button type="button" class="gp-autos-toggle${partsExpanded ? ' is-open' : ''}" aria-expanded="${partsExpanded ? 'true' : 'false'}" title="${__isEN ? 'Show the files that make up this unified case' : 'Ver os arquivos que formam este processo unificado'}"><span class="gp-autos-toggle-count">${mergedParts.length}</span><i class="fa fa-caret-down" aria-hidden="true"></i></button>`
3171            : '';
3172
3173        row.innerHTML = `
3174                <td><input type="checkbox" class="processo-checkbox" value="${processoId}" ${isChecked} ${busyAttr}></td>
3175                <td class="gp-status-cell"><span class="tag-status ${statusClass}" ${errorTitle}>${isProcessing ? '<i class="fa fa-spinner fa-spin"></i>' : ''}${statusLabel}</span>${errorHint}</td>
3176                <td class="gp-autos-cell ${canDownload ? 'clickable-cell' : ''}" ${fileOnclick} style="cursor: ${fileCursor}; color: ${fileColor}; font-weight: 600;" title="${fileTitle}">
3177                    <i class="fa ${fileIcon}" style="margin-right: 5px;"></i><span class="gp-autos-name">${highlightGestaoFilename(p.filename || 'Sem nome', searchTerm)}</span>${extensionBadge}${wordBadge}${sharedBadge}${partsToggle}
3178                </td>
3179                <td class="gp-size-cell">${buildGestaoSizeCellHtml(p)}</td>
3180                <td class="gp-peca-cell">
3181                    <select class="row-peca-select" ${busyAttr}>
3182                        <option value="">Selecione a peça</option>
3183                    </select>
3184                </td>
3185                <td class="gp-param-cell gp-param-cell--instruction">
3186                    <div class="row-additional-prompt-control">
3187                        <input type="hidden" class="row-additional-prompt" value="${inheritedPrompt}">
3188                        <input type="hidden" class="row-additional-prompt-title" value="${inheritedPromptTitle}">
3189                        <button type="button" class="row-additional-prompt-btn${promptClass}" title="${inheritedPromptTitle || inheritedPrompt || (__isEN ? 'Add or choose additional instructions' : 'Adicionar ou selecionar instruções adicionais')}" ${busyAttr}>
3190                            <i class="fa ${promptIcon}" aria-hidden="true"></i>
3191                        </button>
3192                        <span class="row-additional-prompt-label" title="${inheritedPromptTitle || inheritedPrompt}">${promptSummary}</span>
3193                    </div>
3194                </td>
3195                <td class="gp-param-cell gp-param-cell--model">
3196                    <div class="row-model-control">
3197                        <input type="hidden" class="row-model-id" value="${inheritedModelId}">
3198                        <input type="hidden" class="row-model-title" value="${inheritedModelTitle}">
3199                        <button type="button" class="row-model-btn${modelClass}" title="${inheritedModelTitle || (__isEN ? 'Add or choose a template' : 'Adicionar ou selecionar um modelo')}" ${modelBusyAttr}>
3200                            <i class="fa ${modelIcon}" aria-hidden="true"></i>
3201                        </button>
3202                        <span class="row-model-label" title="${inheritedModelTitle}">${modelSummary}</span>
3203                    </div>
3204                </td>
3205                <td class="gp-param-cell gp-param-cell--center gp-param-cell--pages">
3206                    <input type="text" class="row-include-pages gp-param-input gp-param-input--pages" value="${inheritedPages}" placeholder="${__isEN ? 'All' : 'Todas'}" ${busyAttr}>
3207                </td>
3208                <td class="gp-param-cell gp-param-cell--center">
3209                    <input type="checkbox" class="row-anonymization" ${isAnon ? 'checked' : ''} ${busyAttr}>
3210                </td>
3211                <td class="gp-generated-piece-cell" style="text-align: center;">
3212                    ${buildGestaoPecaViewCellHtml(p, processoId)}
3213                </td>
3214                <td>${dataUpload}</td>
3215                <td style="vertical-align: middle; text-align: center;">
3216                    ${deleteCell}
3217                </td>
3218            `;
3219        bindGestaoRowEvents(row);
3220        fragment.appendChild(row);
3221        if (mergedParts.length && partsExpanded) {
3222            fragment.appendChild(buildGestaoPartsDetailRow(processoId, mergedParts));
3223        }
3224    });
3225
3226    tbody.innerHTML = '';
3227    tbody.appendChild(fragment);
3228
3229    // Populate peca selects
3230    const personaSelect = gestaoGetById('personaSelectGestao');
3231    if (personaSelect) {
3232        populateRowPecaSelectsGestao(personaSelect.value);
3233    }
3234
3235    // Populate model selects (usa cache atual e recarrega em background).
3236    populateRowModelSelectsGestao();
3237    loadGestaoModels().then(() => populateRowModelSelectsGestao()).catch(() => { });
3238
3239    tbody.querySelectorAll('tr').forEach(updateProcessoRowHighlightGestao);
3240    scheduleGestaoProcessosTableOverflowUpdate();
3241}
3242
3243function updateProcessoRowHighlightGestao(row) {
3244    if (!row) return;
3245    const checkbox = row.querySelector('.processo-checkbox');
3246    row.classList.toggle('processo-row-selected', !!(checkbox && checkbox.checked));
3247}
3248
3249function rowHasPecaGestao(row) {
3250    const pecaSelect = row ? row.querySelector('.row-peca-select') : null;
3251    return !!(pecaSelect && pecaSelect.value);
3252}
3253
3254function bindGestaoRowEvents(row) {
3255    if (!row || row.dataset.gestaoEventsBound === 'true') return;
3256    row.dataset.gestaoEventsBound = 'true';
3257
3258    const pecaSelect = row.querySelector('.row-peca-select');
3259    const checkbox = row.querySelector('.processo-checkbox');
3260    const promptInput = row.querySelector('.row-additional-prompt');
3261    const promptBtn = row.querySelector('.row-additional-prompt-btn');
3262    const pagesInput = row.querySelector('.row-include-pages');
3263    const anonCheckbox = row.querySelector('.row-anonymization');
3264    const modelBtn = row.querySelector('.row-model-btn');
3265
3266    if (pecaSelect) {
3267        pecaSelect.addEventListener('mousedown', (e) => {
3268            if (personaSelectedGestao()) return;
3269            e.preventDefault();
3270            e.stopPropagation();
3271            pecaSelect.blur();
3272            warnPersonaRequiredGestao();
3273        });
3274        pecaSelect.addEventListener('focus', () => {
3275            if (gestaoBlockPecaFocus || personaSelectedGestao()) return;
3276            pecaSelect.blur();
3277            focusPersonaSelectGestao();
3278        });
3279        pecaSelect.addEventListener('change', () => {
3280            if (!personaSelectedGestao()) {
3281                revertPecaSelectGestao(pecaSelect);
3282                warnPersonaRequiredGestao();
3283                return;
3284            }
3285            if (checkbox) {
3286                setProcessoCheckboxGestao(checkbox, !!pecaSelect.value);
3287            } else {
3288                syncGestaoRowSelectionFromDom(row);
3289            }
3290            applyGestaoPromptAvailability(row);
3291            syncSelecionarTodosGestao();
3292        });
3293    }
3294
3295    if (checkbox) {
3296        checkbox.addEventListener('change', () => {
3297            if (checkbox.checked && !rowHasPecaGestao(row)) {
3298                setProcessoCheckboxGestao(checkbox, false);
3299                const personaSelect = gestaoGetById('personaSelectGestao');
3300                if (!personaSelect || !personaSelect.value) {
3301                    warnPersonaRequiredGestao();
3302                } else if (pecaSelect) {
3303                    highlightPecaSelectGestao(pecaSelect);
3304                }
3305                return;
3306            }
3307            setProcessoCheckboxGestao(checkbox, checkbox.checked);
3308            syncSelecionarTodosGestao();
3309        });
3310    }
3311
3312    [promptInput, pagesInput, anonCheckbox].forEach(el => {
3313        if (!el) return;
3314        el.addEventListener('change', () => syncGestaoRowParamsFromDom(row));
3315    });
3316    if (modelBtn) modelBtn.addEventListener('click', () => openGestaoModelModal(row));
3317    const modelLabel = row.querySelector('.row-model-label');
3318    if (modelLabel) {
3319        modelLabel.addEventListener('click', () => {
3320            if (modelBtn && modelBtn.disabled) return;
3321            openGestaoModelModal(row);
3322        });
3323    }
3324    // A célula "Autos" inteira abre o PDF, então o caret precisa parar a propagação.
3325    const partsToggleBtn = row.querySelector('.gp-autos-toggle');
3326    if (partsToggleBtn) {
3327        partsToggleBtn.addEventListener('click', (e) => {
3328            e.preventDefault();
3329            e.stopPropagation();
3330            toggleGestaoPartsRow(getGestaoRowId(row), row);
3331        });
3332    }
3333
3334    if (promptBtn) promptBtn.addEventListener('click', () => openGestaoPromptModal(row));
3335    if (promptInput) {
3336        promptInput.addEventListener('input', () => {
3337            syncGestaoRowParamsFromDom(row);
3338            updateGestaoPromptCell(row);
3339        });
3340    }
3341    [pagesInput].forEach(el => {
3342        if (!el) return;
3343        el.addEventListener('input', () => syncGestaoRowParamsFromDom(row));
3344    });
3345}
3346
3347function highlightPecaSelectGestao(select) {
3348    if (!select) return;
3349    select.classList.remove('row-peca-select--attention');
3350    void select.offsetWidth;
3351    select.classList.add('row-peca-select--attention');
3352    select.focus();
3353    select.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
3354    setTimeout(() => select.classList.remove('row-peca-select--attention'), 2500);
3355}
3356
3357function scrollToPersonaSelectGestao() {
3358    const personasSection = gestaoQuery('#meus-processos .personas-section') || gestaoGetById('personaSelectGestao');
3359    if (personasSection) {
3360        personasSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
3361    }
3362}
3363
3364let gestaoPersonaWarnTs = 0;
3365let gestaoBlockPecaFocus = false;
3366
3367function focusPersonaSelectGestao() {
3368    scrollToPersonaSelectGestao();
3369    const personaSelect = gestaoGetById('personaSelectGestao');
3370    if (!personaSelect) return;
3371    gestaoBlockPecaFocus = true;
3372    try {
3373        personaSelect.focus({ preventScroll: true });
3374    } catch (_) {
3375        personaSelect.focus();
3376    }
3377    requestAnimationFrame(() => {
3378        gestaoBlockPecaFocus = false;
3379    });
3380}
3381
3382function warnPersonaRequiredGestao() {
3383    const now = Date.now();
3384    focusPersonaSelectGestao();
3385    if (now - gestaoPersonaWarnTs < 800) return;
3386    gestaoPersonaWarnTs = now;
3387    funcMsg('warning', __L.attn, __isEN
3388        ? 'Please select a Persona before choosing a draft.'
3389        : 'Selecione um Perfil (Persona) antes de escolher a peça.', '', '', 'personaSelectGestao', gestaoInsightOptions('choose_piece_without_persona', {
3390            errorCode: 'validation:persona_required',
3391            context: { missingFields: ['persona'], trigger: 'piece_select' }
3392        }));
3393}
3394
3395function personaSelectedGestao() {
3396    const personaSelect = gestaoGetById('personaSelectGestao');
3397    return !!(personaSelect && personaSelect.value);
3398}
3399
3400function revertPecaSelectGestao(select) {
3401    if (!select) return;
3402    select.value = '';
3403    const row = select.closest('tr');
3404    const checkbox = row ? row.querySelector('.processo-checkbox') : null;
3405    if (checkbox) setProcessoCheckboxGestao(checkbox, false);
3406    if (row) syncGestaoRowSelectionFromDom(row);
3407    if (document.activeElement === select) select.blur();
3408    syncSelecionarTodosGestao();
3409}
3410
3411function setProcessoCheckboxGestao(checkbox, checked) {
3412    if (!checkbox) return;
3413    checkbox.checked = checked;
3414    const row = checkbox.closest('tr');
3415    updateProcessoRowHighlightGestao(row);
3416    syncGestaoRowSelectionFromDom(row);
3417}
3418
3419function syncSelecionarTodosGestao() {
3420    updateGerarPecasButtonText();
3421    const checkboxSelecionarTodos = gestaoGetById('selecionarTodos');
3422    const checkboxes = gestaoQueryAll('.processo-checkbox');
3423    if (!checkboxSelecionarTodos || checkboxes.length === 0) return;
3424    checkboxSelecionarTodos.checked = Array.from(checkboxes).every(cb => cb.checked);
3425    checkboxSelecionarTodos.indeterminate = !checkboxSelecionarTodos.checked &&
3426        Array.from(checkboxes).some(cb => cb.checked);
3427}
3428
3429function getGestaoPersonaPlaceholder() {
3430    return __isEN ? 'Select a persona' : 'Selecione uma persona';
3431}
3432
3433function populatePersonaSelectGestaoOptions(select, personas) {
3434    if (!select) return;
3435    const previousValue = select.value;
3436    select.innerHTML = '';
3437    const emptyOption = document.createElement('option');
3438    emptyOption.value = '';
3439    emptyOption.textContent = getGestaoPersonaPlaceholder();
3440    select.appendChild(emptyOption);
3441    (personas || []).forEach(persona => {
3442        const option = document.createElement('option');
3443        option.value = persona;
3444        option.textContent = persona;
3445        select.appendChild(option);
3446    });
3447    if (previousValue && personas.includes(previousValue)) {
3448        select.value = previousValue;
3449    }
3450}
3451
3452function getErpPersonaPolicyGestao() {
3453    if (typeof window.cjErpPersonaPolicy !== 'function') return null;
3454    try {
3455        return window.cjErpPersonaPolicy();
3456    } catch (_) {
3457        return null;
3458    }
3459}
3460
3461function applyErpPersonaPolicyGestao() {
3462    if (!gestaoAssessorConfigLoaded) return;
3463    const personaSelect = gestaoGetById('personaSelectGestao');
3464    if (!personaSelect) return;
3465
3466    const policy = getErpPersonaPolicyGestao();
3467    const allowed = policy && Array.isArray(policy.allowed) && policy.allowed.length
3468        ? policy.allowed.filter(persona => gestaoUniquePersonas.includes(persona))
3469        : gestaoUniquePersonas.slice();
3470    const selected = policy && policy.selected && allowed.includes(policy.selected) ? policy.selected : '';
3471    const locked = !!(policy && policy.locked && selected);
3472
3473    const previousValue = personaSelect.value;
3474    populatePersonaSelectGestaoOptions(personaSelect, allowed);
3475    if (selected) {
3476        personaSelect.value = selected;
3477    } else if (previousValue && allowed.includes(previousValue)) {
3478        personaSelect.value = previousValue;
3479    }
3480    personaSelect.disabled = locked;
3481    personaSelect.title = locked ? 'Persona definida pelo perfil da organização' : '';
3482    if (personaSelect.value !== previousValue) {
3483        personaSelect.dispatchEvent(new Event('change', { bubbles: true }));
3484    } else if (personaSelect.value) {
3485        populateRowPecaSelectsGestao(personaSelect.value);
3486    }
3487}
3488
3489async function loadGestaoAssessorConfig() {
3490    try {
3491        const url = '/extension/assessor-config' + (__isEN ? '?lang=en' : '');
3492        const response = await fetch(url);
3493        const data = await response.json();
3494        if (!response.ok || !data.success || !Array.isArray(data.buttonConfig)) {
3495            console.error('[Gestão Processual] erro ao carregar assessor-config');
3496            return;
3497        }
3498        buttonConfig = data.buttonConfig;
3499        gestaoUniquePersonas = [...new Set(buttonConfig.flatMap(c => (c.personas || []).map(p => p.label)))].sort();
3500        gestaoAssessorConfigLoaded = true;
3501        applyErpPersonaPolicyGestao();
3502    } catch (err) {
3503        console.error('[Gestão Processual] erro ao carregar assessor-config:', err);
3504    }
3505}
3506
3507window.cjApplyErpPersonaPolicy = applyErpPersonaPolicyGestao;
3508window.cjAssessorLoadProcessos = loadProcessosGestao;
3509
3510function gestaoPecaLabelFromButton(button) {
3511    if (!button) return '';
3512    const routeKey = String(button).split('-')[0];
3513    if (routeKey === 'outra') return __isEN ? 'Automatic' : 'Automática';
3514    if (routeKey === 'other') return 'Automatic';
3515    if (routeKey === 'livre') return __isEN ? 'Free' : 'Livre';
3516    if (routeKey === 'free') return 'Free';
3517    const config = buttonConfig;
3518    for (const c of config) {
3519        for (const persona of c.personas || []) {
3520            if (persona.button === button) return c.text;
3521        }
3522    }
3523    return button;
3524}
3525
3526function gestaoPecaRouteKey(value) {
3527    return String(value || '').split('-')[0] || '';
3528}
3529
3530function gestaoIsAutomaticPeca(value) {
3531    const key = gestaoPecaRouteKey(value);
3532    return key === 'outra' || key === 'other';
3533}
3534
3535function gestaoIsLivrePeca(value) {
3536    const key = gestaoPecaRouteKey(value);
3537    return key === 'livre' || key === 'free';
3538}
3539
3540function applyGestaoPromptAvailability(row) {
3541    if (!row) return;
3542    const pecaSelect = row.querySelector('.row-peca-select');
3543    const promptBtn = row.querySelector('.row-additional-prompt-btn');
3544    const promptInput = row.querySelector('.row-additional-prompt');
3545    const titleInput = row.querySelector('.row-additional-prompt-title');
3546    if (!pecaSelect || !promptBtn) return;
3547
3548    const peca = pecaSelect.value || '';
3549    const isAuto = gestaoIsAutomaticPeca(peca);
3550    const rowId = getGestaoRowId(row);
3551    const processo = processosDataGestao.find(item => gestaoProcessoId(item) === rowId);
3552    const busy = processo ? gestaoRowIsBusy(processo) : false;
3553
3554    if (isAuto) {
3555        if (promptInput && String(promptInput.value || '').trim()) {
3556            promptInput.value = '';
3557            if (titleInput) titleInput.value = '';
3558            syncGestaoRowParamsFromDom(row);
3559            updateGestaoPromptCell(row);
3560        }
3561        promptBtn.disabled = true;
3562        promptBtn.classList.add('is-disabled');
3563        promptBtn.title = __isEN
3564            ? 'Additional prompts are disabled for Automatic'
3565            : 'Prompts adicionais desativados na opção Automática';
3566        return;
3567    }
3568
3569    promptBtn.disabled = busy;
3570    promptBtn.classList.remove('is-disabled');
3571    const existingPrompt = promptInput ? String(promptInput.value || '').trim() : '';
3572    const existingTitle = titleInput ? String(titleInput.value || '').trim() : '';
3573    promptBtn.title = existingPrompt
3574        ? (existingTitle || existingPrompt)
3575        : (__isEN ? 'Add or choose additional instructions' : 'Adicionar ou selecionar instruções adicionais');
3576}
3577
3578function populateRowPecaSelectsGestao(persona) {
3579    const selects = gestaoQueryAll('.row-peca-select');
3580    const config = buttonConfig;
3581
3582    selects.forEach(select => {
3583        const row = select.closest('tr');
3584        const checkbox = row ? row.querySelector('.processo-checkbox') : null;
3585        const rowId = getGestaoRowId(row);
3586        const savedSelection = getGestaoRowSelection(rowId);
3587
3588        select.innerHTML = `<option value="">${__L.selectPiece2}</option>`;
3589
3590        if (persona && config.length > 0) {
3591            const availablePieces = config
3592                .filter(c => c.personas.some(p => p.label === persona))
3593                .map(c => ({
3594                    text: c.text,
3595                    button: c.personas.find(p => p.label === persona).button
3596                }))
3597                .sort((a, b) => a.text.localeCompare(b.text));
3598
3599            const _otherKey = __isEN ? 'other' : 'outra';
3600            const _livreKey = __isEN ? 'free' : 'livre';
3601            const sampleBtn = availablePieces.length > 0
3602                ? availablePieces[0].button.split('-').slice(1).join('-')
3603                : persona.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g, '_');
3604            const undefinedGroup = document.createElement('optgroup');
3605            undefinedGroup.label = __isEN ? 'Undefined' : 'Indefinida';
3606
3607            const otherOpt = document.createElement('option');
3608            otherOpt.value = _otherKey + '-' + sampleBtn;
3609            otherOpt.textContent = __isEN ? 'Automatic' : 'Automática';
3610            undefinedGroup.appendChild(otherOpt);
3611
3612            const livreOpt = document.createElement('option');
3613            livreOpt.value = _livreKey + '-' + sampleBtn;
3614            livreOpt.textContent = __isEN ? 'Free' : 'Livre';
3615            undefinedGroup.appendChild(livreOpt);
3616
3617            select.appendChild(undefinedGroup);
3618
3619            const definedGroup = document.createElement('optgroup');
3620            definedGroup.label = __isEN ? 'Defined' : 'Definida';
3621            availablePieces.forEach(piece => {
3622                const option = document.createElement('option');
3623                option.value = piece.button;
3624                option.textContent = piece.text;
3625                definedGroup.appendChild(option);
3626            });
3627            select.appendChild(definedGroup);
3628        }
3629
3630        const savedPecaExists = !!(savedSelection.peca &&
3631            Array.from(select.options).some(option => option.value === savedSelection.peca));
3632        if (savedPecaExists) {
3633            select.value = savedSelection.peca;
3634            if (checkbox) setProcessoCheckboxGestao(checkbox, !!savedSelection.selected);
3635        } else if (savedSelection.peca) {
3636            const staleOpt = document.createElement('option');
3637            staleOpt.value = savedSelection.peca;
3638            staleOpt.textContent = gestaoPecaLabelFromButton(savedSelection.peca);
3639            select.appendChild(staleOpt);
3640            select.value = savedSelection.peca;
3641            if (checkbox) setProcessoCheckboxGestao(checkbox, !!savedSelection.selected);
3642        } else {
3643            select.value = '';
3644            if (checkbox) {
3645                checkbox.checked = false;
3646                updateProcessoRowHighlightGestao(row);
3647            }
3648        }
3649        const processo = processosDataGestao.find(item => gestaoProcessoId(item) === rowId);
3650        if (processo) applyGestaoRowBusyState(row, processo);
3651        applyGestaoPromptAvailability(row);
3652        updateProcessoRowHighlightGestao(row);
3653    });
3654
3655    syncSelecionarTodosGestao();
3656}
3657
3658function gestaoIsInQueue(p) {
3659    if (!p || p.status !== 'pendente') return false;
3660    return p.source === 'gestao-lote' || p.source === 'analise-individual';
3661}
3662
3663function gestaoEffectiveStatus(p) {
3664    const status = p.status || 'pendente';
3665    if (status === 'processando' || gestaoIsInQueue(p)) return 'processando';
3666    return status;
3667}
3668
3669function getStatusClassGestao(status) {
3670    switch (status) {
3671        case 'concluido': return 'concluido';
3672        case 'processando': return 'processando';
3673        case 'erro': return 'erro';
3674        default: return 'pendente';
3675    }
3676}
3677
3678function getStatusLabelGestao(status) {
3679    if (__isEN) {
3680        switch (status) {
3681            case 'concluido': return 'Completed';
3682            case 'processando': return 'Processing';
3683            case 'erro': return 'Error';
3684            default: return 'Pending';
3685        }
3686    }
3687    switch (status) {
3688        case 'concluido': return 'Concluído';
3689        case 'processando': return 'Processando';
3690        case 'erro': return 'Erro';
3691        default: return 'Pendente';
3692    }
3693}
3694
3695// Upload functions
3696function initializeUploadEventsGestao() {
3697    const customFileSelector = gestaoGetById('customFileSelector');
3698    const customUploadArea = gestaoGetById('customUploadArea');
3699    const btnAdicionarProcesso = gestaoGetById('btnAdicionarProcesso');
3700    const uploadModeInputs = gestaoQueryAll('input[name="gestaoUploadMode"]');
3701
3702    if (customFileSelector && customUploadArea) {
3703        customUploadArea.addEventListener('dragover', (e) => {
3704            e.preventDefault();
3705            customUploadArea.classList.add('dragover');
3706        });
3707        customUploadArea.addEventListener('dragleave', () => {
3708            customUploadArea.classList.remove('dragover');
3709        });
3710        customUploadArea.addEventListener('drop', (e) => {
3711            e.preventDefault();
3712            customUploadArea.classList.remove('dragover');
3713            handleFilesSelectGestao(e.dataTransfer.files);
3714        });
3715
3716        customFileSelector.addEventListener('change', () => {
3717            handleFilesSelectGestao(customFileSelector.files);
3718        });
3719    }
3720
3721    if (btnAdicionarProcesso) {
3722        btnAdicionarProcesso.addEventListener('click', uploadSelectedFilesGestao);
3723    }
3724
3725    uploadModeInputs.forEach(input => {
3726        input.addEventListener('change', () => {
3727            renderSelectedFilesListGestao();
3728            updateGestaoUploadButtonText();
3729        });
3730    });
3731
3732    updateGestaoUploadButtonText();
3733}
3734
3735function getGestaoUploadMode() {
3736    const selectedMode = gestaoQuery('input[name="gestaoUploadMode"]:checked');
3737    return selectedMode && selectedMode.value === 'merged' ? 'merged' : 'separate';
3738}
3739
3740function getGestaoUploadButtonLabel() {
3741    const count = selectedUploadFilesGestao.length;
3742    const uploadMode = getGestaoUploadMode();
3743    // Word também é aceito, então o rótulo fala em "arquivos", não em "PDFs".
3744    const fileLabel = __isEN
3745        ? (count === 1 ? 'file' : 'files')
3746        : (count === 1 ? 'arquivo' : 'arquivos');
3747    const processLabel = count === 1 ? 'processo' : 'processos diferentes';
3748    if (uploadMode === 'merged') {
3749        if (count === 0) {
3750            return __isEN ? 'Merge files and upload' : 'Unificar arquivos e cadastrar';
3751        }
3752        if (count === 1) {
3753            return __isEN ? `Register ${count} ${fileLabel}` : `Cadastrar ${count} ${fileLabel}`;
3754        }
3755        return __isEN
3756            ? `Merge ${count} ${fileLabel} and upload`
3757            : `Unificar ${count} ${fileLabel} e cadastrar`;
3758    }
3759
3760    if (count === 0) {
3761        return __isEN ? 'Register different cases' : 'Cadastrar processos diferentes';
3762    }
3763    return __isEN
3764        ? (count === 1 ? `Register ${count} case` : `Register ${count} different cases`)
3765        : `Cadastrar ${count} ${processLabel}`;
3766}
3767
3768function getGestaoUploadModeHint(count, uploadMode, totalBytes) {
3769    const fileLabel = __isEN
3770        ? (count === 1 ? 'file' : 'files')
3771        : (count === 1 ? 'arquivo' : 'arquivos');
3772    if (uploadMode === 'merged') {
3773        return __isEN
3774            ? `${count} ${fileLabel} ${count === 1 ? 'will be merged' : 'will be merged'} into 1 case (${formatAssessorFileSizeLabel(totalBytes)})`
3775            : `${count} ${fileLabel} ${count === 1 ? 'será mesclado' : 'serão mesclados'} em 1 processo (${formatAssessorFileSizeLabel(totalBytes)})`;
3776    }
3777    return __isEN
3778        ? `${count} ${fileLabel} ${count === 1 ? 'will be registered as 1 case' : 'will become different cases'}`
3779        : `${count} ${fileLabel} ${count === 1 ? 'será cadastrado como 1 processo' : 'serão cadastrados como processos diferentes'}`;
3780}
3781
3782function getGestaoUploadButtonHtml(isLoading) {
3783    if (isLoading) {
3784        const loadingLabel = getGestaoUploadMode() === 'merged'
3785            ? (__isEN ? 'Merging...' : 'Unificando...')
3786            : (__isEN ? 'Uploading...' : 'Cadastrando...');
3787        return '<i class="fa fa-spinner fa-spin"></i> ' + loadingLabel;
3788    }
3789    return getGestaoUploadButtonLabel();
3790}
3791
3792function updateGestaoUploadButtonText(isLoading) {
3793    const btnAdicionarProcesso = gestaoGetById('btnAdicionarProcesso');
3794    if (btnAdicionarProcesso) {
3795        btnAdicionarProcesso.innerHTML = getGestaoUploadButtonHtml(!!isLoading);
3796    }
3797}
3798
3799function getGerarPecasButtonLabel() {
3800    const count = gestaoQueryAll('.processo-checkbox:checked').length;
3801    if (__isEN) {
3802        if (count === 0) return 'GENERATE DRAFT';
3803        if (count === 1) return 'GENERATE 1 DRAFT';
3804        return `GENERATE ${count} DRAFTS`;
3805    }
3806    if (count === 0) return 'GERAR PEÇA';
3807    if (count === 1) return 'GERAR 1 PEÇA';
3808    return `GERAR ${count} PEÇAS`;
3809}
3810
3811function updateGerarPecasButtonText() {
3812    const btn = gestaoGetById('btnGerarPecasLote');
3813    if (!btn || btn.dataset.loading === 'true') return;
3814    const count = gestaoQueryAll('.processo-checkbox:checked').length;
3815    btn.disabled = count === 0;
3816    btn.innerHTML = '<i class="fa fa-magic"></i> ' + getGerarPecasButtonLabel();
3817}
3818
3819const GESTAO_DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
3820const GESTAO_DOC_MIMES = ['application/msword', 'application/doc', 'application/vnd.ms-word'];
3821
3822// Alguns sistemas operacionais entregam file.type vazio ou genérico para Word,
3823// então a extensão vale como fallback.
3824function gestaoUploadFileKind(file) {
3825    const name = String(file?.name || '').toLowerCase();
3826    const type = String(file?.type || '').toLowerCase();
3827    if (type === 'application/pdf' || name.endsWith('.pdf')) return 'pdf';
3828    if (type === GESTAO_DOCX_MIME || name.endsWith('.docx')) return 'docx';
3829    if (GESTAO_DOC_MIMES.includes(type) || name.endsWith('.doc')) return 'doc';
3830    return '';
3831}
3832
3833function handleFilesSelectGestao(files) {
3834    if (!files || files.length === 0) return;
3835
3836    Array.from(files).forEach(file => {
3837        if (!gestaoUploadFileKind(file)) {
3838            funcMsg(
3839                'warning',
3840                'Atenção',
3841                __isEN
3842                    ? `The file ${file.name} is not a PDF or Word document.`
3843                    : `O arquivo ${file.name} não é PDF nem Word (DOCX/DOC).`,
3844                '',
3845                ''
3846            );
3847            return;
3848        }
3849        if (!validateAssessorFileForStorage(file, { useFuncMsg: true })) {
3850            return;
3851        }
3852        if (!selectedUploadFilesGestao.some(f => f.name === file.name && f.size === file.size)) {
3853            selectedUploadFilesGestao.push(file);
3854        }
3855    });
3856
3857    renderSelectedFilesListGestao();
3858    updateGestaoUploadButtonText();
3859    const customFileSelector = gestaoGetById('customFileSelector');
3860    if (customFileSelector) customFileSelector.value = '';
3861}
3862
3863function renderSelectedFilesListGestao() {
3864    const listaArquivos = gestaoGetById('listaArquivosSelecionadosGestao');
3865    if (!listaArquivos) return;
3866
3867    listaArquivos.innerHTML = '';
3868
3869    if (selectedUploadFilesGestao.length === 0) {
3870        updateGestaoUploadButtonText();
3871        return;
3872    }
3873
3874    const escHtmlGestao = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3875    const uploadMode = getGestaoUploadMode();
3876    const totalBytes = selectedUploadFilesGestao.reduce((sum, file) => sum + (file.size || 0), 0);
3877    if (selectedUploadFilesGestao.length > 1) {
3878        const modeHint = document.createElement('div');
3879        modeHint.className = 'gestao-upload-mode-hint';
3880        modeHint.innerHTML = uploadMode === 'merged'
3881            ? `<i class="fa fa-compress" aria-hidden="true"></i><span>${getGestaoUploadModeHint(selectedUploadFilesGestao.length, uploadMode, totalBytes)}</span>`
3882            : `<i class="fa fa-files-o" aria-hidden="true"></i><span>${getGestaoUploadModeHint(selectedUploadFilesGestao.length, uploadMode, totalBytes)}</span>`;
3883        listaArquivos.appendChild(modeHint);
3884    }
3885
3886    selectedUploadFilesGestao.forEach((file, index) => {
3887        const item = document.createElement('div');
3888        item.className = 'arquivo-item';
3889        const isWord = gestaoUploadFileKind(file) !== 'pdf';
3890        const itemIcon = isWord ? 'fa-file-word-o' : 'fa-file-pdf-o';
3891        const conversionNote = isWord
3892            ? ` <span class="arquivo-item-meta">${__isEN ? 'Converted to PDF' : 'Será convertido em PDF'}</span>`
3893            : '';
3894        item.innerHTML = `
3895                <div class="arquivo-item-body">
3896                    <i class="fa ${itemIcon} arquivo-item-icon" aria-hidden="true"></i>
3897                    <div class="arquivo-item-text">
3898                        <span class="arquivo-item-name">${escHtmlGestao(normalizeGestaoFilename(file.name))}</span>
3899                        <span class="arquivo-item-meta">(${Math.round(file.size / 1024)} KB)</span>${conversionNote}
3900                    </div>
3901                </div>
3902                <button type="button" class="btn-remove-pdf" title="Remover" onclick="removeSelectedFileGestao(${index})">
3903                    <i class="fa fa-trash" aria-hidden="true"></i>
3904                </button>
3905            `;
3906        listaArquivos.appendChild(item);
3907    });
3908}
3909
3910window.removeSelectedFileGestao = function (index) {
3911    selectedUploadFilesGestao.splice(index, 1);
3912    renderSelectedFilesListGestao();
3913};
3914
3915async function uploadSelectedFilesGestao() {
3916    if (selectedUploadFilesGestao.length === 0) {
3917        funcMsg('warning', 'Atenção', 'Selecione pelo menos um arquivo para inserir.', '', '', undefined, gestaoInsightOptions('upload_process_files_empty', {
3918            errorCode: 'validation:process_files_required',
3919            context: { missingFields: ['files'] }
3920        }));
3921        return;
3922    }
3923
3924    if (!checkAuthenticationBeforeAction()) {
3925        return;
3926    }
3927
3928    const token = cognijusUserToken || localStorage.getItem('CogniJUS_User_Token');
3929
3930    const btnAdicionarProcesso = gestaoGetById('btnAdicionarProcesso');
3931    if (btnAdicionarProcesso) {
3932        btnAdicionarProcesso.disabled = true;
3933        updateGestaoUploadButtonText(true);
3934    }
3935
3936    const uploadMode = getGestaoUploadMode();
3937    let successCount = 0;
3938    let errorCount = 0;
3939    let lastErrorMessage = '';
3940
3941    if (uploadMode === 'merged' && !validateAssessorFilesForAnalysis(selectedUploadFilesGestao, { useFuncMsg: true })) {
3942        if (btnAdicionarProcesso) {
3943            btnAdicionarProcesso.disabled = false;
3944            updateGestaoUploadButtonText(false);
3945        }
3946        return;
3947    }
3948
3949    if (uploadMode === 'merged') {
3950        const formData = new FormData();
3951        selectedUploadFilesGestao.forEach(file => {
3952            formData.append('pdfFiles', file);
3953        });
3954        formData.append('uploadMode', 'merged');
3955        formData.append('form_objKey', JSON.stringify({
3956            funcionalidadeName: 'Gestão Processual',
3957            uploadMode: 'merged',
3958            pdfCount: selectedUploadFilesGestao.length
3959        }));
3960
3961        try {
3962            const response = await fetch(base_url_gestao + '/processos/upload', {
3963                method: 'POST',
3964                headers: { 'x-access-token': token },
3965                body: formData
3966            });
3967
3968            if (response.ok) {
3969                successCount++;
3970            } else {
3971                errorCount++;
3972                const data = await response.json().catch(() => ({}));
3973                lastErrorMessage = data.msg || lastErrorMessage;
3974            }
3975        } catch (error) {
3976            errorCount++;
3977            lastErrorMessage = error.message || lastErrorMessage;
3978        }
3979    } else {
3980        for (const file of selectedUploadFilesGestao) {
3981            const formData = new FormData();
3982            formData.append('pdfFile', file);
3983            formData.append('uploadMode', 'separate');
3984            formData.append('form_objKey', JSON.stringify({
3985                funcionalidadeName: 'Gestão Processual',
3986                uploadMode: 'separate',
3987                pdfCount: 1
3988            }));
3989
3990            try {
3991                const response = await fetch(base_url_gestao + '/processos/upload', {
3992                    method: 'POST',
3993                    headers: { 'x-access-token': token },
3994                    body: formData
3995                });
3996
3997                if (response.ok) {
3998                    successCount++;
3999                } else {
4000                    errorCount++;
4001                    const data = await response.json().catch(() => ({}));
4002                    lastErrorMessage = data.msg || lastErrorMessage;
4003                }
4004            } catch (error) {
4005                errorCount++;
4006                lastErrorMessage = error.message || lastErrorMessage;
4007            }
4008        }
4009    }
4010
4011    if (btnAdicionarProcesso) {
4012        btnAdicionarProcesso.disabled = false;
4013        updateGestaoUploadButtonText(false);
4014    }
4015
4016    if (successCount > 0) {
4017        selectedUploadFilesGestao = [];
4018        renderSelectedFilesListGestao();
4019        loadProcessosGestao();
4020        if (typeof window.cjErpRefreshAfterAssessor === 'function') window.cjErpRefreshAfterAssessor();
4021        const successMessage = gestaoUploadSuccessMessage(successCount, uploadMode);
4022        funcMsg('success', __isEN ? 'Success' : 'Sucesso', successMessage, '', '');
4023        const tableContainer = gestaoQuery('.gestao-processual-table-container');
4024        if (tableContainer) tableContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
4025    }
4026    if (errorCount > 0) {
4027        funcMsg('warning', __L.attn, lastErrorMessage || gestaoUploadErrorMessage(errorCount), '', '');
4028    }
4029}
4030
4031// Global functions for table actions
4032window.verProcessoGestao = function (fileId) {
4033    if (!fileId || fileId === 'undefined') {
4034        funcMsg('error', 'Erro', 'Arquivo não disponível.', '', '');
4035        return;
4036    }
4037    if (!checkAuthenticationBeforeAction()) {
4038        return;
4039    }
4040    const token = cognijusUserToken || localStorage.getItem('CogniJUS_User_Token');
4041    window.open(base_url_gestao + '/processos/download/' + fileId + '?token=' + encodeURIComponent(token), '_blank');
4042};
4043
4044window.deleteProcessGestao = async function (id) {
4045    if (!checkAuthenticationBeforeAction()) {
4046        return;
4047    }
4048    const processo = processosDataGestao.find(p => gestaoProcessoId(p) === String(id || ''));
4049    if (processo && gestaoRowIsBusy(processo)) {
4050        return;
4051    }
4052    const token = cognijusUserToken || localStorage.getItem('CogniJUS_User_Token');
4053    if (!confirm('Tem certeza que deseja excluir este processo?')) return;
4054
4055    try {
4056        const response = await fetch(base_url_gestao + '/processos/delete', {
4057            method: 'POST',
4058            headers: {
4059                'Content-Type': 'application/json',
4060                'x-access-token': token
4061            },
4062            body: JSON.stringify({ processoId: id })
4063        });
4064
4065        if (response.ok) {
4066            gestaoRowSelections.delete(String(id || ''));
4067            funcMsg('success', 'Sucesso', 'Processo excluído.', '', '');
4068            loadProcessosGestao();
4069        } else {
4070            funcMsg('error', 'Erro', 'Erro ao excluir processo.', '', '', undefined, gestaoInsightOptions('delete_process', {
4071                category: 'server',
4072                errorCode: `server:delete_process_http_${response.status}`,
4073                context: { processoId: id, httpStatus: response.status }
4074            }));
4075        }
4076    } catch (error) {
4077        funcMsg('error', 'Erro', 'Erro de conexão.', '', error && error.message ? error.message : '', undefined, gestaoInsightOptions('delete_process', {
4078            category: 'network',
4079            errorCode: 'network:delete_process_failed',
4080            context: { processoId: id, errorName: error && error.name ? error.name : undefined }
4081        }));
4082    }
4083};
4084
4085// Seletor de minuta (substitui prompt()). Resolve com o índice do array
4086// escolhido, ou null se cancelado. Estilo inline para funcionar na página do
4087// Assessor e no shadow DOM do ERP (o overlay vai para document.body).
4088function gestaoPickPecaIndex(pecas) {
4089    if (typeof window.pickAssessorPecaIndex === 'function') {
4090        return window.pickAssessorPecaIndex(pecas);
4091    }
4092    const list = Array.isArray(pecas) ? pecas : [];
4093    return Promise.resolve(list.length ? 0 : null);
4094}
4095window.gestaoPickPecaIndex = gestaoPickPecaIndex;
4096
4097function applyAssessorPecaListRename(processId, pecaIndex, title) {
4098    const id = String(processId || '');
4099    const idx = parseInt(pecaIndex, 10);
4100    if (!id || !Number.isInteger(idx) || !Array.isArray(processosDataGestao)) return;
4101    processosDataGestao.forEach(p => {
4102        if (gestaoProcessoId(p) !== id || !Array.isArray(p.pecas)) return;
4103        p.pecas.forEach((peca, i) => {
4104            if (i === idx) peca.title = title || '';
4105        });
4106    });
4107}
4108
4109(function bindAssessorPecaRename() {
4110    function onMsg(data) {
4111        if (!data || data.type !== 'cognijus-peca-renamed') return;
4112        applyAssessorPecaListRename(data.processId, data.pecaIndex, data.title);
4113    }
4114    window.addEventListener('message', function (ev) {
4115        if (ev.origin !== window.location.origin && ev.origin !== 'null') return;
4116        onMsg(ev.data);
4117    });
4118    try {
4119        const ch = new BroadcastChannel('cognijus-peca');
4120        ch.onmessage = function (ev) { onMsg(ev.data); };
4121    } catch (e) { }
4122})();
4123
4124window.verPecaGeradaGestao = async function (id, pecasParam, triggerEl, meta) {
4125    meta = meta || {};
4126    if (!checkAuthenticationBeforeAction()) {
4127        return;
4128    }
4129    const token = cognijusUserToken || localStorage.getItem('CogniJUS_User_Token');
4130    const processo = processosDataGestao.find(p =>
4130 gestaoProcessoId(p) === id);
4131    const filename = (processo && processo.filename) || meta.filename || 'Documento analisado';
4132    const pecasFromProcess = (processo && Array.isArray(processo.pecas) && processo.pecas.length)
4133        ? processo.pecas
4134        : pecasParam;
4135
4136    // Se houver múltiplas peças, abre o seletor (modal) em vez de prompt().
4137    let chosenPecaIndex = Number.isInteger(meta.pecaIndex) ? meta.pecaIndex : 0;
4138    if (!Number.isInteger(meta.pecaIndex) && pecasFromProcess && pecasFromProcess.length > 1) {
4139        const idx = await gestaoPickPecaIndex(pecasFromProcess);
4140        if (idx == null || idx < 0 || idx >= pecasFromProcess.length) return;
4141        chosenPecaIndex = idx;
4142    }
4143
4144    if (!setPecaViewLoadingGestao(triggerEl, true, id, chosenPecaIndex)) return;
4145
4146    try {
4147        const response = await fetch(base_url_gestao + '/processos/get-peca', {
4148            method: 'POST',
4149            headers: {
4150                'Content-Type': 'application/json',
4151                'x-access-token': token
4152            },
4153            body: JSON.stringify({ processId: id, pecaIndex: chosenPecaIndex })
4154        });
4155
4156        const data = await response.json();
4157
4158        if (response.ok && data.relatorioHtml) {
4159            // Peças antigas gravadas com falha do LLM como "conteúdo" (bug corrigido no worker).
4160            const rawHtml = String(data.relatorioHtml || '').trim();
4161            const looksLikeProcessingError = /^(Erro no processamento\. Tente novamente\.|O processamento (excedeu o tempo limite|demorou mais que o esperado))/i.test(rawHtml.replace(/<[^>]+>/g, '').trim());
4162            if (looksLikeProcessingError) {
4163                funcMsg(
4164                    'error',
4165                    __isEN ? 'Processing failed' : 'Falha na geração',
4166                    __isEN
4167                        ? 'This draft failed during AI generation. Please generate the piece again.'
4168                        : 'Esta peça falhou na geração pela IA. Gere a peça novamente (o status aparece como concluído, mas o conteúdo não foi gerado).',
4169                    '',
4170                    ''
4171                );
4172                return;
4173            }
4174            if (typeof window.openAssessorPecaWindow !== 'function') {
4175                funcMsg('error', 'Erro', 'Não foi possível abrir a peça.', '', '');
4176                return;
4177            }
4178            const _wBaseUrl = (typeof base_url !== 'undefined' && base_url) ? base_url : window.location.origin;
4179            const _wAssetQ = (function () {
4180                try {
4181                    var nodes = document.querySelectorAll('script[src], link[rel="stylesheet"][href]');
4182                    for (var i = 0; i < nodes.length; i++) {
4183                        var href = nodes[i].src || nodes[i].href || '';
4184                        var m = String(href).match(/[?&]v=([^&]+)/);
4185                        if (m) return '?v=' + encodeURIComponent(decodeURIComponent(m[1]));
4186                    }
4187                } catch (e) { }
4188                return '';
4189            })();
4190            window.openAssessorPecaWindow({
4191                html: assessorHtmlNewlinesToBr(data.relatorioHtml),
4192                filename: filename,
4193                selectedPeca: data.selectedPeca || (pecasFromProcess && pecasFromProcess[chosenPecaIndex] && pecasFromProcess[chosenPecaIndex].selectedPeca) || 'Minuta',
4194                title: data.title || (pecasFromProcess && pecasFromProcess[chosenPecaIndex] && pecasFromProcess[chosenPecaIndex].title) || '',
4195                precedentes: data.precedentes || '',
4196                processId: id,
4197                pecaIndex: chosenPecaIndex,
4198                canEdit: data.canEdit === true,
4199                token: cognijusUserToken || '',
4200                codigoUnico: (typeof codigoUnico !== 'undefined' ? codigoUnico : '') || '',
4201                adminCodigoUnico: '',
4202                funcName: funcionalidadeName || 'Assessor jurídico',
4203                llmModel: data.llmModel || (typeof getSelectedLLMModel === 'function' ? getSelectedLLMModel() : '') || 'chatgpt',
4204                isEN: typeof __isEN !== 'undefined' ? __isEN : false,
4205                baseUrl: _wBaseUrl,
4206                assetQ: _wAssetQ
4207            });
4208        } else {
4209            funcMsg('error', 'Erro', 'Não foi possível carregar a peça.', '', '');
4210        }
4211    } catch (error) {
4212        funcMsg('error', 'Erro', 'Erro de conexão.', '', '');
4213    } finally {
4214        setPecaViewLoadingGestao(triggerEl, false, id, chosenPecaIndex);
4215    }
4216};
4217
4218function getPecaViewIconGestao(triggerEl) {
4219    if (!triggerEl || !triggerEl.classList) return null;
4220    if (triggerEl.classList.contains('peca-view-icon')) return triggerEl;
4221    const cell = triggerEl.closest ? triggerEl.closest('.peca-view-cell') : null;
4222    return cell ? cell.querySelector('.peca-view-icon') : null;
4223}
4224
4225function pecaViewLoadingKey(processId, pecaIndex) {
4226    const id = String(processId || '');
4227    if (!id) return '';
4228    if (Number.isInteger(pecaIndex)) return id + ':' + pecaIndex;
4229    return id;
4230}
4231
4232const gestaoPecaViewLoading = new Set();
4233
4234function gestaoPecaViewIsOpening(processId, pecaIndex) {
4235    const id = String(processId || '');
4236    if (!id || !gestaoPecaViewLoading.size) return false;
4237    if (Number.isInteger(pecaIndex)) return gestaoPecaViewLoading.has(id + ':' + pecaIndex);
4238    if (gestaoPecaViewLoading.has(id)) return true;
4239    const prefix = id + ':';
4240    for (const key of gestaoPecaViewLoading) {
4241        if (key === id || key.indexOf(prefix) === 0) return true;
4242    }
4243    return false;
4244}
4245window.gestaoPecaViewIsOpening = gestaoPecaViewIsOpening;
4246
4247function applyPecaViewIconLoading(iconEl, loading) {
4248    if (!iconEl) return;
4249    if (loading) {
4250        iconEl.dataset.loading = '1';
4251        iconEl.className = 'fa fa-spinner fa-spin peca-view-icon';
4252        iconEl.style.color = '#1d4ed8';
4253        iconEl.style.cursor = 'wait';
4254        iconEl.style.opacity = '1';
4255        iconEl.setAttribute('title', __isEN ? 'Opening draft...' : 'Abrindo a peça...');
4256        return;
4257    }
4258    iconEl.dataset.loading = '0';
4259    iconEl.className = 'fa fa-file-text-o peca-view-icon';
4260    iconEl.style.color = '#007bff';
4261    iconEl.style.cursor = 'pointer';
4262    iconEl.setAttribute('title', 'Ver Peça Gerada');
4263}
4264
4265function setErpPecaBtnLoading(btn, loading) {
4266    if (!btn || !btn.classList || !btn.classList.contains('cj-erp-gp-peca-btn')) return;
4267    btn.classList.toggle('is-loading', !!loading);
4268    btn.disabled = !!loading;
4269    btn.setAttribute('aria-busy', loading ? 'true' : 'false');
4270    let spin = btn.querySelector('.cj-erp-gp-peca-spin');
4271    if (loading) {
4272        if (!spin) {
4273            spin = document.createElement('i');
4274            spin.className = 'fa fa-spinner fa-spin cj-erp-gp-peca-spin';
4275            spin.setAttribute('aria-hidden', 'true');
4276            btn.insertBefore(spin, btn.firstChild);
4277        }
4278        return;
4279    }
4280    if (spin) spin.remove();
4281}
4282
4283function findLivePecaViewIcons(processId) {
4284    const id = String(processId || '');
4285    const found = [];
4286    if (!id) return found;
4287    const tbody = typeof gestaoGetById === 'function' ? gestaoGetById('tabelaProcessosBody') : document.getElementById('tabelaProcessosBody');
4288    if (tbody) {
4289        tbody.querySelectorAll('tr').forEach(row => {
4290            const cb = row.querySelector('.processo-checkbox');
4291            if (!cb || String(cb.value) !== id) return;
4292            const icon = row.querySelector('.peca-view-icon');
4293            if (icon) found.push(icon);
4294        });
4295    }
4296    return found;
4297}
4298
4299function findLiveErpPecaButtons(processId, pecaIndex) {
4300    const id = String(processId || '');
4301    const found = [];
4302    if (!id) return found;
4303    const host = document.getElementById('cj-erp-shadow-host');
4304    const root = host && host.shadowRoot;
4305    if (!root) return found;
4306    root.querySelectorAll('[data-gp-peca="' + id + '"]').forEach(btn => {
4307        if (!Number.isInteger(pecaIndex)) {
4308            found.push(btn);
4309            return;
4310        }
4311        if (parseInt(btn.getAttribute('data-gp-peca-index'), 10) === pecaIndex) found.push(btn);
4312    });
4313    return found;
4314}
4315
4316function setPecaViewLoadingGestao(triggerEl, loading, processId, pecaIndex) {
4317    const id = String(processId || '');
4318    const key = pecaViewLoadingKey(id, pecaIndex);
4319    if (loading) {
4320        if (key && gestaoPecaViewLoading.has(key)) return false;
4321        if (key) gestaoPecaViewLoading.add(key);
4322        else {
4323            const iconEl = getPecaViewIconGestao(triggerEl);
4324            if (iconEl && iconEl.dataset.loading === '1') return false;
4325        }
4326    } else if (key) {
4327        gestaoPecaViewLoading.delete(key);
4328    }
4329
4330    const iconEl = getPecaViewIconGestao(triggerEl);
4331    const iconStill = loading || gestaoPecaViewIsOpening(id);
4332    applyPecaViewIconLoading(iconEl, iconStill);
4333    findLivePecaViewIcons(id).forEach(icon => applyPecaViewIconLoading(icon, iconStill));
4334
4335    setErpPecaBtnLoading(triggerEl, loading);
4336    findLiveErpPecaButtons(id, pecaIndex).forEach(btn => setErpPecaBtnLoading(btn, loading));
4337    return true;
4338}
4339
4340// Batch generation
4341function initializeBatchGenerationGestao() {
4342    const btnGerarPecasLote = gestaoGetById('btnGerarPecasLote');
4343    const checkboxSelecionarTodos = gestaoGetById('selecionarTodos');
4344
4345    if (btnGerarPecasLote) {
4346        btnGerarPecasLote.addEventListener('click', async () => {
4347            if (!checkAuthenticationBeforeAction()) {
4348                return;
4349            }
4350
4351            const checkboxes = gestaoQueryAll('.processo-checkbox:checked');
4352            const token = cognijusUserToken || localStorage.getItem('CogniJUS_User_Token');
4353            const personaSelect = gestaoGetById('personaSelectGestao');
4354
4355            if (!personaSelect || !personaSelect.value) {
4356                funcMsg('warning', __L.attn, __isEN ? 'Please select a Persona before generating drafts in batch.' : 'Selecione um Perfil (Persona) antes de gerar as peças em lote.', '', '', undefined, gestaoInsightOptions('batch_generate_without_persona', {
4357                    errorCode: 'validation:persona_required',
4358                    context: { missingFields: ['persona'] }
4359                }));
4360                const personasSection = gestaoQuery('#meus-processos .personas-section') || gestaoGetById('personaSelectGestao');
4361                if (personasSection) {
4362                    personasSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
4363                }
4364                return;
4365            }
4366
4367            if (checkboxes.length === 0) {
4368                funcMsg('warning', __L.attn, __isEN ? 'Please select at least one case.' : 'Selecione ao menos um processo.', '', '', undefined, gestaoInsightOptions('batch_generate_without_selection', {
4369                    errorCode: 'validation:process_selection_required',
4370                    context: { missingFields: ['processSelection'] }
4371                }));
4372                return;
4373            }
4374
4375            if (checkboxes.length > 5) {
4376                funcMsg('warning', __L.attn, __isEN ? 'You can generate documents for up to 5 cases at a time.' : 'Você pode gerar peças para no máximo 5 processos por vez.', '', '', undefined, gestaoInsightOptions('batch_generate_limit_exceeded', {
4377                    errorCode: 'validation:batch_limit_exceeded',
4378                    context: { maxSelected: 5, attemptedSelected: checkboxes.length }
4379                }));
4380                return;
4381            }
4382
4383            // Plano gratuito: pode gerar 1 peça por vez; lote (2+) é exclusivo do PRO.
4384            if (!isAssessorUserPro() && checkboxes.length > 1) {
4385                if (typeof openCogniJUSProModal === 'function') {
4386                    openCogniJUSProModal(__isEN
4387                        ? 'Batch generation is available only for PRO users. On the free plan, generate one draft at a time.'
4388                        : 'A geração em lote está disponível apenas para usuários PRO. No plano gratuito, gere uma peça por vez.');
4389                }
4390                return;
4391            }
4392
4393            const processosData = Array.from(checkboxes).map(cb => {
4394                const row = cb.closest('tr');
4395                const pecaSelect = row.querySelector('.row-peca-select');
4396                const promptInput = row.querySelector('.row-additional-prompt');
4397                const promptTitleInput = row.querySelector('.row-additional-prompt-title');
4398                const pagesInput = row.querySelector('.row-include-pages');
4399                const anonCheckbox = row.querySelector('.row-anonymization');
4400                const modelIdInput = row.querySelector('.row-model-id');
4401                const modelTitleInput = row.querySelector('.row-model-title');
4402                const modelBtn = row.querySelector('.row-model-btn');
4403                const modelBusy = !!(modelBtn && modelBtn.disabled);
4404                const modelId = !modelBusy && modelIdInput ? String(modelIdInput.value || '').trim() : '';
4405                const modelTitle = modelTitleInput ? String(modelTitleInput.value || '').trim() : '';
4406                return {
4407                    id: cb.value,
4408                    peca: pecaSelect ? pecaSelect.value : '',
4409                    additionalPrompt: promptInput ? promptInput.value.trim() : '',
4410                    additionalPromptTitle: promptTitleInput ? promptTitleInput.value.trim() : '',
4411                    includePages: pagesInput ? pagesInput.value.trim() : '',
4412                    enableAnonymization: anonCheckbox ? !!anonCheckbox.checked : false,
4413                    modelId: modelId,
4414                    modelTitle: modelId ? getGestaoModelTitle(modelId, modelTitle) : ''
4415                };
4416            });
4417
4418            const processosSemPeca = processosData.filter(p => !p.peca || p.peca === '');
4419            if (processosSemPeca.length > 0) {
4420                const firstRow = gestaoQuery(`.processo-checkbox[value="${processosSemPeca[0].id}"]`)?.closest('tr');
4421                const pecaSelect = firstRow ? firstRow.querySelector('.row-peca-select') : null;
4422                funcMsg('warning', 'Atenção', __isEN
4423                    ? 'Please select a draft for each marked case before generating.'
4424                    : 'Selecione a peça desejada para cada processo marcado.', '', '', undefined, gestaoInsightOptions('batch_generate_missing_piece', {
4425                        errorCode: 'validation:piece_required',
4426                        context: {
4427                            missingFields: ['piece'],
4428                            missingPieceCount: processosSemPeca.length,
4429                            selectedCount: checkboxes.length
4430                        }
4431                    }));
4432                if (pecaSelect) highlightPecaSelectGestao(pecaSelect);
4433                return;
4434            }
4435
4436            const uploadMaxBytes = getAssessorUploadMaxBytes();
4437            const oversizedBatch = processosData
4438                .map(entry => processosDataGestao.find(p => gestaoProcessoId(p) === entry.id))
4439                .filter(p => p && (p.fileSize || 0) > uploadMaxBytes);
4440            if (oversizedBatch.length > 0) {
4441                const sample = oversizedBatch[0];
4442                const limitMb = getAssessorUploadMaxMbLabel();
4443                const msg = __isEN
4444                    ? `${oversizedBatch.length} selected case(s) exceed the ${limitMb} MB limit (e.g. "${gestaoDisplayFilename(sample.filename)}"). Use Focus Pages or a smaller PDF.`
4445                    : `${oversizedBatch.length} processo(s) selecionado(s) excedem o limite de ${limitMb} MB (ex.: "${gestaoDisplayFilename(sample.filename)}"). Use Páginas em Foco ou um PDF menor.`;
4446                funcMsg('warning', __L.attn, msg, '', '', undefined, gestaoInsightOptions('batch_generate_file_too_large', {
4447                    errorCode: 'validation:file_too_large',
4448                    context: {
4449                        oversizedCount: oversizedBatch.length,
4450                        maxMb: limitMb
4451                    }
4452                }));
4453                return;
4454            }
4455
4456            const persona = personaSelect ? personaSelect.value : null;
4457            const llmModel = typeof getSelectedLLMModel === 'function' ? getSelectedLLMModel() : 'chatgpt';
4458
4459            btnGerarPecasLote.dataset.loading = 'true';
4460            btnGerarPecasLote.disabled = true;
4461            btnGerarPecasLote.innerHTML = '<i class="fa fa-spinner fa-spin"></i> ' + (__isEN ? 'Queuing...' : 'Enfileirando...');
4462
4463            try {
4464                const response = await fetch(base_url_gestao + '/processos/gerar-lote', {
4465                    method: 'POST',
4466                    headers: {
4467                        'Content-Type': 'application/json',
4468                        'x-access-token': token
4469                    },
4470                    body: JSON.stringify({
4471                        processos: processosData,
4472                        persona,
4473                        llmModel
4474                    })
4475                });
4476
4477                const data = await response.json();
4478
4479                if (response.ok) {
4480                    const detalhesLote = Array.isArray(data.detalhes) ? data.detalhes : [];
4481                    const errosDetalhe = detalhesLote.filter(d => d && d.status === 'erro');
4482                    const errosUnicos = [...new Set(errosDetalhe.map(d =>
4482 d.error).filter(Boolean))];
4483                    const causaErro = errosUnicos.length === 1
4484                        ? errosUnicos[0]
4485                        : (errosUnicos.length > 1 ? errosUnicos.join(' ') : '');
4486
4487                    if (data.mode === 'queued') {
4488                        const enfileirados = data.enfileirados || 0;
4489                        const totalErros = data.erros || 0;
4490                        const msgEnfileirados = enfileirados === 1
4491                            ? (__isEN ? '1 draft queued' : '1 peça enfileirada')
4492                            : (__isEN ? `${enfileirados} drafts queued` : `${enfileirados} peças enfileiradas`);
4493                        const msgErros = totalErros === 1
4494                            ? (__isEN ? '1 case could not be queued' : '1 processo não enfileirado')
4495                            : (__isEN ? `${totalErros} cases could not be queued` : `${totalErros} processos não enfileirados`);
4496                        const bgMsg = __isEN
4497                            ? 'Processing continues in the background. The list refreshes automatically.'
4498                            : 'O processamento continua em segundo plano. A lista atualiza automaticamente.';
4499
4500                        if (enfileirados > 0 && totalErros === 0) {
4501                            detalhesLote.filter(d => d && d.status === 'enfileirado').forEach(d => gestaoTrackJob(d.id));
4502                            gestaoSuccessNotify(
4503                                __isEN ? 'Queued' : 'Enfileirado',
4504                                `${msgEnfileirados}. ${bgMsg}`,
4505                                '',
4506                                JSON.stringify(detalhesLote)
4507                            );
4508                        } else if (enfileirados > 0 && totalErros > 0) {
4509                            detalhesLote.filter(d => d && d.status === 'enfileirado').forEach(d => gestaoTrackJob(d.id));
4510                            const msgParcial = causaErro
4511                                ? `${msgEnfileirados}. ${msgErros}: ${causaErro}. ${bgMsg}`
4512                                : `${msgEnfileirados}. ${msgErros}. ${bgMsg}`;
4513                            funcMsg('warning', __isEN ? 'Partially queued' : 'Enfileirado com ressalvas', msgParcial, '', JSON.stringify(detalhesLote), undefined, gestaoInsightOptions('batch_queue_partial', {
4514                                notifyAdmin: false,
4515                                category: 'server',
4516                                errorCode: 'server:batch_queue_partial',
4517                                context: { successCount: enfileirados, errorCount: totalErros, uniqueErrors: errosUnicos }
4518                            }));
4519                        } else {
4520                            // Quando o único motivo é "já está sendo processado", é estado esperado
4521                            // (não erro): rebaixa para informativo e não notifica o admin.
4522                            const apenasJaProcessando = errosDetalhe.length > 0
4523                                && errosDetalhe.every(d => d.reason === 'already_processing');
4524                            if (apenasJaProcessando) {
4525                                const msgJa = __isEN
4526                                    ? `${msgErros} already being processed. Please wait — the list refreshes automatically.`
4527                                    : `${msgErros} já em processamento. Aguarde a conclusão — a lista atualiza automaticamente.`;
4528                                funcMsg('info', __isEN ? 'Already processing' : 'Já em processamento', msgJa, '', JSON.stringify(detalhesLote), undefined, gestaoInsightOptions('batch_queue_already_processing', {
4529                                    notifyAdmin: false,
4530                                    category: 'validation',
4531                                    errorCode: 'validation:batch_queue_already_processing',
4532                                    context: { successCount: enfileirados, errorCount: totalErros, uniqueErrors: errosUnicos }
4533                                }));
4534                            } else {
4535                                const msgFalha = causaErro
4536                                    ? (__isEN ? `No drafts were queued. ${msgErros}: ${causaErro}.` : `Nenhuma peça enfileirada. ${msgErros}: ${causaErro}.`)
4537                                    : (__isEN ? 'No drafts were queued.' : 'Nenhuma peça enfileirada.');
4538                                funcMsg('error', __isEN ? 'Queue failed' : 'Erro ao enfileirar', msgFalha, '', JSON.stringify(detalhesLote), undefined, gestaoInsightOptions('batch_queue_failed', {
4539                                    category: 'server',
4540                                    errorCode: 'server:batch_queue_failed',
4541                                    context: { successCount: enfileirados, errorCount: totalErros, uniqueErrors: errosUnicos }
4542                                }));
4543                            }
4544                        }
4545                    } else {
4546                        const totalSucessos = data.sucessos || 0;
4547                        const totalErros = data.erros || 0;
4548                        const todosRetryable = errosDetalhe.length > 0 && errosDetalhe.every(d => d.retryable);
4549                        const msgPecasOk = totalSucessos === 1
4550                            ? '1 peça gerada com sucesso'
4551                            : `${totalSucessos} peças geradas com sucesso`;
4552                        const msgProcessosErro = totalErros === 1
4553                            ? '1 processo com erro'
4554                            : `${totalErros} processos com erro`;
4555                        const msgComCausa = causaErro
4556                            ? `Nenhuma peça foi gerada. ${msgProcessosErro}: ${causaErro}`
4557                            : `Nenhuma peça foi gerada. ${msgProcessosErro}.`;
4558                        if (totalSucessos === 0) {
4559                            funcMsg(
4560                                todosRetryable ? 'warning' : 'error',
4561                                todosRetryable ? (__isEN ? 'Server busy' : 'Servidor ocupado') : 'Erro ao gerar peças',
4562                                msgComCausa,
4563                                '',
4564                                JSON.stringify(detalhesLote),
4565                                undefined,
4566                                gestaoInsightOptions('batch_generate_failed', {
4567                                    notifyAdmin: !todosRetryable,
4568                                    category: todosRetryable ? 'server_retryable' : 'server',
4569                                    errorCode: todosRetryable ? 'server:batch_generate_retryable' : 'server:batch_generate_failed',
4570                                    context: { successCount: totalSucessos, errorCount: totalErros, uniqueErrors: errosUnicos, retryable: todosRetryable }
4571                                })
4572                            );
4573                        } else if (totalErros > 0) {
4574                            const msgParcial = causaErro
4575                                ? `${msgPecasOk}. ${msgProcessosErro}: ${causaErro}`
4576                                : `${msgPecasOk} e ${msgProcessosErro}.`;
4577                            funcMsg('warning', 'Concluído com erros', msgParcial, '', JSON.stringify(detalhesLote), undefined, gestaoInsightOptions('batch_generate_partial', {
4578                                category: 'server',
4579                                errorCode: 'server:batch_generate_partial',
4580                                context: { successCount: totalSucessos, errorCount: totalErros, uniqueErrors: errosUnicos }
4581                            }));
4582                        } else {
4583                            gestaoSuccessNotify('Sucesso', `${msgPecasOk}.`, '', JSON.stringify(detalhesLote));
4584                        }
4585                    }
4586                    loadProcessosGestao();
4587                    checkboxes.forEach(cb => {
4588                        setProcessoCheckboxGestao(cb, false);
4589                    });
4590                    if (checkboxSelecionarTodos) {
4591                        checkboxSelecionarTodos.checked = false;
4592                        checkboxSelecionarTodos.indeterminate = false;
4593                    }
4594                } else if (response.status === 403 && data.error === 'PRO_REQUIRED') {
4595                    if (typeof openCogniJUSProModal === 'function') openCogniJUSProModal(data.msg || 'Esta funcionalidade requer assinatura PRO.');
4596                } else if (response.status === 403 && data.error === 'ITERATION_LIMIT') {
4597                    if (typeof openCogniJUSProModal === 'function') openCogniJUSProModal(__isEN
4598                        ? 'You reached the free weekly limit. Upgrade to PRO for unlimited use.'
4599                        : 'Você atingiu o limite semanal do plano gratuito. Assine o PRO para uso ilimitado.');
4600                } else if (response.status === 400) {
4601                    funcMsg('warning', 'Atenção', data.msg || 'Erro ao processar lote.', '', '', undefined, gestaoInsightOptions('batch_generate_bad_request', {
4602                        notifyAdmin: false,
4603                        category: 'validation',
4604                        errorCode: 'validation:batch_generate_bad_request',
4605                        context: { httpStatus: response.status, serverMessage: data.msg || data.error }
4606                    }));
4607                } else {
4608                    funcMsg('error', 'Erro', data.msg || 'Erro ao processar lote.', '', '', undefined, gestaoInsightOptions('batch_generate_http_error', {
4609                        category: 'server',
4610                        errorCode: `server:batch_generate_http_${response.status}`,
4611                        context: { httpStatus: response.status, serverMessage: data.msg || data.error }
4612                    }));
4613                }
4614            } catch (error) {
4615                funcMsg('error', 'Erro', 'Falha ao processar lote.', '', error && error.message ? error.message : '', undefined, gestaoInsightOptions('batch_generate_network_error', {
4616                    category: 'network',
4617                    errorCode: 'network:batch_generate_failed',
4618                    context: { errorName: error && error.name ? error.name : undefined }
4619                }));
4620            }
4621
4622            delete btnGerarPecasLote.dataset.loading;
4623            updateGerarPecasButtonText();
4624        });
4625    }
4626
4627    updateGerarPecasButtonText();
4628
4629    if (checkboxSelecionarTodos) {
4630        checkboxSelecionarTodos.addEventListener('change', (e) => {
4631            const checkboxes = gestaoQueryAll('.processo-checkbox');
4632            let skipped = 0;
4633
4634            if (e.target.checked) {
4635                const personaSelect = gestaoGetById('personaSelectGestao');
4636                if (!personaSelect || !personaSelect.value) {
4637                    e.target.checked = false;
4638                    warnPersonaRequiredGestao();
4639                    checkboxSelecionarTodos.indeterminate = false;
4640                    syncSelecionarTodosGestao();
4641                    return;
4642                }
4643
4644                checkboxes.forEach(cb => {
4645                    const row = cb.closest('tr');
4646                    if (rowHasPecaGestao(row)) {
4647                        setProcessoCheckboxGestao(cb, true);
4648                    } else {
4649                        setProcessoCheckboxGestao(cb, false);
4650                        skipped++;
4651                    }
4652                });
4653
4654                if (skipped > 0) {
4655                    funcMsg('warning', __L.attn, __isEN
4656                        ? (skipped === 1
4657                            ? '1 case was not selected because no draft was chosen. Select a draft in that row first.'
4658                            : `${skipped} cases were not selected because no draft was chosen. Select a draft in each row first.`)
4659                        : (skipped === 1
4660                            ? '1 processo não foi selecionado porque não tem peça definida. Escolha a peça na linha.'
4661                            : `${skipped} processos não foram selecionados porque não têm peça definida. Escolha a peça em cada linha.`), '', '', undefined, gestaoInsightOptions('select_all_skipped_missing_piece', {
4662                                errorCode: 'validation:piece_required',
4663                                context: { missingFields: ['piece'], skippedCount: skipped, attemptedSelected: checkboxes.length }
4664                            }));
4665                }
4666
4667                if (skipped === checkboxes.length) {
4668                    e.target.checked = false;
4669                }
4670            } else {
4671                checkboxes.forEach(cb => setProcessoCheckboxGestao(cb, false));
4672            }
4673
4674            checkboxSelecionarTodos.indeterminate = false;
4675            syncSelecionarTodosGestao();
4676        });
4677    }
4678}
4679
4680// Initialize filters
4681function initializeFiltersGestao() {
4682    const busca = gestaoGetById('buscaProcesso');
4683
4684    if (busca) {
4685        busca.addEventListener('input', debounceGestao(() => applyFiltersAndRenderGestao(true), 200));
4686    }
4687
4688    gestaoQueryAll('.gestao-processual-table th.sortable').forEach(th => {
4689        th.addEventListener('click', () => {
4690            const field = th.dataset.sortField;
4691            if (field) handleGestaoColumnSort(field);
4692        });
4693    });
4694
4695    updateGestaoSortIndicators();
4696
4697    // Persona change listener
4698    const personaSelect = gestaoGetById('personaSelectGestao');
4699    if (personaSelect) {
4700        personaSelect.addEventListener('change', function () {
4701            populateRowPecaSelectsGestao(this.value);
4702        });
4703    }
4704
4705    // Block peça selection without Persona; validate again on change as safety net
4706    document.addEventListener('mousedown', function (e) {
4707        if (!e.target.classList.contains('row-peca-select')) return;
4708        if (personaSelectedGestao()) return;
4709        e.preventDefault();
4710        e.stopPropagation();
4711        e.target.blur();
4712        warnPersonaRequiredGestao();
4713    }, true);
4714
4715    document.addEventListener('focusin', function (e) {
4716        if (gestaoBlockPecaFocus) return;
4717        if (!e.target.classList || !e.target.classList.contains('row-peca-select')) return;
4718        if (personaSelectedGestao()) return;
4719        e.target.blur();
4720        focusPersonaSelectGestao();
4721    }, true);
4722
4723    document.addEventListener('change', function (e) {
4724        if (e.target.classList.contains('row-additional-prompt') ||
4725            e.target.classList.contains('row-include-pages') ||
4726            e.target.classList.contains('row-anonymization')) {
4727            syncGestaoRowParamsFromDom(e.target.closest('tr'));
4728            return;
4729        }
4730
4731        if (e.target.classList.contains('row-peca-select')) {
4732            if (!personaSelectedGestao()) {
4733                revertPecaSelectGestao(e.target);
4734                warnPersonaRequiredGestao();
4735                return;
4736            }
4737
4738            const row = e.target.closest('tr');
4739            const checkbox = row ? row.querySelector('.processo-checkbox') : null;
4740            if (checkbox) {
4741                setProcessoCheckboxGestao(checkbox, !!e.target.value);
4742            } else if (row) {
4743                syncGestaoRowSelectionFromDom(row);
4744            }
4745            syncSelecionarTodosGestao();
4746            return;
4747        }
4748
4749        if (e.target.classList.contains('processo-checkbox')) {
4750            const row = e.target.closest('tr');
4751            const pecaSelect = row ? row.querySelector('.row-peca-select') : null;
4752
4753            if (e.target.checked && !rowHasPecaGestao(row)) {
4754                setProcessoCheckboxGestao(e.target, false);
4755
4756                const personaSelect = gestaoGetById('personaSelectGestao');
4757                if (!personaSelect || !personaSelect.value) {
4758                    warnPersonaRequiredGestao();
4759                } else {
4760                    funcMsg('warning', __L.attn, __isEN
4761                        ? 'Select a draft for this case before marking it.'
4762                        : 'Selecione a peça deste processo antes de marcá-lo.', '', '', undefined, gestaoInsightOptions('mark_process_without_piece', {
4763                            errorCode: 'validation:piece_required',
4764                            context: { missingFields: ['piece'], processoId: e.target.value || undefined }
4765                        }));
4766                    highlightPecaSelectGestao(pecaSelect);
4767                }
4768            } else {
4769                setProcessoCheckboxGestao(e.target, e.target.checked);
4770            }
4771
4772            syncSelecionarTodosGestao();
4773        }
4774    });
4775
4776    document.addEventListener('input', function (e) {
4777        if (e.target.classList.contains('row-additional-prompt') ||
4778            e.target.classList.contains('row-include-pages')) {
4779            syncGestaoRowParamsFromDom(e.target.closest('tr'));
4780        }
4781    });
4782}
4783
4784function debounceGestao(func, wait) {
4785    let timeout;
4786    return function executedFunction(...args) {
4787        const later = () => {
4788            clearTimeout(timeout);
4789            func.apply(this, args);
4790        };
4791        clearTimeout(timeout);
4792        timeout = setTimeout(later, wait);
4793    };
4794}
4795
4796function bootGestaoProcessosIfAuthed() {
4797    if (!localStorage.getItem('CogniJUS_User_Token')) return;
4798    ensureGestaoToastStack();
4799    rehydrateGestaoWatchedJobsFromSession();
4800    scheduleGestaoProcessosTableOverflowUpdate();
4801    loadProcessosGestao();
4802}
4803
4804// Initialize all Gestão Processual features on DOMContentLoaded
4805document.addEventListener('DOMContentLoaded', function () {
4806    initializeAssessorTabs();
4807    initializeUploadEventsGestao();
4808    initializeBatchGenerationGestao();
4809    initializeFiltersGestao();
4810    loadGestaoAssessorConfig();
4811
4812    bootGestaoProcessosIfAuthed();
4813
4814    document.addEventListener('cognijus-auth-ready', bootGestaoProcessosIfAuthed);
4815    document.addEventListener('cj-erp-assessor-mounted', bootGestaoProcessosIfAuthed);
4816
4817    // cognijus-auth.js chama executePendingQuery após o login no modal
4818    // (sem recarregar a página). Sem este hook a lista só populava no F5.
4819    const previousPendingQuery = typeof window.executePendingQuery === 'function'
4820        ? window.executePendingQuery
4821        : null;
4822    window.executePendingQuery = function () {
4823        if (previousPendingQuery) {
4824            try { previousPendingQuery(); } catch (e) { /* noop */ }
4825        }
4826        bootGestaoProcessosIfAuthed();
4827    };
4828
4829    document.addEventListener('visibilitychange', function () {
4830        if (document.visibilityState === 'visible' && localStorage.getItem('CogniJUS_User_Token')) {
4831            loadProcessosGestao();
4832        }
4833    });
4834
4835    if (window.AssessorVerify) {
4836        AssessorVerify.init({
4837            baseUrl: window.location.origin,
4838            getToken: function () { return localStorage.getItem('CogniJUS_User_Token'); },
4839            isEN: __isEN
4840        });
4841    }
4842});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.