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
+203
View File
@@ -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>
+3 -7
View File
@@ -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">