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,297 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/documents/DocumentTemplatesPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import Menu from 'primevue/menu'
|
||||
|
||||
import { useDocumentTemplates } from './composables/useDocumentTemplates'
|
||||
import DocumentTemplateEditor from './components/DocumentTemplateEditor.vue'
|
||||
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
|
||||
const {
|
||||
templates, loading, error,
|
||||
globalTemplates, tenantTemplates,
|
||||
TIPOS_TEMPLATE,
|
||||
fetchTemplates, create, update, remove, duplicate
|
||||
} = useDocumentTemplates()
|
||||
|
||||
// ── Views ───────────────────────────────────────────────────
|
||||
|
||||
const view = ref('list') // list | create | edit
|
||||
const editingTemplate = ref({})
|
||||
const editingId = ref(null)
|
||||
|
||||
// ── Mobile menu ─────────────────────────────────────────────
|
||||
|
||||
const mobileMenuRef = ref(null)
|
||||
const mobileMenuItems = [
|
||||
{ label: 'Novo template', icon: 'pi pi-plus', command: () => openCreate() },
|
||||
{ separator: true },
|
||||
{ label: 'Atualizar', icon: 'pi pi-refresh', command: () => fetchTemplates(true) }
|
||||
]
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────
|
||||
|
||||
onMounted(() => fetchTemplates(true))
|
||||
|
||||
// ── Acoes ───────────────────────────────────────────────────
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
editingTemplate.value = {}
|
||||
view.value = 'create'
|
||||
}
|
||||
|
||||
function openEdit(tpl) {
|
||||
if (tpl.is_global) {
|
||||
toast.add({ severity: 'warn', summary: 'Somente leitura', detail: 'Templates padrão não podem ser editados. Duplique para personalizar.', life: 3000 })
|
||||
return
|
||||
}
|
||||
editingId.value = tpl.id
|
||||
editingTemplate.value = { ...tpl }
|
||||
view.value = 'edit'
|
||||
}
|
||||
|
||||
async function onSave(payload) {
|
||||
try {
|
||||
if (view.value === 'create') {
|
||||
await create(payload)
|
||||
toast.add({ severity: 'success', summary: 'Criado', detail: payload.nome_template, life: 3000 })
|
||||
} else {
|
||||
await update(editingId.value, payload)
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: payload.nome_template, life: 3000 })
|
||||
}
|
||||
view.value = 'list'
|
||||
fetchTemplates(true)
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message })
|
||||
}
|
||||
}
|
||||
|
||||
function onDuplicate(tpl) {
|
||||
confirm.require({
|
||||
message: `Deseja copiar "${tpl.nome_template}" para os seus templates? Você poderá editá-lo livremente.`,
|
||||
header: 'Duplicar template',
|
||||
icon: 'pi pi-copy',
|
||||
acceptLabel: 'Copiar',
|
||||
rejectLabel: 'Cancelar',
|
||||
accept: async () => {
|
||||
try {
|
||||
await duplicate(tpl.id)
|
||||
toast.add({ severity: 'success', summary: 'Duplicado', detail: `"${tpl.nome_template}" copiado para Meus Templates.`, life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onDelete(tpl) {
|
||||
confirm.require({
|
||||
message: `Desativar template "${tpl.nome_template}"?`,
|
||||
header: 'Confirmar',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
accept: async () => {
|
||||
try {
|
||||
await remove(tpl.id)
|
||||
toast.add({ severity: 'success', summary: 'Desativado', life: 2000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
view.value = 'list'
|
||||
}
|
||||
|
||||
// ── Tipo label ──────────────────────────────────────────────
|
||||
|
||||
function tipoLabel(tipo) {
|
||||
return TIPOS_TEMPLATE.find(t => t.value === tipo)?.label || tipo
|
||||
}
|
||||
|
||||
// ── Template card menu ──────────────────────────────────────
|
||||
|
||||
function getCardMenuItems(tpl) {
|
||||
const items = [
|
||||
{ label: 'Duplicar', icon: 'pi pi-copy', command: () => onDuplicate(tpl) }
|
||||
]
|
||||
if (!tpl.is_global) {
|
||||
items.push(
|
||||
{ label: 'Editar', icon: 'pi pi-pencil', command: () => openEdit(tpl) },
|
||||
{ separator: true },
|
||||
{ label: 'Desativar', icon: 'pi pi-trash', class: 'text-red-500', command: () => onDelete(tpl) }
|
||||
)
|
||||
}
|
||||
return items
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-4 py-6 max-w-[1200px] mx-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-6">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="view !== 'list'"
|
||||
icon="pi pi-arrow-left"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
@click="view = 'list'"
|
||||
/>
|
||||
<h1 class="text-xl font-bold">
|
||||
<template v-if="view === 'list'">Templates de documentos</template>
|
||||
<template v-else-if="view === 'create'">Novo template</template>
|
||||
<template v-else>Editar template</template>
|
||||
</h1>
|
||||
</div>
|
||||
<p v-if="view === 'list'" class="text-sm text-[var(--text-color-secondary)]">
|
||||
Modelos para declarações, atestados, recibos e outros documentos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="view === 'list'" class="hidden sm:flex items-center gap-2">
|
||||
<Button label="Novo template" icon="pi pi-plus" size="small" @click="openCreate" />
|
||||
</div>
|
||||
<div v-if="view === 'list'" class="sm:hidden">
|
||||
<Button icon="pi pi-ellipsis-v" text rounded @click="mobileMenuRef.toggle($event)" />
|
||||
<Menu ref="mobileMenuRef" :model="mobileMenuItems" :popup="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List view -->
|
||||
<template v-if="view === 'list'">
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-16">
|
||||
<i class="pi pi-spinner pi-spin text-2xl text-[var(--text-color-secondary)]" />
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div v-else-if="!templates.length" class="flex flex-col items-center justify-center py-16 text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-file-edit text-4xl opacity-30 mb-3" />
|
||||
<div class="text-sm mb-1">Nenhum template encontrado.</div>
|
||||
<Button label="Criar primeiro template" icon="pi pi-plus" text size="small" class="mt-2" @click="openCreate" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Templates globais (padrao) -->
|
||||
<div v-if="globalTemplates.length" class="mb-6">
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-[var(--text-color-secondary)] mb-3">
|
||||
Templates padrão do sistema
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<div
|
||||
v-for="tpl in globalTemplates"
|
||||
:key="tpl.id"
|
||||
class="group relative flex flex-col p-4 rounded-lg border border-[var(--surface-border)] hover:border-[var(--surface-400)] hover:bg-[var(--surface-hover)] transition-all cursor-pointer"
|
||||
@click="onDuplicate(tpl)"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="flex-shrink-0 w-9 h-9 rounded-lg bg-blue-500/10 flex items-center justify-center">
|
||||
<i class="pi pi-file text-blue-500" />
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium">{{ tpl.nome_template }}</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)] mt-0.5">{{ tipoLabel(tpl.tipo) }}</div>
|
||||
<div v-if="tpl.descricao" class="text-xs text-[var(--text-color-secondary)] mt-1 line-clamp-2">{{ tpl.descricao }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="absolute top-2 right-2 text-[0.6rem] px-1.5 py-0.5 rounded-full bg-blue-500/10 text-blue-600">
|
||||
padrão
|
||||
</span>
|
||||
<div class="mt-2 text-[0.65rem] text-[var(--text-color-secondary)] opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Clique para duplicar e personalizar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Templates do tenant -->
|
||||
<div v-if="tenantTemplates.length">
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-[var(--text-color-secondary)] mb-3">
|
||||
Meus templates
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<div
|
||||
v-for="tpl in tenantTemplates"
|
||||
:key="tpl.id"
|
||||
class="group relative flex flex-col p-4 rounded-lg border border-[var(--surface-border)] hover:border-primary hover:bg-primary/5 transition-all cursor-pointer"
|
||||
@click="openEdit(tpl)"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="flex-shrink-0 w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<i class="pi pi-file-edit text-primary" />
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium">{{ tpl.nome_template }}</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)] mt-0.5">{{ tipoLabel(tpl.tipo) }}</div>
|
||||
<div v-if="tpl.descricao" class="text-xs text-[var(--text-color-secondary)] mt-1 line-clamp-2">{{ tpl.descricao }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu de acoes -->
|
||||
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
icon="pi pi-ellipsis-v"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="!w-7 !h-7"
|
||||
@click.stop="$refs[`menu_${tpl.id}`]?.[0]?.toggle($event)"
|
||||
/>
|
||||
<Menu :ref="`menu_${tpl.id}`" :model="getCardMenuItems(tpl)" :popup="true" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span
|
||||
v-if="!tpl.ativo"
|
||||
class="text-[0.6rem] px-1.5 py-0.5 rounded-full bg-red-500/10 text-red-500"
|
||||
>
|
||||
inativo
|
||||
</span>
|
||||
<span class="text-[0.6rem] text-[var(--text-color-secondary)]">
|
||||
{{ tpl.variaveis?.length || 0 }} variáveis
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Create / Edit view -->
|
||||
<template v-if="view === 'create' || view === 'edit'">
|
||||
<DocumentTemplateEditor
|
||||
v-model="editingTemplate"
|
||||
:mode="view"
|
||||
@save="onSave"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ConfirmDialog />
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user