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:
@@ -0,0 +1,203 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/components/ui/JoditEmailEditor.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { Jodit } from 'jodit/esm/index.js';
|
||||
import 'jodit/es2021/jodit.min.css';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
minHeight: { type: Number, default: 150 },
|
||||
// true → toolbar enxuta + botões ▣ de layout para header/footer
|
||||
layoutButtons: { type: Boolean, default: false },
|
||||
// URL da logo do tenant usada nos snippets de layout
|
||||
logoUrl: { type: String, default: null }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const container = ref(null);
|
||||
let jodit = null;
|
||||
let _ignoreChange = false;
|
||||
let _themeObserver = null;
|
||||
|
||||
// ── Dark mode ─────────────────────────────────────────────────
|
||||
function isDark() {
|
||||
return document.documentElement.classList.contains('app-dark');
|
||||
}
|
||||
|
||||
// ── Snippets de layout ────────────────────────────────────────
|
||||
function logoSnippet(url) {
|
||||
return url
|
||||
? `<img src="${url}" width="72" height="72" style="display:block;object-fit:contain;border-radius:4px;" alt="Logo" />`
|
||||
: `<div style="width:72px;height:72px;background:#e5e7eb;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:10px;color:#9ca3af;">[logo]</div>`;
|
||||
}
|
||||
|
||||
function snippetLogoLeft(logo) {
|
||||
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
||||
<tr>
|
||||
<td width="88" valign="middle" style="padding-right:16px;">${logoSnippet(logo)}</td>
|
||||
<td valign="middle"><p style="margin:0;font-size:14px;color:#374151;">Seu texto aqui</p></td>
|
||||
</tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function snippetLogoRight(logo) {
|
||||
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
||||
<tr>
|
||||
<td valign="middle" style="padding-right:16px;"><p style="margin:0;font-size:14px;color:#374151;">Seu texto aqui</p></td>
|
||||
<td width="88" valign="middle" style="text-align:right;">${logoSnippet(logo)}</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function snippetLogoCenter(logo) {
|
||||
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom:8px;">${logoSnippet(logo)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><p style="margin:0;font-size:14px;color:#374151;">Seu texto aqui</p></td>
|
||||
</tr>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// ── Config Jodit ─────────────────────────────────────────────
|
||||
function buildConfig() {
|
||||
// Botões customizados de layout (somente nos editores de header/footer)
|
||||
const layoutExtraButtons = props.layoutButtons
|
||||
? [
|
||||
{
|
||||
name: 'layout-logo-left',
|
||||
tooltip: 'Logo à esquerda, texto à direita',
|
||||
text: '▣ Logo Esq.',
|
||||
exec(editor) {
|
||||
editor.selection.insertHTML(snippetLogoLeft(props.logoUrl));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'layout-logo-right',
|
||||
tooltip: 'Logo à direita, texto à esquerda',
|
||||
text: '▣ Logo Dir.',
|
||||
exec(editor) {
|
||||
editor.selection.insertHTML(snippetLogoRight(props.logoUrl));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'layout-logo-center',
|
||||
tooltip: 'Logo centralizada, texto abaixo',
|
||||
text: '▣ Logo Centro',
|
||||
exec(editor) {
|
||||
editor.selection.insertHTML(snippetLogoCenter(props.logoUrl));
|
||||
}
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
// Toolbar enxuta para header/footer — sem hr, eraser, source
|
||||
const layoutButtons = [
|
||||
'bold', 'italic', 'underline', '|',
|
||||
'font', 'fontsize', 'brush', '|',
|
||||
'align', '|',
|
||||
'link', '|',
|
||||
'layout-logo-left', 'layout-logo-right', 'layout-logo-center'
|
||||
];
|
||||
|
||||
// Toolbar completa para o corpo do e-mail
|
||||
const bodyButtons = [
|
||||
'bold', 'italic', 'underline', 'strikethrough', '|',
|
||||
'ul', 'ol', '|',
|
||||
'font', 'fontsize', 'brush', 'paragraph', '|',
|
||||
'align', '|',
|
||||
'link', 'table', '|',
|
||||
'hr', 'eraser', '|',
|
||||
'source'
|
||||
];
|
||||
|
||||
return {
|
||||
height: props.minHeight,
|
||||
language: 'pt_br',
|
||||
theme: isDark() ? 'dark' : 'default',
|
||||
toolbarAdaptive: false,
|
||||
toolbarSticky: false,
|
||||
showCharsCounter: false,
|
||||
showWordsCounter: false,
|
||||
showXPathInStatusbar: false,
|
||||
disablePlugins: ['about', 'stat'],
|
||||
buttons: props.layoutButtons ? layoutButtons : bodyButtons,
|
||||
extraButtons: layoutExtraButtons,
|
||||
uploader: { insertImageAsBase64URI: false },
|
||||
filebrowser: { ajax: { url: '' } }
|
||||
};
|
||||
}
|
||||
|
||||
// ── Init / destroy ────────────────────────────────────────────
|
||||
function initJodit() {
|
||||
if (jodit) {
|
||||
jodit.destruct();
|
||||
jodit = null;
|
||||
}
|
||||
jodit = Jodit.make(container.value, buildConfig());
|
||||
if (props.modelValue) jodit.value = props.modelValue;
|
||||
jodit.events.on('change', (content) => {
|
||||
if (!_ignoreChange) emit('update:modelValue', content);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────
|
||||
onMounted(() => {
|
||||
initJodit();
|
||||
|
||||
// Recria o editor se o tema mudar enquanto o componente estiver montado
|
||||
_themeObserver = new MutationObserver(() => {
|
||||
const current = isDark() ? 'dark' : 'default';
|
||||
if (jodit && jodit.o?.theme !== current) {
|
||||
const saved = jodit.value;
|
||||
initJodit();
|
||||
if (saved) jodit.value = saved;
|
||||
}
|
||||
});
|
||||
_themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
_themeObserver?.disconnect();
|
||||
_themeObserver = null;
|
||||
jodit?.destruct();
|
||||
jodit = null;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (!jodit) return;
|
||||
if (jodit.value !== (val ?? '')) {
|
||||
_ignoreChange = true;
|
||||
jodit.value = val ?? '';
|
||||
_ignoreChange = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ── API exposta ───────────────────────────────────────────────
|
||||
defineExpose({
|
||||
insertHTML: (html) => jodit?.selection.insertHTML(html)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container" />
|
||||
</template>
|
||||
@@ -31,10 +31,8 @@ import { ref, computed } from 'vue';
|
||||
import Popover from 'primevue/popover';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import PatientCadastroDialog from './PatientCadastroDialog.vue';
|
||||
|
||||
const emit = defineEmits(['quick-create']);
|
||||
const showCadastroDialog = ref(false);
|
||||
const emit = defineEmits(['quick-create', 'go-complete', 'show', 'hide']);
|
||||
const toast = useToast();
|
||||
|
||||
const popRef = ref(null);
|
||||
@@ -83,7 +81,7 @@ function onQuickCreate() {
|
||||
}
|
||||
function onGoComplete() {
|
||||
close();
|
||||
showCadastroDialog.value = true;
|
||||
emit('go-complete');
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
@@ -114,9 +112,7 @@ defineExpose({ toggle, close });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PatientCadastroDialog v-model="showCadastroDialog" />
|
||||
|
||||
<Popover ref="popRef">
|
||||
<Popover ref="popRef" @show="emit('show')" @hide="emit('hide')">
|
||||
<div class="flex flex-col min-w-[230px]">
|
||||
<!-- Cadastro rápido -->
|
||||
<button class="flex items-center gap-2.5 px-3 py-2.5 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]" @click="onQuickCreate">
|
||||
|
||||
Reference in New Issue
Block a user