Copyright, Financeiro, Lançamentos, aprimoramentos de ui

This commit is contained in:
Leonardo
2026-03-21 08:05:40 -03:00
parent 29ed349cf2
commit a89d1f5560
268 changed files with 58870 additions and 1752 deletions
@@ -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>