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:
@@ -31,6 +31,7 @@ import Select from 'primevue/select';
|
||||
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { digitsOnly, isValidEmail, toISODate, generateCPF, fmtCPF } from '@/utils/validators';
|
||||
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
@@ -48,37 +49,6 @@ function cleanStr(v) {
|
||||
const s = String(v ?? '').trim();
|
||||
return s ? s : null;
|
||||
}
|
||||
function digitsOnly(v) {
|
||||
const d = String(v ?? '').replace(/\D/g, '');
|
||||
return d ? d : null;
|
||||
}
|
||||
function isValidEmail(v) {
|
||||
const s = String(v ?? '').trim();
|
||||
if (!s) return false;
|
||||
return /.+@.+\..+/.test(s);
|
||||
}
|
||||
|
||||
// DD-MM-YYYY -> YYYY-MM-DD
|
||||
function parseDDMMYYYY(s) {
|
||||
const str = String(s || '').trim();
|
||||
const m = /^(\d{2})-(\d{2})-(\d{4})$/.exec(str);
|
||||
if (!m) return null;
|
||||
const dd = Number(m[1]);
|
||||
const mm = Number(m[2]);
|
||||
const yyyy = Number(m[3]);
|
||||
const dt = new Date(yyyy, mm - 1, dd);
|
||||
if (Number.isNaN(dt.getTime())) return null;
|
||||
if (dt.getFullYear() !== yyyy || dt.getMonth() !== mm - 1 || dt.getDate() !== dd) return null;
|
||||
return dt;
|
||||
}
|
||||
function toISODateFromDDMMYYYY(s) {
|
||||
const dt = parseDDMMYYYY(s);
|
||||
if (!dt) return null;
|
||||
const yyyy = String(dt.getFullYear()).padStart(4, '0');
|
||||
const mm = String(dt.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(dt.getDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Mock fill (Preencher tudo)
|
||||
@@ -112,18 +82,8 @@ function randomPhoneBR() {
|
||||
return `(${ddd}) ${p1}-${p2}`;
|
||||
}
|
||||
|
||||
function generateCPF() {
|
||||
const n = Array.from({ length: 9 }, () => randInt(0, 9));
|
||||
const calcDV = (base) => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < base.length; i++) sum += base[i] * (base.length + 1 - i);
|
||||
const mod = sum % 11;
|
||||
return mod < 2 ? 0 : 11 - mod;
|
||||
};
|
||||
const d1 = calcDV(n);
|
||||
const d2 = calcDV([...n, d1]);
|
||||
const cpf = [...n, d1, d2].join('');
|
||||
return cpf.replace(/^(\d{3})(\d{3})(\d{3})(\d{2})$/, '$1.$2.$3-$4');
|
||||
function generateCPFFormatted() {
|
||||
return fmtCPF(generateCPF());
|
||||
}
|
||||
|
||||
function generateRG() {
|
||||
@@ -170,7 +130,7 @@ function preencherMock() {
|
||||
form.telefone = randomPhoneBR();
|
||||
form.telefone_alternativo = maybe(0.35) ? randomPhoneBR() : '';
|
||||
|
||||
form.cpf = generateCPF();
|
||||
form.cpf = generateCPFFormatted();
|
||||
form.rg = generateRG();
|
||||
|
||||
form.observacoes = maybe(0.5) ? 'Cadastro realizado via link externo.' : 'Tenho disponibilidade no período da noite.';
|
||||
@@ -199,7 +159,7 @@ function preencherMock() {
|
||||
const temResponsavel = maybe(0.35);
|
||||
form.nome_responsavel = temResponsavel ? `${pick(first)} ${pick(last)} ${pick(last)}` : '';
|
||||
form.telefone_responsavel = temResponsavel ? randomPhoneBR() : '';
|
||||
form.cpf_responsavel = temResponsavel ? generateCPF() : '';
|
||||
form.cpf_responsavel = temResponsavel ? generateCPFFormatted() : '';
|
||||
form.observacao_responsavel = temResponsavel ? 'Responsável ciente e de acordo com o cadastro.' : '';
|
||||
form.cobranca_no_responsavel = temResponsavel ? maybe(0.5) : false;
|
||||
|
||||
@@ -494,7 +454,7 @@ async function enviar() {
|
||||
return;
|
||||
}
|
||||
|
||||
const isoBirth = toISODateFromDDMMYYYY(form.data_nascimento);
|
||||
const isoBirth = toISODate(form.data_nascimento);
|
||||
if (cleanStr(form.data_nascimento) && !isoBirth) {
|
||||
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Data de nascimento inválida (use DD-MM-AAAA).', life: 2500 });
|
||||
openPanel(0);
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Arquivo: src/views/pages/public/SharedDocumentPage.vue
|
||||
| Pagina publica para visualizar documento compartilhado via link temporario.
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { validateShareToken } from '@/services/DocumentShareLinks.service'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const doc = ref(null)
|
||||
const previewUrl = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
const token = route.params.token
|
||||
if (!token) {
|
||||
error.value = 'Link inválido.'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await validateShareToken(token)
|
||||
if (!result?.document) {
|
||||
error.value = 'Este link expirou, atingiu o limite de acessos ou é inválido.'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
doc.value = result.document
|
||||
|
||||
// Gerar URL assinada para download/visualizacao
|
||||
const bucket = result.document.storage_bucket || 'documents'
|
||||
const { data, error: storageErr } = await supabase.storage
|
||||
.from(bucket)
|
||||
.createSignedUrl(result.document.bucket_path, 300) // 5 min
|
||||
|
||||
if (storageErr) throw storageErr
|
||||
previewUrl.value = data?.signedUrl || ''
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao acessar o documento.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function downloadFile() {
|
||||
if (!previewUrl.value) return
|
||||
const a = document.createElement('a')
|
||||
a.href = previewUrl.value
|
||||
a.download = doc.value?.nome_original || 'documento'
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
const isPdf = () => doc.value?.mime_type === 'application/pdf'
|
||||
const isImage = () => String(doc.value?.mime_type || '').startsWith('image/')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-3xl">
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center py-20">
|
||||
<i class="pi pi-spinner pi-spin text-3xl text-gray-400 mb-3" />
|
||||
<p class="text-sm text-gray-500">Validando link...</p>
|
||||
</div>
|
||||
|
||||
<!-- Erro -->
|
||||
<div v-else-if="error" class="flex flex-col items-center justify-center py-20 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-red-50 flex items-center justify-center mb-4">
|
||||
<i class="pi pi-times-circle text-3xl text-red-400" />
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-700 mb-2">Documento indisponível</h2>
|
||||
<p class="text-sm text-gray-500 max-w-md">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Documento -->
|
||||
<div v-else class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="flex-shrink-0 w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i :class="isPdf() ? 'pi pi-file-pdf text-red-500' : isImage() ? 'pi pi-image text-blue-500' : 'pi pi-file text-gray-500'" />
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium text-gray-800 truncate">{{ doc?.nome_original }}</div>
|
||||
<div class="text-xs text-gray-400">Documento compartilhado via AgênciaPSI</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700 transition-colors"
|
||||
@click="downloadFile"
|
||||
>
|
||||
<i class="pi pi-download text-xs" />
|
||||
Baixar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="bg-gray-100">
|
||||
<iframe
|
||||
v-if="isPdf()"
|
||||
:src="previewUrl"
|
||||
class="w-full border-0"
|
||||
style="height: 80vh;"
|
||||
/>
|
||||
<div v-else-if="isImage()" class="flex items-center justify-center p-6">
|
||||
<img :src="previewUrl" :alt="doc?.nome_original" class="max-w-full max-h-[80vh] object-contain rounded" />
|
||||
</div>
|
||||
<div v-else class="flex flex-col items-center justify-center py-20 text-gray-400">
|
||||
<i class="pi pi-file text-4xl mb-3" />
|
||||
<p class="text-sm">Pré-visualização não disponível para este tipo de arquivo.</p>
|
||||
<p class="text-xs mt-1">Clique em "Baixar" para acessar o documento.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-3 text-center text-xs text-gray-400 border-t border-gray-200">
|
||||
Compartilhado com segurança via AgênciaPSI
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user