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:
245
src/features/documents/components/DocumentShareDialog.vue
Normal file
245
src/features/documents/components/DocumentShareDialog.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Arquivo: src/features/documents/components/DocumentShareDialog.vue
|
||||
| Gerar link temporario para compartilhamento externo.
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import {
|
||||
createShareLink,
|
||||
listShareLinks,
|
||||
deactivateShareLink,
|
||||
buildShareUrl
|
||||
} from '@/services/DocumentShareLinks.service'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
doc: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
const links = ref([])
|
||||
|
||||
const OPCOES_EXPIRACAO = [
|
||||
{ value: 24, label: '24 horas' },
|
||||
{ value: 48, label: '48 horas' },
|
||||
{ value: 168, label: '7 dias' },
|
||||
{ value: 720, label: '30 dias' }
|
||||
]
|
||||
|
||||
const formExpiracao = ref(48)
|
||||
const formUsosMax = ref(5)
|
||||
|
||||
// ── Reset ao abrir ──────────────────────────────────────────
|
||||
|
||||
watch(() => props.visible, async (v) => {
|
||||
if (v && props.doc) {
|
||||
formExpiracao.value = 48
|
||||
formUsosMax.value = 5
|
||||
await fetchLinks()
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchLinks() {
|
||||
loading.value = true
|
||||
try {
|
||||
links.value = await listShareLinks(props.doc.id)
|
||||
} catch {
|
||||
links.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Criar link ──────────────────────────────────────────────
|
||||
|
||||
async function criarLink() {
|
||||
creating.value = true
|
||||
try {
|
||||
const link = await createShareLink(props.doc.id, {
|
||||
expiracaoHoras: formExpiracao.value,
|
||||
usosMax: formUsosMax.value
|
||||
})
|
||||
links.value.unshift(link)
|
||||
toast.add({ severity: 'success', summary: 'Link criado', detail: 'Link copiado para a área de transferência.', life: 3000 })
|
||||
copyUrl(link.token)
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao criar link.' })
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Copiar URL ──────────────────────────────────────────────
|
||||
|
||||
function copyUrl(token) {
|
||||
const url = buildShareUrl(token)
|
||||
navigator.clipboard.writeText(url).catch(() => {})
|
||||
}
|
||||
|
||||
// ── Desativar link ──────────────────────────────────────────
|
||||
|
||||
async function desativar(linkId) {
|
||||
try {
|
||||
await deactivateShareLink(linkId)
|
||||
const idx = links.value.findIndex(l => l.id === linkId)
|
||||
if (idx >= 0) links.value[idx].ativo = false
|
||||
toast.add({ severity: 'info', summary: 'Link desativado', life: 2000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message })
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
|
||||
function isExpired(link) {
|
||||
return new Date(link.expira_em) < new Date()
|
||||
}
|
||||
|
||||
function isExhausted(link) {
|
||||
return link.usos >= link.usos_max
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return '—'
|
||||
return new Date(d).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:visible="visible"
|
||||
@update:visible="$emit('update:visible', $event)"
|
||||
modal
|
||||
:draggable="false"
|
||||
class="w-[36rem]"
|
||||
:breakpoints="{ '768px': '94vw' }"
|
||||
:pt="{
|
||||
header: { class: '!p-4 !rounded-t-xl border-b border-[var(--surface-border)]' },
|
||||
content: { class: '!p-4' },
|
||||
footer: { class: '!p-3 !rounded-b-xl border-t border-[var(--surface-border)]' }
|
||||
}"
|
||||
pt:mask:class="backdrop-blur-xs"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="flex items-center justify-center w-8 h-8 rounded-lg bg-orange-500/10">
|
||||
<i class="pi pi-share-alt text-orange-500" />
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-base font-semibold">Compartilhar documento</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)] truncate max-w-[300px]">{{ doc?.nome_original }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Criar novo link -->
|
||||
<div class="p-3 rounded-lg border border-[var(--surface-border)] bg-[var(--surface-ground)]">
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-[var(--text-color-secondary)] mb-3">Novo link</div>
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-xs text-[var(--text-color-secondary)]">Expira em</label>
|
||||
<Select
|
||||
v-model="formExpiracao"
|
||||
:options="OPCOES_EXPIRACAO"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-xs text-[var(--text-color-secondary)]">Limite de acessos</label>
|
||||
<InputNumber v-model="formUsosMax" :min="1" :max="100" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
label="Gerar link"
|
||||
icon="pi pi-link"
|
||||
size="small"
|
||||
:loading="creating"
|
||||
@click="criarLink"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Links existentes -->
|
||||
<div v-if="links.length">
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-[var(--text-color-secondary)] mb-2">Links criados</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 max-h-[250px] overflow-y-auto">
|
||||
<div
|
||||
v-for="link in links"
|
||||
:key="link.id"
|
||||
class="flex items-center gap-2 p-2.5 rounded-md border border-[var(--surface-border)]"
|
||||
:class="{ 'opacity-50': !link.ativo || isExpired(link) || isExhausted(link) }"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
!link.ativo ? 'pi pi-ban text-gray-400' :
|
||||
isExpired(link) ? 'pi pi-clock text-red-400' :
|
||||
isExhausted(link) ? 'pi pi-exclamation-circle text-amber-400' :
|
||||
'pi pi-link text-green-500'
|
||||
"
|
||||
class="text-sm flex-shrink-0"
|
||||
/>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-xs text-[var(--text-color-secondary)]">
|
||||
Expira: {{ formatDate(link.expira_em) }}
|
||||
</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)]">
|
||||
Usos: {{ link.usos }}/{{ link.usos_max }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
v-if="link.ativo && !isExpired(link)"
|
||||
icon="pi pi-copy"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="!w-7 !h-7"
|
||||
v-tooltip.top="'Copiar link'"
|
||||
@click="copyUrl(link.token)"
|
||||
/>
|
||||
<Button
|
||||
v-if="link.ativo"
|
||||
icon="pi pi-ban"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
class="!w-7 !h-7"
|
||||
v-tooltip.top="'Desativar'"
|
||||
@click="desativar(link.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<Button label="Fechar" text @click="close" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user