mesa-react / frontend /src /components /AnexosModal.jsx
Guilherme Silberfarb Costa
alteracoes nos mapas e no carregamento de planilha
fef776b
Raw
History Blame Contribute Delete
10.4 kB
import React, { useEffect, useMemo, useState } from 'react'
import { api, downloadBlob } from '../api'
function normalizarChave(value) {
return String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
.replace(/\.dai$/i, '')
}
function resolverModeloId(chave, modelos) {
const normalizada = normalizarChave(chave)
if (!normalizada) return ''
for (const modelo of modelos || []) {
const candidatos = [modelo?.id, modelo?.arquivo, modelo?.nome_modelo]
.map(normalizarChave)
.filter(Boolean)
if (candidatos.includes(normalizada)) return String(modelo.id || '')
}
return ''
}
function formatarFonte(fonte) {
if (!fonte || typeof fonte !== 'object') return ''
if (String(fonte.provider || '').toLowerCase() === 'hf_dataset') {
const repo = String(fonte.repo_id || '').trim()
const revisao = String(fonte.revision || '').trim()
return `HF Dataset${repo ? ` (${repo})` : ''}${revisao ? ` | revisão ${revisao.slice(0, 8)}` : ''}`
}
return 'Pasta local'
}
export default function AnexosModal({
open,
defaultModeloId = '',
avaliacao = null,
lockModelo = false,
title = 'Gerar anexos',
onClose,
}) {
const [modelos, setModelos] = useState([])
const [fonte, setFonte] = useState(null)
const [modeloSelecionado, setModeloSelecionado] = useState('')
const [colunas, setColunas] = useState([])
const [loadingModelos, setLoadingModelos] = useState(false)
const [loadingColunas, setLoadingColunas] = useState(false)
const [generating, setGenerating] = useState(false)
const [downloading, setDownloading] = useState(false)
const [error, setError] = useState('')
const [result, setResult] = useState(null)
const colunasSelecionadas = useMemo(
() => colunas.filter((item) => item.selected).map((item) => item.name),
[colunas],
)
const busy = loadingModelos || loadingColunas || generating || downloading
const blocking = generating || downloading
const canGenerate = Boolean(modeloSelecionado && colunasSelecionadas.length && !busy)
useEffect(() => {
if (!open) return undefined
let active = true
setLoadingModelos(true)
setError('')
setResult(null)
setColunas([])
setModeloSelecionado('')
api.anexosModelos()
.then((response) => {
if (!active) return
const lista = Array.isArray(response?.modelos) ? response.modelos : []
const resolvido = resolverModeloId(defaultModeloId, lista)
setModelos(lista)
setFonte(response?.fonte || null)
if (lockModelo && defaultModeloId && !resolvido) {
setModeloSelecionado('')
setError('O modelo usado nesta avaliação não foi encontrado no repositório configurado.')
return
}
setModeloSelecionado(resolvido || String(lista[0]?.id || ''))
})
.catch((err) => {
if (!active) return
setModelos([])
setModeloSelecionado('')
setError(err?.message || 'Não foi possível carregar os modelos.')
})
.finally(() => {
if (active) setLoadingModelos(false)
})
return () => {
active = false
}
}, [open, defaultModeloId, lockModelo])
useEffect(() => {
if (!open || !modeloSelecionado) {
setColunas([])
return undefined
}
let active = true
setLoadingColunas(true)
setError('')
setResult(null)
api.anexosModeloColunas(modeloSelecionado)
.then((response) => {
if (!active) return
setColunas((response?.columns || []).map((name) => ({ name, selected: true })))
})
.catch((err) => {
if (!active) return
setColunas([])
setError(err?.message || 'Não foi possível carregar as colunas do modelo.')
})
.finally(() => {
if (active) setLoadingColunas(false)
})
return () => {
active = false
}
}, [open, modeloSelecionado])
if (!open) return null
function toggleColuna(nome) {
setColunas((current) => current.map((item) => (
item.name === nome ? { ...item, selected: !item.selected } : item
)))
setResult(null)
}
function moverColuna(index, direction) {
setColunas((current) => {
const destino = index + direction
if (destino < 0 || destino >= current.length) return current
const next = [...current]
;[next[index], next[destino]] = [next[destino], next[index]]
return next
})
setResult(null)
}
async function gerar(event) {
event.preventDefault()
if (!canGenerate) return
setGenerating(true)
setError('')
setResult(null)
try {
const response = await api.anexosGerar(
modeloSelecionado,
colunasSelecionadas,
avaliacao,
)
setResult(response)
} catch (err) {
setError(err?.message || 'Falha ao gerar os anexos.')
} finally {
setGenerating(false)
}
}
async function baixar() {
const filename = String(result?.filename || '').trim()
if (!filename) return
setDownloading(true)
setError('')
try {
const blob = await api.anexosDownload(filename)
downloadBlob(blob, filename)
} catch (err) {
setError(err?.message || 'Falha ao baixar os anexos.')
} finally {
setDownloading(false)
}
}
function fechar() {
if (!blocking && typeof onClose === 'function') onClose()
}
return (
<div className="pesquisa-modal-backdrop" onClick={(event) => {
if (event.target === event.currentTarget) fechar()
}}>
<div className="pesquisa-modal anexos-modal" role="dialog" aria-modal="true" aria-label={title}>
<div className="pesquisa-modal-head">
<div>
<h4>{title}</h4>
{fonte ? <p>Fonte: {formatarFonte(fonte)}</p> : null}
</div>
<button type="button" className="pesquisa-modal-close" onClick={fechar} disabled={blocking}>
Fechar
</button>
</div>
<form className="pesquisa-modal-body anexos-modal-body" onSubmit={gerar}>
<label className="anexos-modal-model-field">
Modelo de avaliação
<select
value={modeloSelecionado}
onChange={(event) => setModeloSelecionado(event.target.value)}
disabled={busy || lockModelo || !modelos.length}
>
{!modelos.length ? <option value="">Nenhum modelo encontrado</option> : null}
{modelos.map((modelo) => (
<option key={String(modelo.id)} value={String(modelo.id)}>
{modelo.nome_modelo || modelo.arquivo || modelo.id}
</option>
))}
</select>
</label>
<section className="anexos-columns-section">
<div className="anexos-columns-head">
<div>
<strong>Colunas do banco de dados</strong>
<span>{colunasSelecionadas.length} selecionadas</span>
</div>
<div className="anexos-columns-actions">
<button
type="button"
onClick={() => setColunas((current) => current.map((item) => ({ ...item, selected: true })))}
disabled={busy}
>
Todas
</button>
<button
type="button"
onClick={() => setColunas((current) => current.map((item) => ({ ...item, selected: false })))}
disabled={busy}
>
Nenhuma
</button>
</div>
</div>
{loadingColunas ? (
<div className="section1-empty-hint">Carregando colunas...</div>
) : (
<div className="anexos-columns-list">
{colunas.map((coluna, index) => (
<div className="anexos-column-row" key={coluna.name}>
<label>
<input
type="checkbox"
checked={coluna.selected}
onChange={() => toggleColuna(coluna.name)}
disabled={busy}
/>
<span>{coluna.name}</span>
</label>
<div className="anexos-reorder-actions">
<button
type="button"
title="Mover para cima"
aria-label={`Mover ${coluna.name} para cima`}
onClick={() => moverColuna(index, -1)}
disabled={busy || index === 0}
>
</button>
<button
type="button"
title="Mover para baixo"
aria-label={`Mover ${coluna.name} para baixo`}
onClick={() => moverColuna(index, 1)}
disabled={busy || index === colunas.length - 1}
>
</button>
</div>
</div>
))}
</div>
)}
</section>
{error ? <div className="error-line inline-error">{error}</div> : null}
{result?.statistical_warnings?.length ? (
<div className="anexos-warning-panel">
<strong>Atenção</strong>
<ul>
{result.statistical_warnings.map((warning) => <li key={warning}>{warning}</li>)}
</ul>
</div>
) : null}
{result ? (
<div className="anexos-result-panel">
<div>
<strong>{result.message}</strong>
<span>{result.filename}</span>
</div>
<button type="button" onClick={() => void baixar()} disabled={downloading}>
{downloading ? 'Baixando...' : 'Baixar DOCX'}
</button>
</div>
) : null}
<div className="anexos-modal-footer">
<button type="submit" disabled={!canGenerate}>
{generating ? 'Gerando...' : 'Gerar DOCX'}
</button>
</div>
</form>
</div>
</div>
)
}