Documentos Pacientes, Template Documentos Pacientes Saas, Documentos prontuários, Documentos Externos, Visualização Externa, Permissão de Visualização, Render Otimização

This commit is contained in:
Leonardo
2026-03-30 14:08:19 -03:00
parent 0658e2e9bf
commit d088a89fb7
112 changed files with 115867 additions and 5266 deletions
@@ -0,0 +1,207 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Arquivo: src/features/documents/components/DocumentTemplateEditor.vue
| Editor de template: edicao HTML, insercao de variaveis, preview ao vivo.
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, watch, computed } from 'vue'
import { useDocumentTemplates } from '../composables/useDocumentTemplates'
import JoditEmailEditor from '@/components/ui/JoditEmailEditor.vue'
const props = defineProps({
modelValue: { type: Object, default: () => ({}) },
mode: { type: String, default: 'create' } // create | edit
})
const emit = defineEmits(['update:modelValue', 'save', 'cancel'])
const { TIPOS_TEMPLATE, TEMPLATE_VARIABLES, variablesGrouped, previewHtml } = useDocumentTemplates()
const activeTab = ref('editor') // editor | preview
// ── Form reativo synced com modelValue ──────────────────────
const form = ref({ ...defaultForm(), ...props.modelValue })
function defaultForm() {
return {
nome_template: '',
tipo: 'outro',
descricao: '',
corpo_html: '',
cabecalho_html: '',
rodape_html: '',
variaveis: [],
logo_url: ''
}
}
watch(() => props.modelValue, (val) => {
form.value = { ...defaultForm(), ...val }
}, { deep: true })
watch(form, (val) => {
emit('update:modelValue', { ...val })
}, { deep: true })
// ── Preview ─────────────────────────────────────────────────
const renderedPreview = computed(() => previewHtml(form.value.corpo_html))
const renderedCabecalho = computed(() => previewHtml(form.value.cabecalho_html || ''))
const renderedRodape = computed(() => previewHtml(form.value.rodape_html || ''))
// ── Inserir variavel no corpo ───────────────────────────────
const cursorField = ref('corpo_html') // qual campo esta ativo
const editorCabecalho = ref(null)
const editorCorpo = ref(null)
const editorRodape = ref(null)
function insertVariable(varKey) {
const tag = `{{${varKey}}}`
const editorMap = {
cabecalho_html: editorCabecalho,
corpo_html: editorCorpo,
rodape_html: editorRodape
}
const editorRef = editorMap[cursorField.value]
if (editorRef?.value?.insertHTML) {
editorRef.value.insertHTML(tag)
} else {
form.value[cursorField.value] = (form.value[cursorField.value] || '') + tag
}
// Adiciona a variavel na lista se nao estiver
if (!form.value.variaveis.includes(varKey)) {
form.value.variaveis = [...form.value.variaveis, varKey]
}
}
// ── Save ────────────────────────────────────────────────────
function onSave() {
emit('save', { ...form.value })
}
</script>
<template>
<div class="flex flex-col gap-4">
<!-- Header: nome e tipo -->
<div class="grid grid-cols-1 sm:grid-cols-[1fr_200px] gap-3">
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-[var(--text-color-secondary)]">Nome do template</label>
<InputText v-model="form.nome_template" placeholder="Ex: Declaração de comparecimento" class="w-full" />
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-[var(--text-color-secondary)]">Tipo</label>
<Select
v-model="form.tipo"
:options="TIPOS_TEMPLATE"
optionLabel="label"
optionValue="value"
class="w-full"
/>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-[var(--text-color-secondary)]">Descrição</label>
<InputText v-model="form.descricao" placeholder="Breve descrição do template" class="w-full" />
</div>
<!-- Tabs: Editor / Preview -->
<div class="flex items-center gap-1 border-b border-[var(--surface-border)]">
<button
class="px-3 py-2 text-sm font-medium border-b-2 transition-colors"
:class="activeTab === 'editor' ? 'border-primary text-primary' : 'border-transparent text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'"
@click="activeTab = 'editor'"
>
Editor
</button>
<button
class="px-3 py-2 text-sm font-medium border-b-2 transition-colors"
:class="activeTab === 'preview' ? 'border-primary text-primary' : 'border-transparent text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'"
@click="activeTab = 'preview'"
>
Preview
</button>
</div>
<!-- Editor -->
<div v-show="activeTab === 'editor'" class="flex flex-col lg:flex-row gap-4">
<!-- Campos HTML -->
<div class="flex-1 flex flex-col gap-3">
<div class="flex flex-col gap-1" @focusin="cursorField = 'cabecalho_html'">
<label class="text-xs font-medium text-[var(--text-color-secondary)]">Cabeçalho</label>
<JoditEmailEditor ref="editorCabecalho" v-model="form.cabecalho_html" :minHeight="120" layoutButtons :logoUrl="form.logo_url" />
</div>
<div class="flex flex-col gap-1" @focusin="cursorField = 'corpo_html'">
<label class="text-xs font-medium text-[var(--text-color-secondary)]">Corpo do documento</label>
<JoditEmailEditor ref="editorCorpo" v-model="form.corpo_html" :minHeight="350" />
</div>
<div class="flex flex-col gap-1" @focusin="cursorField = 'rodape_html'">
<label class="text-xs font-medium text-[var(--text-color-secondary)]">Rodapé</label>
<JoditEmailEditor ref="editorRodape" v-model="form.rodape_html" :minHeight="120" layoutButtons :logoUrl="form.logo_url" />
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-[var(--text-color-secondary)]">URL do logo (opcional)</label>
<InputText v-model="form.logo_url" placeholder="https://..." class="w-full" />
</div>
</div>
<!-- Painel de variaveis -->
<div class="w-full lg:w-[220px] flex-shrink-0">
<div class="sticky top-0">
<div class="text-xs font-semibold uppercase tracking-wider text-[var(--text-color-secondary)] mb-2">
Variáveis
</div>
<div class="text-[0.65rem] text-[var(--text-color-secondary)] mb-3">
Clique para inserir no campo ativo
</div>
<div class="flex flex-col gap-3 max-h-[500px] overflow-y-auto pr-1">
<div v-for="(vars, grupo) in variablesGrouped" :key="grupo">
<div class="text-[0.65rem] font-semibold uppercase tracking-wider text-[var(--text-color-secondary)] mb-1">{{ grupo }}</div>
<div class="flex flex-col gap-0.5">
<button
v-for="v in vars"
:key="v.key"
class="text-left text-xs px-2 py-1 rounded hover:bg-primary/10 hover:text-primary transition-colors truncate"
:title="v.key"
@click="insertVariable(v.key)"
>
<span class="font-mono text-[0.65rem] opacity-60">&lbrace;&lbrace;</span>
{{ v.label }}
<span class="font-mono text-[0.65rem] opacity-60">&rbrace;&rbrace;</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Preview -->
<div v-show="activeTab === 'preview'" class="border border-[var(--surface-border)] rounded-lg bg-white overflow-hidden">
<div class="p-6 text-black" style="font-family: 'Segoe UI', Arial, sans-serif; font-size: 12pt; line-height: 1.6;">
<div v-if="form.cabecalho_html" class="text-center mb-4 pb-3 border-b border-gray-300" v-html="renderedCabecalho" />
<div class="min-h-[300px]" v-html="renderedPreview" />
<div v-if="form.rodape_html" class="mt-8 pt-3 border-t border-gray-300 text-center text-[10pt] text-gray-500" v-html="renderedRodape" />
</div>
</div>
<!-- Acoes -->
<div class="flex items-center justify-end gap-2 pt-2">
<Button label="Cancelar" text @click="emit('cancel')" />
<Button :label="mode === 'create' ? 'Criar template' : 'Salvar'" icon="pi pi-check" @click="onSave" />
</div>
</div>
</template>