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,646 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI — CadastroRapidoMedico.vue
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Dialog de cadastro rápido de médicos / profissionais de referência.
|
||||
| Usado em PatientsCadastroPage (campo "Encaminhado por") e acessível
|
||||
| pela futura MedicosCadastroPage.
|
||||
|
|
||||
| Props:
|
||||
| visible (Boolean)
|
||||
|
|
||||
| Emits:
|
||||
| update:visible
|
||||
| created — objeto do médico recém-criado
|
||||
| selected — médico selecionado da lista (para preencher campo no form)
|
||||
|
|
||||
| Tabela: public.medicos (ver medicos.sql)
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { digitsOnly, fmtPhone } from '@/utils/validators'
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
editId: { type: String, default: null }, // uuid do médico a editar (null = novo)
|
||||
})
|
||||
const emit = defineEmits(['update:visible', 'created', 'selected'])
|
||||
|
||||
const toast = useToast()
|
||||
const tenantStore = useTenantStore()
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Auth / tenant
|
||||
// ─────────────────────────────────────────────────────────
|
||||
async function getOwnerId () {
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
const uid = data?.user?.id
|
||||
if (!uid) throw new Error('Sessão inválida.')
|
||||
return uid
|
||||
}
|
||||
async function getTenantId () {
|
||||
const tid = tenantStore.tenantId || tenantStore.currentTenantId || tenantStore.tenant?.id
|
||||
if (tid) return tid
|
||||
const ownerId = await getOwnerId()
|
||||
const { data, error } = await supabase
|
||||
.from('tenant_members').select('tenant_id')
|
||||
.eq('user_id', ownerId).eq('status', 'active')
|
||||
.order('created_at', { ascending: false }).limit(1).single()
|
||||
if (error) throw error
|
||||
return data?.tenant_id
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Views: 'list' | 'create' | 'edit'
|
||||
// ─────────────────────────────────────────────────────────
|
||||
const view = ref('list')
|
||||
const medicos = ref([])
|
||||
const loading = ref(false)
|
||||
const searchTerm = ref('')
|
||||
const editingId = ref(null) // uuid do médico sendo editado
|
||||
|
||||
// Form
|
||||
const saving = ref(false)
|
||||
const formErr = ref('')
|
||||
const showTelProfissional = ref(false)
|
||||
const showTelPessoal = ref(false)
|
||||
|
||||
function resetForm () {
|
||||
return {
|
||||
nome: '',
|
||||
crm: '',
|
||||
especialidade: '',
|
||||
especialidade_outra: '',
|
||||
telefone_profissional: '',
|
||||
telefone_pessoal: '',
|
||||
email: '',
|
||||
clinica: '',
|
||||
cidade: '',
|
||||
estado: 'SP',
|
||||
observacoes: '',
|
||||
}
|
||||
}
|
||||
const form = ref(resetForm())
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Especialidades
|
||||
// ─────────────────────────────────────────────────────────
|
||||
const especialidadesOpts = [
|
||||
{ label: 'Psiquiatria', value: 'Psiquiatria' },
|
||||
{ label: 'Neurologia', value: 'Neurologia' },
|
||||
{ label: 'Neuropsiquiatria infantil', value: 'Neuropsiquiatria infantil' },
|
||||
{ label: 'Clínica geral', value: 'Clínica geral' },
|
||||
{ label: 'Pediatria', value: 'Pediatria' },
|
||||
{ label: 'Geriatria', value: 'Geriatria' },
|
||||
{ label: 'Endocrinologia', value: 'Endocrinologia' },
|
||||
{ label: 'Psicologia (encaminhador)', value: 'Psicologia (encaminhador)' },
|
||||
{ label: 'Assistência social', value: 'Assistência social' },
|
||||
{ label: 'Fonoaudiologia', value: 'Fonoaudiologia' },
|
||||
{ label: 'Terapia ocupacional', value: 'Terapia ocupacional' },
|
||||
{ label: 'Fisioterapia', value: 'Fisioterapia' },
|
||||
{ label: 'Outra', value: '__outra__' },
|
||||
]
|
||||
|
||||
const especialidadeFinal = computed(() =>
|
||||
form.value.especialidade === '__outra__'
|
||||
? (form.value.especialidade_outra.trim() || null)
|
||||
: (form.value.especialidade || null)
|
||||
)
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Computed
|
||||
// ─────────────────────────────────────────────────────────
|
||||
const filteredMedicos = computed(() => {
|
||||
const q = searchTerm.value.toLowerCase().trim()
|
||||
if (!q) return medicos.value
|
||||
return medicos.value.filter(m =>
|
||||
(m.nome || '').toLowerCase().includes(q) ||
|
||||
(m.especialidade || '').toLowerCase().includes(q) ||
|
||||
(m.crm || '').toLowerCase().includes(q) ||
|
||||
(m.clinica || '').toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Load
|
||||
// ─────────────────────────────────────────────────────────
|
||||
async function loadMedicos () {
|
||||
loading.value = true
|
||||
try {
|
||||
const ownerId = await getOwnerId()
|
||||
const { data, error } = await supabase
|
||||
.from('medicos')
|
||||
.select('id, nome, crm, especialidade, telefone_profissional, clinica, cidade, estado')
|
||||
.eq('owner_id', ownerId)
|
||||
.eq('ativo', true)
|
||||
.order('nome', { ascending: true })
|
||||
if (error) throw error
|
||||
medicos.value = data || []
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao carregar médicos.', life: 3500 })
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
watch(() => props.visible, async (v) => {
|
||||
if (v) {
|
||||
searchTerm.value = ''
|
||||
formErr.value = ''
|
||||
showTelProfissional.value = false
|
||||
showTelPessoal.value = false
|
||||
if (props.editId) {
|
||||
// Abre direto no form de edição com os dados carregados
|
||||
await loadMedicoForEdit(props.editId)
|
||||
} else {
|
||||
view.value = 'list'
|
||||
loadMedicos()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function loadMedicoForEdit (id) {
|
||||
try {
|
||||
const ownerId = await getOwnerId()
|
||||
const { data, error } = await supabase
|
||||
.from('medicos').select('*').eq('id', id).eq('owner_id', ownerId).single()
|
||||
if (error) throw error
|
||||
form.value = {
|
||||
nome: data.nome || '',
|
||||
crm: data.crm || '',
|
||||
especialidade: data.especialidade || '',
|
||||
especialidade_outra: '',
|
||||
telefone_profissional: data.telefone_profissional ? fmtPhone(data.telefone_profissional) : '',
|
||||
telefone_pessoal: data.telefone_pessoal ? fmtPhone(data.telefone_pessoal) : '',
|
||||
email: data.email || '',
|
||||
clinica: data.clinica || '',
|
||||
cidade: data.cidade || '',
|
||||
estado: data.estado || 'SP',
|
||||
observacoes: data.observacoes || '',
|
||||
}
|
||||
editingId.value = id
|
||||
view.value = 'edit'
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao carregar médico.', life: 3000 })
|
||||
view.value = 'list'
|
||||
loadMedicos()
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Ações lista
|
||||
// ─────────────────────────────────────────────────────────
|
||||
function openCreate () {
|
||||
form.value = resetForm()
|
||||
formErr.value = ''
|
||||
editingId.value = null
|
||||
showTelProfissional.value = false
|
||||
showTelPessoal.value = false
|
||||
view.value = 'create'
|
||||
}
|
||||
function backToList () {
|
||||
view.value = 'list'
|
||||
formErr.value = ''
|
||||
editingId.value = null
|
||||
loadMedicos()
|
||||
}
|
||||
function selectMedico (m) {
|
||||
emit('selected', m)
|
||||
close()
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Salvar
|
||||
// ─────────────────────────────────────────────────────────
|
||||
async function saveMedico () {
|
||||
const nome = String(form.value.nome || '').trim()
|
||||
if (!nome) { formErr.value = 'Informe o nome do médico.'; return }
|
||||
if (form.value.especialidade === '__outra__' && !form.value.especialidade_outra.trim()) {
|
||||
formErr.value = 'Informe a especialidade.'; return
|
||||
}
|
||||
|
||||
saving.value = true; formErr.value = ''
|
||||
const isUpdate = !!editingId.value
|
||||
|
||||
try {
|
||||
const ownerId = await getOwnerId()
|
||||
const tenantId = await getTenantId()
|
||||
|
||||
const payload = {
|
||||
owner_id: ownerId,
|
||||
tenant_id: tenantId,
|
||||
nome,
|
||||
crm: String(form.value.crm || '').trim() || null,
|
||||
especialidade: especialidadeFinal.value,
|
||||
telefone_profissional: form.value.telefone_profissional ? digitsOnly(form.value.telefone_profissional) : null,
|
||||
telefone_pessoal: form.value.telefone_pessoal ? digitsOnly(form.value.telefone_pessoal) : null,
|
||||
email: String(form.value.email || '').trim() || null,
|
||||
clinica: String(form.value.clinica || '').trim() || null,
|
||||
cidade: String(form.value.cidade || '').trim() || null,
|
||||
estado: String(form.value.estado || '').trim() || null,
|
||||
observacoes: String(form.value.observacoes || '').trim() || null,
|
||||
ativo: true,
|
||||
}
|
||||
|
||||
let data
|
||||
if (isUpdate) {
|
||||
const { data: d, error } = await supabase
|
||||
.from('medicos').update({ ...payload, updated_at: new Date().toISOString() })
|
||||
.eq('id', editingId.value).eq('owner_id', ownerId)
|
||||
.select('id, nome, crm, especialidade, telefone_profissional, clinica, cidade, estado')
|
||||
.single()
|
||||
if (error) throw error
|
||||
data = d
|
||||
toast.add({ severity: 'success', summary: 'Médico atualizado', detail: `Dr(a). ${data.nome} atualizado.`, life: 2500 })
|
||||
} else {
|
||||
const { data: d, error } = await supabase
|
||||
.from('medicos').insert(payload)
|
||||
.select('id, nome, crm, especialidade, telefone_profissional, clinica, cidade, estado')
|
||||
.single()
|
||||
if (error) throw error
|
||||
data = d
|
||||
toast.add({ severity: 'success', summary: 'Médico cadastrado', detail: `Dr(a). ${data.nome} adicionado.`, life: 2500 })
|
||||
}
|
||||
|
||||
emit(isUpdate ? 'selected' : 'created', data)
|
||||
emit('selected', data)
|
||||
close()
|
||||
} catch (e) {
|
||||
const msg = e?.message || ''
|
||||
if (e?.code === '23505' || /duplicate/i.test(msg)) {
|
||||
formErr.value = 'Já existe um cadastro com este CRM para este profissional.'
|
||||
} else {
|
||||
formErr.value = msg || 'Falha ao salvar.'
|
||||
}
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
|
||||
function close () {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:visible="visible"
|
||||
@update:visible="$emit('update:visible', $event)"
|
||||
modal
|
||||
:draggable="false"
|
||||
:closable="!saving"
|
||||
:dismissableMask="!saving"
|
||||
maximizable
|
||||
class="dc-dialog w-[50rem]"
|
||||
:breakpoints="{ '1199px': '90vw', '768px': '94vw' }"
|
||||
:pt="{
|
||||
header: { class: '!p-3 !rounded-t-[12px] border-b border-[var(--surface-border)] shadow-[0_1px_0_0_rgba(255,255,255,0.06)] bg-gray-100' },
|
||||
content: { class: '!p-3' },
|
||||
footer: { class: '!p-0 !rounded-b-[12px] border-t border-[var(--surface-border)] shadow-[0_1px_0_0_rgba(255,255,255,0.06)] bg-gray-100' },
|
||||
pcCloseButton: { root: { class: '!rounded-md hover:!text-red-500' } },
|
||||
pcMaximizeButton: { root: { class: '!rounded-md hover:!text-primary' } },
|
||||
}"
|
||||
pt:mask:class="backdrop-blur-xs"
|
||||
>
|
||||
<!-- ── Header ──────────────────────────────────────── -->
|
||||
<template #header>
|
||||
<div class="flex w-full items-center justify-between gap-3 px-1">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="flex items-center justify-center w-7 h-7 rounded-lg bg-teal-100 text-teal-600 text-[0.8rem] shrink-0">
|
||||
<i class="pi pi-user-plus"/>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-base font-semibold truncate">Médicos & referências</div>
|
||||
<div class="text-xs opacity-50">
|
||||
<template v-if="view === 'list'">Selecione ou cadastre um novo profissional</template>
|
||||
<template v-else-if="editingId">Editar dados do médico</template>
|
||||
<template v-else>Novo médico / profissional de referência</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
VIEW: LISTA
|
||||
════════════════════════════════════════════════════ -->
|
||||
<div v-if="view === 'list'" class="flex flex-col -mt-1">
|
||||
|
||||
<!-- Busca -->
|
||||
<div class="mb-3">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search"/>
|
||||
<InputText v-model="searchTerm" class="w-full" variant="filled" placeholder="Buscar por nome, especialidade, CRM…"/>
|
||||
</IconField>
|
||||
</div>
|
||||
|
||||
<!-- Lista -->
|
||||
<div class="flex flex-col gap-1 max-h-[300px] overflow-y-auto pr-0.5">
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-8 gap-2 text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-spin pi-spinner"/> Carregando…
|
||||
</div>
|
||||
|
||||
<div v-else-if="!filteredMedicos.length" class="flex flex-col items-center py-8 gap-2 text-center">
|
||||
<div class="w-12 h-12 rounded-full bg-teal-50 flex items-center justify-center">
|
||||
<i class="pi pi-user-plus text-xl text-teal-300"/>
|
||||
</div>
|
||||
<div class="text-[0.82rem] text-[var(--text-color-secondary)]">
|
||||
{{ searchTerm ? 'Nenhum médico encontrado.' : 'Nenhum médico cadastrado ainda.' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-for="m in filteredMedicos" :key="m.id"
|
||||
type="button"
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-left border border-transparent hover:bg-[var(--surface-ground)] hover:border-teal-100 transition-all duration-100 w-full group"
|
||||
@click="selectMedico(m)"
|
||||
>
|
||||
<!-- Iniciais -->
|
||||
<div class="w-9 h-9 rounded-full bg-teal-100 flex items-center justify-center font-black text-[0.75rem] text-teal-700 shrink-0 group-hover:bg-teal-200 transition-colors select-none">
|
||||
{{ (m.nome||'?').split(' ').filter(Boolean).map(w=>w[0].toUpperCase()).slice(0,2).join('') }}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-[0.88rem] font-semibold text-[var(--text-color)] truncate leading-tight">
|
||||
Dr(a). {{ m.nome }}
|
||||
</div>
|
||||
<div class="text-[0.7rem] text-[var(--text-color-secondary)] truncate mt-0.5">
|
||||
<template v-if="m.especialidade">{{ m.especialidade }}</template>
|
||||
<template v-if="m.crm"> · CRM {{ m.crm }}</template>
|
||||
<template v-if="m.clinica"> · {{ m.clinica }}</template>
|
||||
<template v-if="m.cidade"> · {{ m.cidade }}<template v-if="m.estado">/{{ m.estado }}</template></template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<i class="pi pi-chevron-right text-[0.68rem] text-[var(--text-color-secondary)] opacity-30 group-hover:opacity-70 shrink-0"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-[var(--surface-border)] mt-3 pt-3">
|
||||
<Button
|
||||
label="Cadastrar novo médico"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full w-full"
|
||||
@click="openCreate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════
|
||||
VIEW: CRIAR
|
||||
════════════════════════════════════════════════════ -->
|
||||
<div v-else class="flex flex-col gap-3.5 -mt-1">
|
||||
|
||||
<!-- Voltar -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 text-[0.77rem] text-[var(--text-color-secondary)] hover:text-teal-600 transition-colors w-fit"
|
||||
@click="backToList"
|
||||
>
|
||||
<i class="pi pi-arrow-left text-[0.72rem]"/> Voltar para a lista
|
||||
</button>
|
||||
|
||||
<!-- Nome + CRM -->
|
||||
<div class="grid grid-cols-1 gap-3.5 sm:grid-cols-[1fr_150px]">
|
||||
<div>
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-user"/>
|
||||
<InputText id="m_nome" v-model="form.nome" class="w-full" variant="filled" autofocus/>
|
||||
</IconField>
|
||||
<label for="m_nome">Nome completo *</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div>
|
||||
<FloatLabel variant="on">
|
||||
<InputText id="m_crm" v-model="form.crm" class="w-full" variant="filled"/>
|
||||
<label for="m_crm">CRM (ex: 123456/SP)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Especialidade -->
|
||||
<div>
|
||||
<FloatLabel variant="on">
|
||||
<Select
|
||||
id="m_esp"
|
||||
v-model="form.especialidade"
|
||||
:options="especialidadesOpts"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
filter
|
||||
filterPlaceholder="Buscar especialidade…"
|
||||
/>
|
||||
<label for="m_esp">Especialidade</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Especialidade "Outra" — aparece condicionalmente -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-150 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-1"
|
||||
leave-active-class="transition-all duration-100 ease-in"
|
||||
leave-to-class="opacity-0 -translate-y-1"
|
||||
>
|
||||
<div v-if="form.especialidade === '__outra__'">
|
||||
<FloatLabel variant="on">
|
||||
<InputText
|
||||
id="m_esp_outra"
|
||||
v-model="form.especialidade_outra"
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
placeholder="Descreva a especialidade"
|
||||
/>
|
||||
<label for="m_esp_outra">Qual especialidade? *</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Divider contatos -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[0.63rem] font-bold uppercase tracking-widest text-teal-500">Contatos</span>
|
||||
<div class="flex-1 h-px bg-teal-200/50"/>
|
||||
</div>
|
||||
|
||||
<!-- Telefone profissional — máscara normal, olho aparece só quando preenchido -->
|
||||
<div>
|
||||
<div class="relative">
|
||||
<InputMask
|
||||
id="m_tel_prof"
|
||||
v-model="form.telefone_profissional"
|
||||
mask="(99) 99999-9999"
|
||||
:unmask="false"
|
||||
class="w-full"
|
||||
:class="form.telefone_profissional ? 'pr-10' : ''"
|
||||
variant="filled"
|
||||
placeholder="(00) 00000-0000"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<!-- Olho — só renderiza quando há dígitos preenchidos -->
|
||||
<button
|
||||
v-if="form.telefone_profissional?.replace(/\D/g,'').length >= 10"
|
||||
type="button"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 transition-colors z-10"
|
||||
:class="showTelProfissional ? 'text-teal-600' : 'text-[var(--text-color-secondary)] hover:text-teal-600'"
|
||||
tabindex="-1"
|
||||
:title="showTelProfissional ? 'Ocultar número' : 'Revelar número completo'"
|
||||
@click="showTelProfissional = !showTelProfissional"
|
||||
>
|
||||
<i :class="showTelProfissional ? 'pi pi-eye-slash' : 'pi pi-eye'" class="text-[0.9rem]"/>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Número revelado abaixo do campo -->
|
||||
<div
|
||||
v-if="showTelProfissional && form.telefone_profissional"
|
||||
class="mt-1.5 flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-teal-50 border border-teal-200/60"
|
||||
>
|
||||
<i class="pi pi-phone text-teal-500 text-[0.75rem] shrink-0"/>
|
||||
<span class="text-[0.82rem] font-semibold text-teal-700 tracking-wide">{{ form.telefone_profissional }}</span>
|
||||
</div>
|
||||
<div v-else class="mt-1 text-[0.63rem] text-[var(--text-color-secondary)] opacity-60">
|
||||
Número do consultório ou clínica.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telefone pessoal — mesma lógica -->
|
||||
<div>
|
||||
<div class="relative">
|
||||
<InputMask
|
||||
id="m_tel_pes"
|
||||
v-model="form.telefone_pessoal"
|
||||
mask="(99) 99999-9999"
|
||||
:unmask="false"
|
||||
class="w-full"
|
||||
:class="form.telefone_pessoal ? 'pr-10' : ''"
|
||||
variant="filled"
|
||||
placeholder="(00) 00000-0000"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
v-if="form.telefone_pessoal?.replace(/\D/g,'').length >= 10"
|
||||
type="button"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 transition-colors z-10"
|
||||
:class="showTelPessoal ? 'text-teal-600' : 'text-[var(--text-color-secondary)] hover:text-teal-600'"
|
||||
tabindex="-1"
|
||||
:title="showTelPessoal ? 'Ocultar número' : 'Revelar número completo'"
|
||||
@click="showTelPessoal = !showTelPessoal"
|
||||
>
|
||||
<i :class="showTelPessoal ? 'pi pi-eye-slash' : 'pi pi-eye'" class="text-[0.9rem]"/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="showTelPessoal && form.telefone_pessoal"
|
||||
class="mt-1.5 flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-teal-50 border border-teal-200/60"
|
||||
>
|
||||
<i class="pi pi-mobile text-teal-500 text-[0.75rem] shrink-0"/>
|
||||
<span class="text-[0.82rem] font-semibold text-teal-700 tracking-wide">{{ form.telefone_pessoal }}</span>
|
||||
</div>
|
||||
<div v-else class="mt-1 text-[0.63rem] text-[var(--text-color-secondary)] opacity-60">
|
||||
Pessoal / WhatsApp — toque no olho para revelar após digitar.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-envelope"/>
|
||||
<InputText id="m_email" v-model="form.email" class="w-full" variant="filled"/>
|
||||
</IconField>
|
||||
<label for="m_email">E-mail profissional</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Divider localização -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[0.63rem] font-bold uppercase tracking-widest text-teal-500">Localização</span>
|
||||
<div class="flex-1 h-px bg-teal-200/50"/>
|
||||
</div>
|
||||
|
||||
<!-- Clínica + Cidade + UF -->
|
||||
<div>
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-building"/>
|
||||
<InputText id="m_clinica" v-model="form.clinica" class="w-full" variant="filled"/>
|
||||
</IconField>
|
||||
<label for="m_clinica">Clínica / Hospital</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-[1fr_90px] gap-3">
|
||||
<div>
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-map-marker"/>
|
||||
<InputText id="m_cidade" v-model="form.cidade" class="w-full" variant="filled"/>
|
||||
</IconField>
|
||||
<label for="m_cidade">Cidade</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div>
|
||||
<FloatLabel variant="on">
|
||||
<InputText id="m_uf" v-model="form.estado" class="w-full" variant="filled"/>
|
||||
<label for="m_uf">UF</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Observações -->
|
||||
<div>
|
||||
<FloatLabel variant="on">
|
||||
<Textarea id="m_obs" v-model="form.observacoes" rows="2" class="w-full" variant="filled"/>
|
||||
<label for="m_obs">Observações internas</label>
|
||||
</FloatLabel>
|
||||
<div class="mt-1 text-[0.63rem] text-[var(--text-color-secondary)] opacity-60">
|
||||
Ex: aceita WhatsApp, convênios atendidos, melhor horário.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erro -->
|
||||
<div v-if="formErr" class="flex items-start gap-1.5 text-[0.82rem] text-red-500 font-medium">
|
||||
<i class="pi pi-exclamation-circle mt-0.5 shrink-0"/> {{ formErr }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Footer ──────────────────────────────────────── -->
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end gap-2 px-3 py-3">
|
||||
<Button
|
||||
v-if="view !== 'list'"
|
||||
label="Cancelar"
|
||||
severity="secondary"
|
||||
text
|
||||
class="rounded-full hover:!text-red-500"
|
||||
:disabled="saving"
|
||||
@click="backToList"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
label="Fechar"
|
||||
severity="secondary"
|
||||
text
|
||||
class="rounded-full hover:!text-red-500"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
v-if="view !== 'list'"
|
||||
:label="editingId ? 'Salvar alterações' : 'Salvar médico'"
|
||||
icon="pi pi-check"
|
||||
class="rounded-full"
|
||||
:loading="saving"
|
||||
@click="saveMedico"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user