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
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/Crud.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ProductService } from '@/services/ProductService';
import { FilterMatchMode } from '@primevue/core/api';
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/Empty.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div className="card">
<div class="font-semibold text-xl mb-4">Empty Page</div>
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/Landing.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup lang="ts">
import FeaturesWidget from '@/components/landing/FeaturesWidget.vue';
import FooterWidget from '@/components/landing/FooterWidget.vue';
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/NotFound.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
+23 -1
View File
@@ -1,5 +1,20 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/account/ProfilePage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<Toast />
<!-- HERO -->
<div ref="heroSentinelRef" class="h-px" />
@@ -774,6 +789,10 @@
</div>
</div>
<div class="px-3 md:px-4 pb-3">
<LoadedPhraseBlock v-if="mounted" />
</div>
<!-- Dialog: Trocar senha -->
<Dialog
v-model:visible="openPassword"
@@ -832,6 +851,8 @@ const router = useRouter()
const toast = useToast()
const confirm = useConfirm()
const mounted = ref(false)
/** trava para não marcar dirty durante o load */
const silentApplying = ref(true)
@@ -1457,6 +1478,7 @@ function confirmSignOut () {
onMounted(async () => {
try {
await loadProfile()
mounted.value = true
disconnectObserver = observeSections()
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não consegui carregar o perfil.', life: 6000 })
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/auth/Access.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
<div class="flex flex-col items-center justify-center">
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/auth/Error.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
<div class="flex flex-col items-center justify-center">
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/auth/Login.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { useTenantStore } from '@/stores/tenantStore'
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/auth/ResetPasswordPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
+22 -1
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/auth/SecurityPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
@@ -20,6 +36,7 @@ const newPassword = ref('')
const confirmPassword = ref('')
const loading = ref(false)
const loadingReset = ref(false)
const mounted = ref(false)
const done = ref(false)
// ── validações ────────────────────────────────────────────────────────────
@@ -119,6 +136,7 @@ onMounted(() => {
{ threshold: 0, rootMargin }
)
if (headerSentinelRef.value) _observer.observe(headerSentinelRef.value)
mounted.value = true
})
onBeforeUnmount(() => { _observer?.disconnect() })
@@ -146,7 +164,6 @@ async function sendResetEmail () {
</script>
<template>
<Toast />
<!-- Sentinel -->
<div ref="headerSentinelRef" class="h-px" />
@@ -351,6 +368,10 @@ async function sendResetEmail () {
</div>
</div>
<div class="px-3 md:px-4 pb-3">
<LoadedPhraseBlock v-if="mounted" />
</div>
</template>
<style scoped>
+16 -1
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/auth/WelcomePage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/auth/Welcome.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
+17 -7
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/billing/ClinicMeuPlanoPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/billing/ClinicMeuPlanoPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -422,7 +437,6 @@ onMounted(fetchMeuPlanoClinic)
</script>
<template>
<Toast />
<!-- Sentinel -->
<div class="h-px" />
@@ -686,8 +700,4 @@ onMounted(fetchMeuPlanoClinic)
</div>
</div>
</div>
</template>
<style scoped>
/* (intencionalmente vazio) */
</style>
</template>
@@ -1,4 +1,19 @@
<!-- src/views/pages/billing/TherapistMeuPlanoPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/billing/TherapistMeuPlanoPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router'
@@ -8,7 +23,8 @@ import { supabase } from '@/lib/supabase/client'
const router = useRouter()
const toast = useToast()
const loading = ref(false)
const loading = ref(false)
const hasLoaded = ref(false)
const subscription = ref(null)
const plan = ref(null)
const price = ref(null)
@@ -282,6 +298,7 @@ async function fetchMeuPlanoTherapist() {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || String(e), life: 5000 })
} finally {
loading.value = false
hasLoaded.value = true
}
}
@@ -299,7 +316,6 @@ onBeforeUnmount(() => { _observer?.disconnect() })
</script>
<template>
<Toast />
<!-- Sentinel -->
<div ref="headerSentinelRef" class="h-px" />
@@ -553,10 +569,9 @@ onBeforeUnmount(() => { _observer?.disconnect() })
</div>
</div>
<LoadedPhraseBlock v-if="hasLoaded" />
</template>
</div>
</template>
<style scoped>
/* (intencionalmente vazio) */
</style>
</template>
@@ -1,4 +1,19 @@
<!-- src/views/pages/billing/TherapistUpgradePage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/billing/TherapistUpgradePage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -254,7 +269,6 @@ onMounted(loadData)
</script>
<template>
<Toast />
<!-- Sentinel -->
<div class="h-px" />
@@ -467,8 +481,4 @@ onMounted(loadData)
</div>
</div>
</template>
<style scoped>
/* (intencionalmente vazio) */
</style>
</template>
+16 -2
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/billing/UpgradePage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/billing/UpgradePage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -309,7 +324,6 @@ watch(() => tenantStore.user?.id, () => { fetchAll() })
</script>
<template>
<Toast />
<!-- Sentinel -->
<div class="h-px" />
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/clinic/ClinicDashboard.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import BestSellingWidget from '@/components/dashboard/BestSellingWidget.vue';
import NotificationsWidget from '@/components/dashboard/NotificationsWidget.vue';
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/clinic/OnlineSchedulingAdminPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="p-4">
<h1>Online Scheduling (Manage)</h1>
@@ -1,4 +1,19 @@
<!-- src/views/pages/admin/ClinicTypesPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/clinic/clinic/ClinicFeaturesPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
@@ -324,7 +339,6 @@ watch(
</script>
<template>
<Toast />
<!-- Sentinel -->
<div class="h-px" />
@@ -1,4 +1,19 @@
<!-- src/views/pages/admin/ProfissionaisPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/clinic/clinic/ClinicProfessionalsPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
@@ -743,7 +758,6 @@ onMounted(async () => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/editor/EditorDashboard.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import NotificationsWidget from '@/components/dashboard/NotificationsWidget.vue';
</script>
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/misc/AccessDeniedPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { useRouter } from 'vue-router'
import { useTenantStore } from '@/stores/tenantStore'
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/portal/MinhasSessoes.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="p-4 md:p-6">
<!-- HEADER CONCEITUAL -->
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/portal/PortalDashboard.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import BestSellingWidget from '@/components/dashboard/BestSellingWidget.vue';
import NotificationsWidget from '@/components/dashboard/NotificationsWidget.vue';
+17 -2
View File
@@ -1,7 +1,22 @@
<!-- src/views/pages/public/AcceptInvitePage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/public/AcceptInvitePage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<Toast />
<div class="min-h-screen flex items-center justify-center p-6 bg-[var(--surface-ground)] text-[var(--text-color)]">
<Toast />
<div class="w-full max-w-lg overflow-hidden rounded-[2rem] border border-[var(--surface-border)] bg-[var(--surface-card)] shadow-sm">
<!-- Header / Hero -->
@@ -1,4 +1,19 @@
<!-- src/views/pages/public/AgendadorPublicoPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/public/AgendadorPublicoPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, watch, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
@@ -1,6 +1,22 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/public/CadastroPacienteExterno.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<Toast />
<div class="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-slate-950 text-slate-100">
<Toast />
<!-- "Backdrop" conceitual -->
<div class="pointer-events-none fixed inset-0 opacity-30">
+16 -1
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/public/LandingPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/public/Landingpage-v1.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="min-h-screen bg-[var(--surface-ground)] text-[var(--text-color)]">
<!-- TOPBAR -->
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/public/PatientsExternalLinkPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="p-4">
<!-- HEADER -->
+16 -1
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/auth/SignupPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/public/Signup.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
+16 -1
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasDashboard.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'
@@ -404,7 +420,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
+16 -2
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/saas/SaasDocsPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasDocsPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<!-- SaaS admin: CRUD de documentação dinâmica exibida nas páginas do sistema -->
<script setup>
import { ref, computed, onMounted } from 'vue'
@@ -519,7 +534,6 @@ function mediasCount (doc) {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Input oculto para importação de JSON -->
@@ -1,4 +1,19 @@
<!-- src/views/pages/saas/SaasEmailTemplatesPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasEmailTemplatesPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useToast } from 'primevue/usetoast'
+16 -2
View File
@@ -1,5 +1,19 @@
<!-- src/views/pages/saas/SaasFaqPage.vue -->
<!-- Portal de FAQ consulta de perguntas frequentes por usuários logados -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasFaqPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
+16 -2
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/billing/SaasFeaturesPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasFeaturesPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -230,7 +245,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
+16 -3
View File
@@ -1,5 +1,19 @@
<!-- src/views/pages/saas/SaasFeriadosPage.vue -->
<!-- SAAS admin: visualização centralizada de feriados municipais cadastrados pelos tenants -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasFeriadosPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -398,7 +412,6 @@ async function excluir (id) {
</script>
<template>
<Toast />
<!-- Sentinel -->
<div class="h-px" />
+16 -2
View File
@@ -1,5 +1,19 @@
<!-- src/views/pages/saas/SaasGlobalNoticesPage.vue -->
<!-- Painel de gerenciamento de Avisos Globais (SaaS Admin) -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasGlobalNoticesPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useToast } from 'primevue/usetoast'
+16 -1
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasLoginCarousel.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -176,7 +192,6 @@ onMounted(load)
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
+16
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasPlaceholder.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<!-- Sentinel -->
<div class="h-px" />
@@ -1,4 +1,19 @@
<!-- src/views/pages/billing/PlanFeaturesMatrixPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasPlanFeaturesMatrixPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -395,7 +410,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
+16 -2
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/billing/SaasPlanLimitsPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasPlanLimitsPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -326,7 +341,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
+16 -1
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasPlansPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -429,7 +445,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
+16 -2
View File
@@ -1,4 +1,19 @@
<!-- src/views/pages/billing/PlansPublicShowcasePage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasPlansPublicPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -446,7 +461,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasSubscriptionEventsPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -314,7 +330,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<!-- Sentinel -->
<div ref="sentinelRef" class="h-px" />
@@ -1,4 +1,19 @@
<!-- src/views/pages/saas/SubscriptionHealthPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasSubscriptionHealthPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
@@ -398,7 +413,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
+16 -1
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasSubscriptionsPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter, useRoute } from 'vue-router'
@@ -414,7 +430,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
+16 -1
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SaasSupportPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useToast } from 'primevue/usetoast'
@@ -240,7 +256,6 @@ function sessionStatusLabel (session) {
</script>
<template>
<Toast />
<!-- Sentinel -->
<div class="h-px" />
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/saas/SubscriptionIntentsPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { useToast } from 'primevue/usetoast'
@@ -578,7 +594,6 @@ onBeforeUnmount(() => {
</script>
<template>
<Toast />
<ConfirmDialog />
<!-- Sentinel -->
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/supervisor/SupervisaoSalaPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
// SupervisaoSalaPage.vue Fase 1 placeholder
// Fase 2: listar supervisionados, convidar, gerenciar sessões
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/supervisor/SupervisorDashboard.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import StatsWidget from '@/components/dashboard/StatsWidget.vue';
import NotificationsWidget from '@/components/dashboard/NotificationsWidget.vue';
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/therapist/OnlineSchedulingPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="p-4">
<h1>Online Scheduling Terapeuta (Therapist) (Manage)</h1>
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/therapist/RelatoriosPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -38,6 +54,7 @@ function periodRange (period) {
// Dados
const loading = ref(false)
const hasLoaded = ref(false)
const sessions = ref([])
const loadError = ref('')
@@ -67,6 +84,7 @@ async function loadSessions () {
loadError.value = e?.message || 'Falha ao carregar relatório.'
} finally {
loading.value = false
hasLoaded.value = true
}
}
@@ -442,6 +460,8 @@ onMounted(loadSessions)
</DataTable>
</div>
<LoadedPhraseBlock v-if="hasLoaded" />
</template>
</div>
</template>
+392 -144
View File
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/therapist/TherapistDashboard.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="flex min-h-[calc(100vh-4.5rem)] bg-[var(--surface-ground,#f5f7fa)]">
@@ -21,34 +37,47 @@
<i class="pi pi-calendar" />
<span>Março 2026</span>
</div>
<div class="grid grid-cols-7 mb-0.5">
<span v-for="d in diasSemana" :key="d" class="text-center text-xs font-bold text-[var(--text-color-secondary,#94a3b8)] py-0.5">{{ d }}</span>
</div>
<div class="grid grid-cols-7 gap-px">
<button
v-for="cell in calCells"
:key="cell.key"
class="relative aspect-square flex items-center justify-center text-xs font-medium rounded border-none cursor-pointer transition-colors duration-100"
:class="{
'bg-[var(--primary-color,#6366f1)] text-white font-bold rounded-md': cell.isToday,
'text-[var(--text-color-secondary,#cbd5e1)] cursor-default': cell.isOther,
'bg-indigo-500/10 text-[var(--primary-color,#6366f1)] font-bold': cell.day === selectedDay && !cell.isToday,
'text-[var(--text-color,#1e293b)] hover:bg-[var(--surface-hover,#f1f5f9)]': !cell.isToday && !cell.isOther,
}"
@click="cell.day && onCalDayClick($event, cell.day)"
>
<span>{{ cell.day }}</span>
<span
v-if="cell.count"
class="absolute bottom-px right-0.5 w-1 h-1 rounded-full"
<!-- Skeleton -->
<template v-if="loading">
<div class="grid grid-cols-7 mb-0.5">
<Skeleton v-for="n in 7" :key="n" height="14px" class="mx-0.5" />
</div>
<div class="grid grid-cols-7 gap-px mt-1">
<Skeleton v-for="n in 35" :key="n" height="22px" class="rounded" />
</div>
</template>
<template v-else>
<div class="grid grid-cols-7 mb-0.5">
<span v-for="d in diasSemana" :key="d" class="text-center text-xs font-bold text-[var(--text-color-secondary,#94a3b8)] py-0.5">{{ d }}</span>
</div>
<div class="grid grid-cols-7 gap-px">
<button
v-for="cell in calCells"
:key="cell.key"
class="relative aspect-square flex items-center justify-center text-xs font-medium rounded border-none cursor-pointer transition-colors duration-100"
:class="{
'bg-red-500': cell.urgency === 'urg-alta',
'bg-amber-500': cell.urgency === 'urg-media',
'bg-green-500': cell.urgency === 'urg-baixa',
'bg-[var(--primary-color,#6366f1)] text-white font-bold rounded-md': cell.isToday,
'text-[var(--text-color-secondary,#cbd5e1)] cursor-default': cell.isOther,
'bg-indigo-500/10 text-[var(--primary-color,#6366f1)] font-bold': cell.day === selectedDay && !cell.isToday,
'text-[var(--text-color,#1e293b)] hover:bg-[var(--surface-hover,#f1f5f9)]': !cell.isToday && !cell.isOther,
}"
/>
</button>
</div>
@click="cell.day && onCalDayClick($event, cell.day)"
>
<span>{{ cell.day }}</span>
<span
v-if="cell.count"
class="absolute bottom-px right-0.5 w-1 h-1 rounded-full"
:class="{
'bg-red-500': cell.urgency === 'urg-alta',
'bg-amber-500': cell.urgency === 'urg-media',
'bg-green-500': cell.urgency === 'urg-baixa',
}"
/>
</button>
</div>
</template>
</div>
<!-- Eventos do dia -->
@@ -56,33 +85,52 @@
<div class="flex items-center gap-1.5 text-xs font-bold uppercase tracking-widest text-[var(--text-color-secondary,#64748b)] mb-2.5">
<i class="pi pi-clock" /><span>{{ labelDiaSelecionado }}</span>
</div>
<div class="flex flex-col gap-1.5 max-h-[220px] overflow-y-auto">
<!-- Skeleton -->
<template v-if="loading">
<div class="flex flex-col gap-2">
<div v-for="n in 3" :key="n" class="aside-ev aside-ev--skeleton">
<div class="flex flex-col gap-1 min-w-[36px] items-end">
<Skeleton width="32px" height="10px" />
<Skeleton width="24px" height="8px" />
</div>
<Skeleton shape="square" size="28px" border-radius="4px" />
<div class="flex flex-col gap-1.5 flex-1">
<Skeleton :width="n === 1 ? '75%' : n === 2 ? '60%' : '70%'" height="10px" />
<Skeleton width="40%" height="8px" />
</div>
</div>
</div>
</template>
<div v-else class="flex flex-col gap-2 max-h-[260px] overflow-y-auto pr-0.5">
<div
v-for="ev in eventosDoDia"
:key="ev.id"
class="flex items-center gap-1.5 px-1.5 py-1.5 rounded-md bg-[var(--surface-ground,#f8fafc)] border-l-[3px] cursor-pointer hover:bg-[var(--surface-hover,#f1f5f9)] transition-colors duration-100"
:style="ev.bgColor ? { borderLeftColor: ev.bgColor } : {}"
class="aside-ev"
:style="ev.bgColor ? { '--ev-color': ev.bgColor } : {}"
:class="{
'border-l-sky-400': !ev.bgColor && ev.tipo === 'reuniao',
'border-l-green-400': !ev.bgColor && ev.status === 'realizado',
'border-l-[var(--primary-color,#6366f1)]': !ev.bgColor && ev.tipo !== 'reuniao' && ev.status !== 'realizado',
'aside-ev--reuniao': !ev.bgColor && ev.tipo === 'reuniao',
'aside-ev--realizado': !ev.bgColor && ev.status === 'realizado',
'aside-ev--default': !ev.bgColor && ev.tipo !== 'reuniao' && ev.status !== 'realizado',
}"
@click="openEvMenu($event, ev)"
>
<div class="flex flex-col items-end min-w-[38px]">
<span class="text-[0.7rem] font-bold text-[var(--text-color)]">{{ ev.hora }}</span>
<span class="text-xs text-[var(--text-color-secondary)]">{{ ev.dur }}</span>
<div class="aside-ev__time">
<span class="aside-ev__hora">{{ ev.hora }}</span>
<span class="aside-ev__dur">{{ ev.dur }}</span>
</div>
<Avatar :label="initials(ev.nome)" shape="square" size="small" class="flex-shrink-0" />
<div class="flex-1 min-w-0">
<span class="block text-xs font-semibold truncate">{{ ev.nome }}</span>
<div class="flex gap-1 mt-0.5">
<span class="text-xs bg-[var(--surface-border,#e2e8f0)] text-[var(--text-color-secondary)] px-1 rounded font-semibold">{{ ev.modalidade }}</span>
<span v-if="ev.recorrente" class="text-xs text-[var(--primary-color,#6366f1)]" title="Recorrente"></span>
<span class="block text-[0.8125rem] font-semibold truncate text-[var(--text-color)]">{{ ev.nome }}</span>
<div class="flex gap-1.5 mt-0.5 items-center">
<span class="aside-ev__badge">{{ ev.modalidade }}</span>
<i v-if="ev.recorrente" class="pi pi-sync text-[0.6rem] text-[var(--primary-color,#6366f1)]" title="Recorrente" />
</div>
</div>
<i :class="ev.statusIcon" class="text-xs text-[var(--text-color-secondary)]" />
<i :class="ev.statusIcon" class="text-xs text-[var(--text-color-secondary)] flex-shrink-0" />
</div>
<div v-if="!eventosDoDia.length" class="flex items-center gap-1.5 py-2.5 text-[var(--text-color-secondary)] text-xs">
<div v-if="!eventosDoDia.length" class="flex items-center gap-2 py-3 text-[var(--text-color-secondary)] text-sm">
<i class="pi pi-sun" /><span>Sem compromissos</span>
</div>
</div>
@@ -94,14 +142,40 @@
<i class="pi pi-sync" /><span>Recorrências ativas</span>
<span class="ml-auto bg-[var(--primary-color,#6366f1)] text-white rounded-full px-1.5 text-xs font-bold">{{ recorrencias.length }}</span>
</div>
<div class="flex flex-col gap-1.5 max-h-[170px] overflow-y-auto">
<div v-for="r in recorrencias" :key="r.id" class="flex items-center gap-2 py-0.5 cursor-pointer hover:bg-[var(--surface-hover,#f1f5f9)] rounded-md px-1 transition-colors duration-100" @click="openRecMenu($event, r)">
<div class="w-[26px] h-[26px] rounded-full flex items-center justify-center text-[0.58rem] font-bold text-white flex-shrink-0" :style="{ background: r.color }">{{ r.initials }}</div>
<!-- Skeleton -->
<template v-if="loading">
<div class="flex flex-col gap-2">
<div v-for="n in 4" :key="n" class="aside-rec aside-rec--skeleton">
<Skeleton shape="square" size="30px" border-radius="4px" />
<div class="flex flex-col gap-1.5 flex-1">
<Skeleton :width="n % 2 === 0 ? '65%' : '50%'" height="10px" />
<Skeleton width="75%" height="8px" />
</div>
<Skeleton width="28px" height="10px" />
</div>
</div>
</template>
<div v-else class="flex flex-col gap-1.5 max-h-[210px] overflow-y-auto pr-0.5">
<div
v-for="r in recorrencias"
:key="r.id"
class="aside-rec"
@click="openRecMenu($event, r)"
>
<Avatar :label="r.initials" shape="square" size="normal" class="flex-shrink-0" />
<div class="flex-1 min-w-0">
<span class="block text-xs font-semibold">{{ r.nome }}</span>
<span class="block text-[0.8125rem] font-semibold truncate text-[var(--text-color)]">{{ r.nome }}</span>
<span class="block text-xs text-[var(--text-color-secondary)]">{{ r.freq }}</span>
</div>
<div class="text-xs font-bold whitespace-nowrap" :class="r.proxHoje ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color-secondary)]'">{{ r.proxLabel }}</div>
<div
class="aside-rec__prox"
:class="r.proxHoje ? 'text-[var(--primary-color,#6366f1)] font-bold' : 'text-[var(--text-color-secondary)]'"
>{{ r.proxLabel }}</div>
</div>
<div v-if="!recorrencias.length" class="flex items-center gap-2 py-3 text-[var(--text-color-secondary)] text-sm">
<i class="pi pi-info-circle" /><span>Nenhuma recorrência ativa</span>
</div>
</div>
</div>
@@ -115,7 +189,27 @@
<main class="flex-1 min-w-0 p-4 xl:p-[1.125rem_1.375rem] flex flex-col gap-4 overflow-y-auto">
<!-- Hero Header -->
<!-- Skeleton hero -->
<section v-if="loading" class="relative overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-2.5">
<div class="flex items-center gap-3 mb-3">
<Skeleton width="40px" height="40px" border-radius="8px" />
<div class="flex flex-col gap-1.5 flex-1">
<Skeleton width="12rem" height="14px" />
<Skeleton width="18rem" height="11px" />
</div>
<Skeleton width="36px" height="36px" border-radius="999px" />
<Skeleton width="36px" height="36px" border-radius="999px" />
</div>
<div class="flex flex-wrap gap-2.5">
<div v-for="n in 4" :key="n" class="flex flex-col gap-1.5 px-4 py-2.5 rounded-md border border-[var(--surface-border)] flex-1 min-w-[90px]">
<Skeleton width="2rem" height="20px" />
<Skeleton width="4rem" height="10px" />
</div>
</div>
</section>
<section
v-if="!loading"
class="relative overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-2.5"
:class="{ 'rounded-tl-none rounded-tr-none': heroStuck }"
>
@@ -131,7 +225,7 @@
<i class="pi pi-home text-lg" />
</div>
<div class="min-w-0">
<div class="text-[1.1rem] font-bold tracking-tight text-[var(--text-color)]">{{ saudacao }} <span class="text-[var(--primary-color,#6366f1)]">👋</span></div>
<div class="text-[1.1rem] font-bold tracking-tight text-[var(--text-color-secondary)]">{{ saudacao }} <span class="text-[var(--primary-color,#6366f1)]">👋</span></div>
<div class="text-[0.78rem] text-[var(--text-color-secondary)] mt-0.5">{{ resumoHoje }}</div>
</div>
</div>
@@ -185,20 +279,40 @@
</button>
<!-- Linha do tempo -->
<section class="bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)] px-[1.125rem] py-3.5">
<section v-if="loading" class="bg-[var(--surface-card)] rounded-md border border-[var(--surface-border)] p-2.5">
<div class="flex items-center justify-between mb-2.5">
<div class="flex items-center gap-1.5 text-xs font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)]">
<i class="pi pi-chart-bar" /> Linha do tempo Hoje
</div>
<div class="flex items-center gap-1.5 text-xs font-semibold text-[var(--text-color-secondary)]">
<span class="pulse-dot w-[7px] h-[7px] rounded-full bg-red-500 flex-shrink-0" />
Agora: {{ horaAtual }}
</div>
<Skeleton width="12rem" height="12px" />
<Skeleton width="6rem" height="12px" />
</div>
<Skeleton width="100%" height="40px" border-radius="6px" class="mt-2.5" />
</section>
<section v-if="!loading" class="bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)] p-2.5 shadow-[0_0_0_3px_color-mix(in_srgb,var(--primary-color)_7%,transparent)]">
<div class="flex items-center justify-between mb-2.5">
<div class="flex items-center gap-2.5">
<i class="pi pi-chart-bar w-10 h-10 rounded-md cfg-subheader__icon flex-shrink-0" />
<div class="flex flex-col leading-tight">
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">
Linha do tempo Hoje
</div>
<div class="text-xs font-semibold text-[var(--text-color-secondary)] flex items-center gap-1.5">
<span class="pulse-dot w-[15px] h-[5px] rounded-full bg-red-500"></span>
Agora: {{ horaAtual }}
</div>
</div>
</div>
<!-- Exemplo: badge ou ação -->
<span class="text-xs font-semibold text-[var(--text-color-secondary)]">
<Button icon="pi pi-cog" severity="secondary" outlined class="rounded-full" title="Ver sua Agenda" @click="$router.push('/therapist/agenda')" label="Agenda" />
</span>
</div>
<div class="mt-2.5 relative">
<div class="flex justify-between mb-1">
<div v-for="h in hoursRange" :key="h" class="flex-1 text-left">
<span class="text-[0.57rem] text-[var(--text-color-secondary)] font-semibold">{{ h }}h</span>
<span class="text-[0.80rem] text-[var(--text-color-secondary)] font-semibold">{{ h }}h</span>
</div>
</div>
<div class="relative h-10 bg-[var(--surface-ground,#f8fafc)] rounded-md overflow-visible">
@@ -228,131 +342,169 @@
<!-- Cards de notificação -->
<section class="grid grid-cols-1 lg:grid-cols-2 gap-3.5">
<!-- SKELETON dos cards -->
<template v-if="loading">
<div
v-for="n in 4" :key="n"
class="flex flex-col bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)]"
>
<!-- header -->
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2 border-b border-[var(--surface-border,#f1f5f9)] bg-[var(--surface-ground,#f8fafc)]">
<Skeleton width="32px" height="32px" border-radius="6px" />
<div class="flex flex-col gap-1.5 flex-1">
<Skeleton :width="n % 2 === 0 ? '9rem' : '7rem'" height="12px" />
<Skeleton :width="n % 3 === 0 ? '13rem' : '10rem'" height="10px" />
</div>
</div>
<!-- body -->
<div class="flex-1 flex flex-col gap-2 px-3.5 py-3 min-h-[72px]">
<div v-for="i in 2" :key="i" class="flex items-center gap-2">
<Skeleton shape="circle" size="26px" />
<div class="flex flex-col gap-1 flex-1">
<Skeleton :width="i === 1 ? '65%' : '50%'" height="10px" />
<Skeleton width="75%" height="9px" />
</div>
</div>
</div>
<!-- footer -->
<div class="px-3.5 py-2 border-t border-[var(--surface-border,#f1f5f9)]">
<Skeleton width="5rem" height="10px" />
</div>
</div>
<!-- frase durante carregamento -->
<div class="lg:col-span-2">
<AppLoadingPhrases action="Carregando seu dashboard..." containerClass="py-8" />
</div>
</template>
<!-- Agendador Online -->
<div
v-if="!loading"
id="card-agendador"
class="notif-card flex flex-col bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)] transition-shadow duration-200 hover:shadow-[0_4px_18px_rgba(0,0,0,0.06)]"
:class="{ 'border-red-300 shadow-[0_0_0_3px_rgba(239,68,68,0.07)]': solicitacoesPendentes > 0 }"
class="dash-card rounded-md"
:class="{ '': solicitacoesPendentes > 0 }"
>
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2 border-b border-[var(--surface-border,#f1f5f9)]">
<div class="w-8 h-8 rounded-md flex items-center justify-center text-[0.9rem] flex-shrink-0 bg-red-500/10 text-red-500"><i class="pi pi-inbox" /></div>
<div class="flex-1">
<span class="block text-[0.78rem] font-bold text-[var(--text-color)]">Agendador Online</span>
<span class="block text-xs text-[var(--text-color-secondary)]">Solicitações do portal externo</span>
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-inbox w-10 h-10 rounded-md cfg-subheader__icon" />
<div>
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Agendamentos Recebidos</div>
<div class="dash-card__sub">Solicitações vindas do agendador online</div>
</div>
<span v-if="solicitacoesPendentes > 0" class="rounded-full px-1.5 py-px text-xs font-bold bg-red-50 text-red-500 border border-red-300">{{ solicitacoesPendentes }}</span>
<span v-if="solicitacoesPendentes > 0" class="dash-card__badge" style="background:color-mix(in srgb,#ef4444 10%,transparent);color:#ef4444;border:1px solid color-mix(in srgb,#ef4444 30%,transparent)">{{ solicitacoesPendentes }}</span>
</div>
<div class="flex-1 flex flex-col gap-1.5 px-3.5 py-1.5 min-h-[72px]">
<div v-for="sol in solicitacoes" :key="sol.id" class="flex items-center gap-2 py-1">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500 to-sky-400 text-white text-[0.6rem] font-bold flex items-center justify-center flex-shrink-0">{{ sol.initials }}</div>
<div class="flex-1 min-w-0">
<span class="block text-xs font-semibold">{{ sol.nome }}</span>
<span class="block text-xs text-[var(--text-color-secondary)]">{{ sol.detalhe }}</span>
<div class="dash-card__body">
<div v-for="sol in solicitacoes" :key="sol.id" class="dash-item bg-[color-mix(in_srgb,var(--surface-ground)_50%,transparent)] p-2 gap-2 rounded-md">
<Avatar :label="sol.initials" shape="square" size="normal" />
<div class="min-w-0 flex-1">
<div class="dash-item__name">{{ sol.nome }}</div>
<div class="dash-item__sub">{{ sol.detalhe }}</div>
</div>
<div class="flex gap-1">
<button class="flex items-center gap-1 px-2 py-0.5 rounded bg-green-100 text-green-700 text-xs font-bold hover:bg-green-200" @click="aceitarSol(sol.id)"><i class="pi pi-check" /> Aceitar</button>
<button class="px-1.5 py-0.5 rounded bg-red-50 text-red-500 text-xs hover:bg-red-100" @click="recusarSol(sol.id)"><i class="pi pi-times" /></button>
<div class="dash-item__actions">
<button class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-green-100 text-green-700 text-sm font-bold hover:bg-green-200 transition-colors" @click="aceitarSol(sol.id)">
<i class="pi pi-check text-xs" /> Aceitar
</button>
<button class="px-2.5 py-1.5 rounded-full bg-red-50 text-red-500 hover:bg-red-100 transition-colors" @click="recusarSol(sol.id)">
<i class="pi pi-times text-xs" />
</button>
</div>
</div>
<div v-if="!solicitacoes.length" class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] py-1">
<div v-if="!solicitacoes.length" class="dash-empty">
<i class="pi pi-check-circle" /> Nenhuma solicitação pendente
</div>
</div>
<div class="px-3.5 py-1.5 text-xs font-bold text-[var(--primary-color,#6366f1)] cursor-pointer border-t border-[var(--surface-border,#f1f5f9)] hover:bg-[var(--surface-ground,#f8fafc)] transition-colors duration-100" @click="$router.push('/therapist/agendador/solicitacoes')">
<div class="dash-card__foot" @click="$router.push('/therapist/agendamentos-recebidos')">
Ver todas
</div>
</div>
<!-- Cadastros externos -->
<div id="card-cadastros" class="notif-card flex flex-col bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)] transition-shadow duration-200 hover:shadow-[0_4px_18px_rgba(0,0,0,0.06)]">
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2 border-b border-[var(--surface-border,#f1f5f9)]">
<div class="w-8 h-8 rounded-md flex items-center justify-center text-[0.9rem] flex-shrink-0 bg-sky-500/10 text-sky-500"><i class="pi pi-user-plus" /></div>
<div class="flex-1">
<span class="block text-[0.78rem] font-bold text-[var(--text-color)]">Cadastros Externos</span>
<span class="block text-xs text-[var(--text-color-secondary)]">Pacientes aguardando triagem</span>
<div v-if="!loading" id="card-cadastros" class="dash-card">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-user-plus w-10 h-10 rounded-md cfg-subheader__icon" style="background:color-mix(in srgb,#0ea5e9 15%,transparent);color:#0ea5e9" />
<div>
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Cadastros Recebidos (Externos)</div>
<div class="dash-card__sub">Pacientes que preencheram seus próprios dados</div>
</div>
<span v-if="cadastrosPendentes > 0" class="rounded-full px-1.5 py-px text-xs font-bold bg-blue-50 text-blue-500 border border-blue-200">{{ cadastrosPendentes }}</span>
<span v-if="cadastrosPendentes > 0" class="dash-card__badge" style="background:color-mix(in srgb,#0ea5e9 10%,transparent);color:#0ea5e9;border:1px solid color-mix(in srgb,#0ea5e9 30%,transparent)">{{ cadastrosPendentes }}</span>
</div>
<div class="flex-1 flex flex-col gap-1.5 px-3.5 py-1.5 min-h-[72px]">
<div v-for="c in cadastros" :key="c.id" class="flex items-center gap-2">
<div class="w-[26px] h-[26px] rounded-full bg-gradient-to-br from-sky-400 to-indigo-500 text-white text-[0.58rem] font-bold flex items-center justify-center flex-shrink-0">{{ c.initials }}</div>
<div class="flex-1 min-w-0">
<span class="block text-[0.7rem] font-semibold">{{ c.nome }}</span>
<span class="block text-xs text-[var(--text-color-secondary)]">{{ c.detalhe }}</span>
<div class="dash-card__body">
<div v-for="c in cadastros" :key="c.id" class="dash-item bg-[color-mix(in_srgb,var(--surface-ground)_50%,transparent)] p-2 gap-2 rounded-md">
<Avatar :label="c.initials" shape="square" size="normal" />
<div class="min-w-0 flex-1">
<div class="dash-item__name">{{ c.nome }}</div>
<div class="dash-item__sub">{{ c.detalhe }}</div>
</div>
<button
class="w-[26px] h-[26px] rounded-full border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground)] text-[var(--text-color-secondary)] text-[0.6rem] cursor-pointer flex items-center justify-center transition-all duration-100 hover:bg-[var(--primary-color,#6366f1)] hover:text-white hover:border-[var(--primary-color)]"
class="w-8 h-8 rounded-full border border-[var(--surface-border)] bg-[var(--surface-ground)] text-[var(--text-color-secondary)] flex items-center justify-center transition-all hover:bg-[var(--primary-color)] hover:text-white hover:border-[var(--primary-color)]"
@click="$router.push('/therapist/pacientes/triagem/' + c.id)"
>
<i class="pi pi-arrow-right" />
<i class="pi pi-arrow-right text-xs" />
</button>
</div>
<div v-if="!cadastros.length" class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] py-1">
<div v-if="!cadastros.length" class="dash-empty">
<i class="pi pi-check-circle" /> Nenhum cadastro pendente
</div>
</div>
<div class="px-3.5 py-1.5 text-xs font-bold text-[var(--primary-color,#6366f1)] cursor-pointer border-t border-[var(--surface-border,#f1f5f9)] hover:bg-[var(--surface-ground,#f8fafc)] transition-colors duration-100" @click="$router.push('/therapist/pacientes/triagem')">
<div class="dash-card__foot" @click="$router.push('/therapist/patients/cadastro/recebidos')">
Gerenciar triagem
</div>
</div>
<!-- Recorrências com alerta -->
<div id="card-recorrencias" class="notif-card flex flex-col bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)] transition-shadow duration-200 hover:shadow-[0_4px_18px_rgba(0,0,0,0.06)]">
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2 border-b border-[var(--surface-border,#f1f5f9)]">
<div class="w-8 h-8 rounded-md flex items-center justify-center text-[0.9rem] flex-shrink-0 bg-amber-500/10 text-amber-500"><i class="pi pi-refresh" /></div>
<div class="flex-1">
<span class="block text-[0.78rem] font-bold text-[var(--text-color)]">Recorrências</span>
<span class="block text-xs text-[var(--text-color-secondary)]">Atenção necessária</span>
<div v-if="!loading" id="card-recorrencias" class="dash-card">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-refresh w-10 h-10 rounded-md cfg-subheader__icon" style="background:color-mix(in srgb,#f59e0b 15%,transparent);color:#f59e0b" />
<div>
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Recorrências</div>
<div class="dash-card__sub">Atenção necessária</div>
</div>
<span v-if="recAlerta.length" class="rounded-full px-1.5 py-px text-xs font-bold bg-amber-50 text-amber-500 border border-amber-200">{{ recAlerta.length }}</span>
<span v-if="recAlerta.length" class="dash-card__badge" style="background:color-mix(in srgb,#f59e0b 10%,transparent);color:#f59e0b;border:1px solid color-mix(in srgb,#f59e0b 30%,transparent)">{{ recAlerta.length }}</span>
</div>
<div class="flex-1 flex flex-col gap-1.5 px-3.5 py-1.5 min-h-[72px]">
<div v-for="r in recAlerta" :key="r.id" class="flex items-center gap-2.5 py-1">
<div class="flex-1">
<span class="block text-[0.7rem] font-semibold">{{ r.nome }}</span>
<span
class="block text-xs font-semibold mt-0.5"
:class="{
'text-red-500': r.tipo === 'limite',
'text-amber-500': r.tipo === 'conflito',
'text-sky-500': r.tipo === 'feriado',
}"
>{{ r.motivo }}</span>
</div>
<div v-if="r.progresso !== undefined" class="flex items-center gap-1.5">
<div class="w-[55px] h-[5px] rounded bg-[var(--surface-border,#e2e8f0)] overflow-hidden">
<div
class="h-full rounded transition-all duration-300"
:style="{ width: r.progresso + '%' }"
:class="r.progresso > 75 ? 'bg-red-500' : 'bg-[var(--primary-color,#6366f1)]'"
/>
<div class="dash-card__body">
<div v-for="r in recAlerta" :key="r.id" class="dash-item bg-[color-mix(in_srgb,var(--surface-ground)_50%,transparent)] p-2 gap-2 rounded-md">
<div class="min-w-0 flex-1">
<div class="dash-item__name">{{ r.nome }}</div>
<div class="dash-item__sub" :class="{ 'text-red-500': r.tipo === 'limite', 'text-amber-500': r.tipo === 'conflito', 'text-sky-500': r.tipo === 'feriado' }">
{{ r.motivo }}
</div>
<span class="text-xs text-[var(--text-color-secondary)] whitespace-nowrap">{{ r.sessoesUsadas }}/{{ r.totalSessoes }}</span>
</div>
<div v-if="r.progresso !== undefined" class="flex items-center gap-2 shrink-0">
<div class="w-14 h-1.5 rounded-full bg-[var(--surface-border)] overflow-hidden">
<div class="h-full rounded-full transition-all duration-300" :style="{ width: r.progresso + '%' }" :class="r.progresso > 75 ? 'bg-red-500' : 'bg-[var(--primary-color)]'" />
</div>
<span class="text-sm text-[var(--text-color-secondary)] whitespace-nowrap">{{ r.sessoesUsadas }}/{{ r.totalSessoes }}</span>
</div>
</div>
<div v-if="!recAlerta.length" class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] py-1">
<div v-if="!recAlerta.length" class="dash-empty">
<i class="pi pi-check-circle" /> Recorrências em dia
</div>
</div>
<div class="px-3.5 py-1.5 text-xs font-bold text-[var(--primary-color,#6366f1)] cursor-pointer border-t border-[var(--surface-border,#f1f5f9)] hover:bg-[var(--surface-ground,#f8fafc)] transition-colors duration-100" @click="$router.push('/therapist/agenda/recorrencias')">
<div class="dash-card__foot" @click="$router.push('/therapist/agenda/recorrencias')">
Ver recorrências
</div>
</div>
<!-- Radar da semana -->
<div id="card-radar" class="notif-card flex flex-col bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)] transition-shadow duration-200 hover:shadow-[0_4px_18px_rgba(0,0,0,0.06)]">
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2 border-b border-[var(--surface-border,#f1f5f9)]">
<div class="w-8 h-8 rounded-md flex items-center justify-center text-[0.9rem] flex-shrink-0 bg-indigo-500/10 text-indigo-500"><i class="pi pi-chart-pie" /></div>
<div class="flex-1">
<span class="block text-[0.78rem] font-bold text-[var(--text-color)]">Radar da Semana</span>
<span class="block text-xs text-[var(--text-color-secondary)]">Presença, faltas e reposições</span>
<div v-if="!loading" id="card-radar" class="dash-card">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-chart-pie w-10 h-10 rounded-md cfg-subheader__icon" style="background:color-mix(in srgb,#6366f1 15%,transparent);color:#6366f1" />
<div>
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Radar da Semana</div>
<div class="dash-card__sub">Presença, faltas e reposições</div>
</div>
</div>
<div class="px-3.5 pt-1.5 pb-2.5 mt-4">
<div class="flex gap-1.5 items-end h-[72px]">
<div v-for="d in radarSemana" :key="d.dia" class="flex-1 flex flex-col items-center gap-0.5">
<div class="dash-card__body" style="min-height:auto">
<div class="flex gap-1.5 items-end h-[88px] mt-6">
<div v-for="d in radarSemana" :key="d.dia" class="flex-1 flex flex-col items-center gap-1">
<div class="flex-1 w-full flex items-end">
<div class="w-full h-[52px] bg-[var(--surface-ground,#f8fafc)] rounded overflow-hidden flex items-end">
<div class="w-full h-[68px] bg-[var(--surface-ground)] rounded overflow-hidden flex items-end">
<div
class="w-full rounded transition-all duration-500 ease-in-out"
:style="{ height: d.pct + '%' }"
@@ -364,14 +516,20 @@
/>
</div>
</div>
<span class="text-xs font-semibold text-[var(--text-color-secondary)]" :class="{ 'text-[var(--primary-color,#6366f1)] font-extrabold': d.isToday }">{{ d.dia }}</span>
<span class="text-sm font-semibold" :class="d.isToday ? 'text-[var(--primary-color)] font-extrabold' : 'text-[var(--text-color-secondary)]'">{{ d.dia }}</span>
<span class="text-xs text-[var(--text-color-secondary)]">{{ d.total }}</span>
</div>
</div>
<div class="flex gap-2.5 mt-1.5 flex-wrap">
<span class="flex items-center gap-1 text-xs text-[var(--text-color-secondary)] font-semibold"><span class="w-[17px] h-2.5 rounded-sm bg-gradient-to-r from-indigo-500 to-indigo-400 flex-shrink-0" />Presentes</span>
<span class="flex items-center gap-1 text-xs text-[var(--text-color-secondary)] font-semibold"><span class="w-[17px] h-2.5 rounded-sm bg-gradient-to-r from-red-500 to-red-300 flex-shrink-0" />Faltas</span>
<span class="flex items-center gap-1 text-xs text-[var(--text-color-secondary)] font-semibold"><span class="w-[17px] h-2.5 rounded-sm bg-gradient-to-r from-amber-500 to-amber-300 flex-shrink-0" />Reposição</span>
<div class="flex gap-3 mt-2 flex-wrap">
<span class="flex items-center gap-1.5 text-sm text-[var(--text-color-secondary)] font-semibold">
<span class="w-4 h-2.5 rounded-sm bg-gradient-to-r from-indigo-500 to-indigo-400 flex-shrink-0" />Presentes
</span>
<span class="flex items-center gap-1.5 text-sm text-[var(--text-color-secondary)] font-semibold">
<span class="w-4 h-2.5 rounded-sm bg-gradient-to-r from-red-500 to-red-300 flex-shrink-0" />Faltas
</span>
<span class="flex items-center gap-1.5 text-sm text-[var(--text-color-secondary)] font-semibold">
<span class="w-4 h-2.5 rounded-sm bg-gradient-to-r from-amber-500 to-amber-300 flex-shrink-0" />Reposição
</span>
</div>
</div>
</div>
@@ -379,14 +537,45 @@
</section>
<!-- Compromissos especiais -->
<section class="bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)] px-[1.125rem] py-3.5">
<section v-if="loading" class="bg-[var(--surface-card)] rounded-md border border-[var(--surface-border)] px-[1.125rem] py-3.5">
<div class="flex items-center justify-between mb-2.5">
<div class="flex items-center gap-1.5 text-xs font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)]">
<i class="pi pi-briefcase" /> Compromissos especiais
<Skeleton width="10rem" height="12px" />
<Skeleton width="5rem" height="12px" />
</div>
<div class="flex flex-col gap-1.5">
<div v-for="n in 3" :key="n" class="flex items-center gap-2.5 px-2.5 py-2 rounded-md bg-[var(--surface-ground)]">
<Skeleton width="3px" height="28px" border-radius="4px" />
<div class="flex flex-col gap-1 flex-1">
<Skeleton :width="n === 1 ? '12rem' : n === 2 ? '9rem' : '11rem'" height="11px" />
<Skeleton width="6rem" height="10px" />
</div>
<div class="flex flex-col items-end gap-1">
<Skeleton width="4rem" height="11px" />
<Skeleton width="5rem" height="10px" />
</div>
</div>
<button class="flex items-center gap-1 bg-transparent border-none cursor-pointer text-xs font-semibold text-[var(--primary-color,#6366f1)] p-0" @click="$router.push('/therapist/compromissos')">
Ver todos <i class="pi pi-arrow-right" />
</button>
</div>
</section>
<section v-if="!loading" class="bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border,#e2e8f0)] p-2.5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5 text-xs font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)]">
<i class="pi pi-briefcase" /> Compromissos especiais (Em breve)
</div>
<span
v-tooltip.top="{
value: 'Em breve!',
showDelay: 0,
hideDelay: 100
}"
>
<button
class="flex items-center gap-1 bg-transparent border-none cursor-pointer text-xs font-semibold text-[var(--primary-color,#6366f1)] p-0"
disabled
>
Ver todos <i class="pi pi-arrow-right" />
</button>
</span>
</div>
<div class="flex flex-col gap-1.5">
<div v-for="c in commitments" :key="c.id" class="flex items-center gap-2.5 px-2.5 py-2 rounded-md bg-[var(--surface-ground,#f8fafc)] hover:bg-[var(--surface-hover,#f1f5f9)] transition-colors duration-100">
@@ -419,6 +608,8 @@
</div>
</section>
<LoadedPhraseBlock v-if="!loading" />
</main>
<!-- Menus de contexto (fora do aside para evitar visibility:hidden) -->
@@ -723,6 +914,7 @@ async function onAgendaDialogSave (arg) {
}
}
const loading = ref(true)
const ownerId = ref(null)
const eventosDoMes = ref([])
const regraRecorrencias = ref([])
@@ -1022,9 +1214,10 @@ const nowCursorLeft = computed(() => {
})
async function load () {
loading.value = true
const { data: authData } = await supabase.auth.getUser()
ownerId.value = authData?.user?.id || null
if (!ownerId.value) return
if (!ownerId.value) { loading.value = false; return }
await tenantStore.ensureLoaded()
const tid = tenantStore.activeTenantId || tenantStore.tenantId || null
await loadCommitments()
@@ -1054,6 +1247,7 @@ async function load () {
}
regraRecorrencias.value = rawRules
} catch (e) { console.error('[TherapistDashboard] load:', e) }
finally { loading.value = false }
}
onMounted(async () => {
@@ -1094,4 +1288,58 @@ onMounted(async () => {
0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.4); }
50% { box-shadow: 0 0 0 4px rgba(239,68,68,0); }
}
/* ── Aside: Eventos do dia ───────────────────────── */
.aside-ev {
display: flex; align-items: center; gap: 0.625rem;
padding: 0.5rem 0.625rem;
border-radius: 8px;
border-left: 3px solid var(--ev-color, var(--primary-color, #6366f1));
background: var(--surface-ground, #f8fafc);
cursor: pointer;
transition: background 0.12s, box-shadow 0.12s;
}
.aside-ev:hover { background: var(--surface-hover, #f1f5f9); box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.aside-ev--reuniao { --ev-color: #38bdf8; }
.aside-ev--realizado { --ev-color: #22c55e; }
.aside-ev--default { --ev-color: var(--primary-color, #6366f1); }
.aside-ev--skeleton { cursor: default; pointer-events: none; }
.aside-ev__time {
display: flex; flex-direction: column; align-items: flex-end;
min-width: 36px; flex-shrink: 0; gap: 1px;
}
.aside-ev__hora {
font-size: 0.78rem; font-weight: 800; color: var(--text-color);
letter-spacing: -0.01em; line-height: 1;
}
.aside-ev__dur {
font-size: 0.68rem; color: var(--text-color-secondary); line-height: 1;
}
.aside-ev__badge {
font-size: 0.68rem; font-weight: 600; padding: 1px 6px;
border-radius: 4px; background: var(--surface-border, #e2e8f0);
color: var(--text-color-secondary); line-height: 1.5; white-space: nowrap;
}
/* ── Aside: Recorrências ativas ──────────────────── */
.aside-rec {
display: flex; align-items: center; gap: 0.625rem;
padding: 0.45rem 0.5rem;
border-radius: 8px;
border: 1px solid transparent;
cursor: pointer;
transition: background 0.12s, border-color 0.12s;
}
.aside-rec:hover {
background: var(--surface-ground, #f8fafc);
border-color: var(--surface-border, #e2e8f0);
}
.aside-rec--skeleton { cursor: default; pointer-events: none; }
.aside-rec__prox {
font-size: 0.72rem; white-space: nowrap; flex-shrink: 0;
padding: 2px 7px; border-radius: 999px;
background: color-mix(in srgb, currentColor 10%, transparent);
}
</style>
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/therapist/agenda/MyAppointmentsPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="p-4">
<h1>My Appointments</h1>
@@ -1,3 +1,19 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/therapist/agenda/NewAppointmentPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="p-4">
<h1>Add New Appointments</h1>