Copyright, Financeiro, Lançamentos, aprimoramentos de ui
This commit is contained in:
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/patients/cadastro/PatientsCadastroPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -7,6 +23,12 @@ import { useConfirm } from 'primevue/useconfirm'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
|
||||
const props = defineProps({
|
||||
dialogMode: { type: Boolean, default: false },
|
||||
patientId: { type: String, default: null }
|
||||
})
|
||||
const emit = defineEmits(['cancel', 'created'])
|
||||
|
||||
const { canSee } = useRoleGuard()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -91,8 +113,12 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
// ── Route helpers ─────────────────────────────────────────
|
||||
const patientId = computed(() => String(route.params?.id || '').trim() || null)
|
||||
const isEdit = computed(() => !!patientId.value)
|
||||
const patientId = computed(() =>
|
||||
props.dialogMode
|
||||
? (props.patientId || null)
|
||||
: (String(route.params?.id || '').trim() || null)
|
||||
)
|
||||
const isEdit = computed(() => !!patientId.value)
|
||||
|
||||
function getAreaKey () {
|
||||
const seg = String(route.path || '').split('/').filter(Boolean)[0] || 'admin'
|
||||
@@ -121,6 +147,7 @@ async function safePush (toNameObj, fallbackPath) {
|
||||
}
|
||||
|
||||
function goBack () {
|
||||
if (props.dialogMode) { emit('cancel'); return }
|
||||
const { listName, listPath } = getPatientsRoutes()
|
||||
if (window.history.length > 1) router.back()
|
||||
else safePush({ name: listName }, listPath)
|
||||
@@ -363,8 +390,7 @@ async function fetchAll () {
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
watch(() => route.params?.id, fetchAll, { immediate: true })
|
||||
onMounted(fetchAll)
|
||||
watch(patientId, fetchAll, { immediate: true })
|
||||
|
||||
// ── Tenant resolve ────────────────────────────────────────
|
||||
async function resolveTenantContextOrFail () {
|
||||
@@ -393,13 +419,16 @@ async function onSubmit () {
|
||||
await maybeUploadAvatar(ownerId, patientId.value)
|
||||
await replacePatientGroups(patientId.value, grupoIdSelecionado.value)
|
||||
await replacePatientTags(patientId.value, tagIdsSelecionadas.value)
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Paciente atualizado.', life: 2500 }); return
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Paciente atualizado.', life: 2500 })
|
||||
if (props.dialogMode) { emit('created', { id: patientId.value }); return }
|
||||
return
|
||||
}
|
||||
const created = await createPatient(payload)
|
||||
await maybeUploadAvatar(ownerId, created.id)
|
||||
await replacePatientGroups(created.id, grupoIdSelecionado.value)
|
||||
await replacePatientTags(created.id, tagIdsSelecionadas.value)
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Paciente cadastrado.', life: 2500 })
|
||||
if (props.dialogMode) { emit('created', created); return }
|
||||
form.value = resetForm(); grupoIdSelecionado.value = null; tagIdsSelecionadas.value = []
|
||||
avatarFile.value = null; revokePreview(); avatarPreviewUrl.value = ''
|
||||
await openPanel(0)
|
||||
@@ -422,7 +451,9 @@ async function doDelete () {
|
||||
const { error: e1 } = await supabase.from('patient_group_patient').delete().eq('patient_id', pid); if (e1) throw e1
|
||||
const { error: e2 } = await supabase.from('patient_patient_tag').delete().eq('patient_id', pid); if (e2) throw e2
|
||||
const { error: e3 } = await supabase.from('patients').delete().eq('id', pid); if (e3) throw e3
|
||||
toast.add({ severity: 'success', summary: 'Excluído', detail: 'Paciente excluído.', life: 2500 }); goBack()
|
||||
toast.add({ severity: 'success', summary: 'Excluído', detail: 'Paciente excluído.', life: 2500 })
|
||||
if (props.dialogMode) { emit('created', null); return }
|
||||
goBack()
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: err?.message || 'Falha ao excluir paciente', life: 4000 })
|
||||
} finally { deleting.value = false }
|
||||
@@ -509,19 +540,21 @@ async function createTagPersist () {
|
||||
createTagError.value = (e?.code === '23505' || /duplicate key value/i.test(msg)) ? 'Já existe uma tag com esse nome.' : (msg || 'Falha ao criar tag.')
|
||||
} finally { createTagSaving.value = false }
|
||||
}
|
||||
|
||||
defineExpose({ fillRandomPatient, onSubmit, confirmDelete, saving, deleting, canSee, isEdit })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
<ConfirmDialog v-if="!dialogMode" />
|
||||
|
||||
<!-- Sentinel -->
|
||||
<div ref="headerSentinelRef" class="h-px" />
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
HERO sticky
|
||||
HERO sticky (oculto no modo dialog)
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<section
|
||||
v-if="!dialogMode"
|
||||
class="sticky mx-3 md:mx-4 mb-3 z-20 overflow-hidden rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] px-3 py-2.5 transition-[border-radius] duration-200"
|
||||
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
|
||||
>
|
||||
@@ -552,8 +585,8 @@ async function createTagPersist () {
|
||||
<!-- Espaçador -->
|
||||
<div class="flex-1" />
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<!-- Ações (ocultas no modo dialog — o Dialog tem seu próprio footer) -->
|
||||
<div v-if="!dialogMode" class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<Button
|
||||
v-if="canSee('testMODE')"
|
||||
label="Preencher tudo"
|
||||
@@ -593,7 +626,10 @@ async function createTagPersist () {
|
||||
<div v-else class="grid grid-cols-1 gap-3 xl:grid-cols-[260px_1fr] max-w-[1100px] mx-auto">
|
||||
|
||||
<!-- ── SIDEBAR ──────────────────────────────────── -->
|
||||
<aside class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] p-3.5 xl:sticky xl:top-[calc(var(--layout-sticky-top,56px)+3.5rem)] xl:self-start">
|
||||
<aside
|
||||
class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] p-3.5 xl:sticky xl:self-start"
|
||||
:class="dialogMode ? 'xl:top-4' : 'xl:top-[calc(var(--layout-sticky-top,56px)+3.5rem)]'"
|
||||
>
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="flex items-center gap-3 pb-3.5 mb-3.5 border-b border-[var(--surface-border,#e2e8f0)] xl:flex-col xl:items-center xl:gap-2">
|
||||
@@ -868,8 +904,8 @@ async function createTagPersist () {
|
||||
|
||||
</Accordion>
|
||||
|
||||
<!-- Botão salvar bottom -->
|
||||
<div class="mt-4 flex justify-center">
|
||||
<!-- Botão salvar bottom (oculto no modo dialog — o footer cuida disso) -->
|
||||
<div v-if="!dialogMode" class="mt-4 flex justify-center">
|
||||
<Button label="Salvar" icon="pi pi-check" :loading="saving" class="min-w-[200px] rounded-full" @click="onSubmit" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/patients/cadastro/PatientsExternalLinkPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<!-- Sentinel -->
|
||||
<div ref="headerSentinelRef" class="h-px" />
|
||||
@@ -172,6 +187,8 @@
|
||||
<Button icon="pi pi-copy" label="Copiar mensagem" severity="secondary" outlined class="rounded-full" :disabled="!publicUrl" @click="copyInviteMessage" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoadedPhraseBlock v-if="inviteToken" />
|
||||
</div>
|
||||
|
||||
<!-- ── DIREITA: instruções ────────────────────────── -->
|
||||
@@ -223,6 +240,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/views/pages/patients/PatientIntakeRequestsPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
@@ -19,6 +34,7 @@ const tenantStore = useTenantStore()
|
||||
|
||||
const converting = ref(false)
|
||||
const loading = ref(false)
|
||||
const hasLoaded = ref(false)
|
||||
const rows = ref([])
|
||||
const q = ref('')
|
||||
|
||||
@@ -262,6 +278,7 @@ async function fetchIntakes () {
|
||||
toast.add({ severity: 'error', summary: 'Erro ao carregar', detail: e.message || String(e), life: 3500 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
hasLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +421,6 @@ onBeforeUnmount(() => { _observer?.disconnect() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Sentinel -->
|
||||
@@ -499,47 +515,52 @@ onBeforeUnmount(() => { _observer?.disconnect() })
|
||||
QUICK-STATS
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="flex flex-wrap gap-2 px-3 md:px-4 mb-3">
|
||||
<div
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] flex-1 min-w-[72px] cursor-pointer select-none transition-[border-color,box-shadow] duration-150 hover:border-indigo-400/40 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
||||
:class="{ 'border-[var(--primary-color,#6366f1)] shadow-[0_0_0_3px_rgba(99,102,241,0.15)]': statusFilter === '' }"
|
||||
@click="toggleStatusFilter('')"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none text-[var(--text-color)]">{{ totals.total }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">Total</div>
|
||||
</div>
|
||||
<template v-if="loading">
|
||||
<Skeleton v-for="n in 4" :key="n" height="3.5rem" class="flex-1 min-w-[72px] rounded-md" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] flex-1 min-w-[72px] cursor-pointer select-none transition-[border-color,box-shadow] duration-150 hover:border-indigo-400/40 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
||||
:class="{ 'border-[var(--primary-color,#6366f1)] shadow-[0_0_0_3px_rgba(99,102,241,0.15)]': statusFilter === '' }"
|
||||
@click="toggleStatusFilter('')"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none text-[var(--text-color)]">{{ totals.total }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">Total</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[72px] cursor-pointer select-none transition-[border-color,box-shadow,background] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
||||
:class="statusFilter === 'new'
|
||||
? 'border-sky-500 bg-sky-500/5 shadow-[0_0_0_3px_rgba(14,165,233,0.15)]'
|
||||
: 'border-sky-400/30 bg-sky-500/5 hover:border-sky-400/60'"
|
||||
@click="toggleStatusFilter('new')"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none text-sky-500">{{ totals.nNew }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">Novos</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[72px] cursor-pointer select-none transition-[border-color,box-shadow,background] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
||||
:class="statusFilter === 'new'
|
||||
? 'border-sky-500 bg-sky-500/5 shadow-[0_0_0_3px_rgba(14,165,233,0.15)]'
|
||||
: 'border-sky-400/30 bg-sky-500/5 hover:border-sky-400/60'"
|
||||
@click="toggleStatusFilter('new')"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none text-sky-500">{{ totals.nNew }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">Novos</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[72px] cursor-pointer select-none transition-[border-color,box-shadow,background] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
||||
:class="statusFilter === 'converted'
|
||||
? 'border-green-500 bg-green-500/5 shadow-[0_0_0_3px_rgba(34,197,94,0.15)]'
|
||||
: 'border-green-500/30 bg-green-500/5 hover:border-green-500/50'"
|
||||
@click="toggleStatusFilter('converted')"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none text-green-500">{{ totals.nConv }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">Convertidos</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[72px] cursor-pointer select-none transition-[border-color,box-shadow,background] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
||||
:class="statusFilter === 'converted'
|
||||
? 'border-green-500 bg-green-500/5 shadow-[0_0_0_3px_rgba(34,197,94,0.15)]'
|
||||
: 'border-green-500/30 bg-green-500/5 hover:border-green-500/50'"
|
||||
@click="toggleStatusFilter('converted')"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none text-green-500">{{ totals.nConv }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">Convertidos</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[72px] cursor-pointer select-none transition-[border-color,box-shadow,background] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
||||
:class="statusFilter === 'rejected'
|
||||
? 'border-red-500 bg-red-500/5 shadow-[0_0_0_3px_rgba(239,68,68,0.15)]'
|
||||
: 'border-red-500/30 bg-red-500/5 hover:border-red-500/50'"
|
||||
@click="toggleStatusFilter('rejected')"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none text-red-500">{{ totals.nRej }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">Rejeitados</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[72px] cursor-pointer select-none transition-[border-color,box-shadow,background] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
|
||||
:class="statusFilter === 'rejected'
|
||||
? 'border-red-500 bg-red-500/5 shadow-[0_0_0_3px_rgba(239,68,68,0.15)]'
|
||||
: 'border-red-500/30 bg-red-500/5 hover:border-red-500/50'"
|
||||
@click="toggleStatusFilter('rejected')"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none text-red-500">{{ totals.nRej }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">Rejeitados</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
@@ -635,8 +656,21 @@ onBeforeUnmount(() => { _observer?.disconnect() })
|
||||
CARDS — mobile (<md)
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="md:hidden mx-3 mb-5">
|
||||
<div v-if="loading" class="flex justify-center py-10">
|
||||
<ProgressSpinner />
|
||||
<div v-if="loading" class="flex flex-col gap-2.5">
|
||||
<div v-for="n in 5" :key="n" class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-3.5 flex flex-col gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<Skeleton shape="circle" size="2.5rem" />
|
||||
<div class="flex flex-col gap-1.5 flex-1">
|
||||
<Skeleton width="55%" height="13px" />
|
||||
<Skeleton width="40%" height="11px" />
|
||||
</div>
|
||||
<Skeleton width="60px" height="22px" border-radius="999px" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Skeleton width="30%" height="11px" />
|
||||
<Skeleton width="60px" height="28px" border-radius="999px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredRows.length === 0" class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] py-10 text-center">
|
||||
@@ -681,6 +715,10 @@ onBeforeUnmount(() => { _observer?.disconnect() })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 md:px-4 pb-3">
|
||||
<LoadedPhraseBlock v-if="hasLoaded" />
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
MODAL detalhe
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
|
||||
Reference in New Issue
Block a user