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/layout/AppConfigurator.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, inject } from 'vue'
import { useLayout } from '@/layout/composables/layout'
+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/layout/AppFooter.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup></script>
<template>
+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/layout/AppLayout.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { useLayout } from '@/layout/composables/layout'
import { computed, onMounted, onBeforeUnmount, provide, watch } from '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/layout/AppMenu.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
+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/layout/AppMenuFooterPanel.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
+75 -6
View File
@@ -1,10 +1,26 @@
<!-- src/layout/AppMenuItem.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/AppMenuItem.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { useLayout } from '@/layout/composables/layout'
import { computed, ref, nextTick, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import Popover from 'primevue/popover'
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue'
import { useTenantStore } from '@/stores/tenantStore'
import { useEntitlementsStore } from '@/stores/entitlementsStore'
@@ -172,6 +188,8 @@ const onMouseEnter = () => {
}
}
const showCadastroDialog = ref(false)
/* ---------- POPUP + ---------- */
function togglePopover (event) {
if (isBlocked.value) return
@@ -187,7 +205,17 @@ function abrirCadastroRapido () {
emit('quick-create', { entity: props.item?.quickCreateEntity || 'patient', mode: 'rapido' })
}
async function irCadastroCompleto () {
function irCadastroCompleto () {
closePopover()
layoutState.overlayMenuActive = false
layoutState.mobileMenuActive = false
layoutState.menuHoverActive = false
showCadastroDialog.value = true
}
async function irLinkCadastro () {
closePopover()
layoutState.overlayMenuActive = false
@@ -195,11 +223,14 @@ async function irCadastroCompleto () {
layoutState.menuHoverActive = false
await nextTick()
router.push({ name: 'admin-pacientes-cadastro' })
const linkTo = props.item?.quickCreateLinkTo || '/therapist/patients/link-externo'
router.push(linkTo)
}
</script>
<template>
<PatientCadastroDialog v-if="item.quickCreate" v-model="showCadastroDialog" />
<li v-show="isVisible" :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActive }">
<div v-if="root" class="layout-menuitem-root-text">
{{ item.label }}
@@ -255,9 +286,47 @@ async function irCadastroCompleto () {
</div>
<Popover v-if="item.quickCreate" ref="pop">
<div class="flex flex-column gap-2 min-w-[180px]">
<Button label="Cadastro rápido" icon="pi pi-bolt" text @click="abrirCadastroRapido" />
<Button label="Cadastro completo" icon="pi pi-user-plus" text @click="irCadastroCompleto" />
<div class="flex flex-col gap-0.5 min-w-[190px] py-0.5">
<button
class="flex items-center gap-2.5 px-3 py-2 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]"
@click="abrirCadastroRapido"
>
<div class="w-7 h-7 rounded-md flex items-center justify-center flex-shrink-0 bg-indigo-500/10 text-indigo-500">
<i class="pi pi-bolt text-xs" />
</div>
<div>
<div class="text-sm font-semibold text-[var(--text-color)]">Cadastro Rápido</div>
<div class="text-[0.68rem] text-[var(--text-color-secondary)]">Nome, e-mail e telefone</div>
</div>
</button>
<button
class="flex items-center gap-2.5 px-3 py-2 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]"
@click="irCadastroCompleto"
>
<div class="w-7 h-7 rounded-md flex items-center justify-center flex-shrink-0 bg-emerald-500/10 text-emerald-600">
<i class="pi pi-user-plus text-xs" />
</div>
<div>
<div class="text-sm font-semibold text-[var(--text-color)]">Cadastro Completo</div>
<div class="text-[0.68rem] text-[var(--text-color-secondary)]">Formulário detalhado</div>
</div>
</button>
<div class="mx-3 my-1 border-t border-[var(--surface-border,#e2e8f0)]" />
<button
class="flex items-center gap-2.5 px-3 py-2 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]"
@click="irLinkCadastro"
>
<div class="w-7 h-7 rounded-md flex items-center justify-center flex-shrink-0 bg-sky-500/10 text-sky-600">
<i class="pi pi-link text-xs" />
</div>
<div>
<div class="text-sm font-semibold text-[var(--text-color)]">Link de Cadastro</div>
<div class="text-[0.68rem] text-[var(--text-color-secondary)]">Enviar link ao paciente</div>
</div>
</button>
</div>
</Popover>
+17 -1
View File
@@ -1,4 +1,20 @@
<!-- src/layout/AppRail.vue Mini icon rail (Layout 2) -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/AppRail.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<!-- Mini icon rail (Layout 2) -->
<script setup>
import { computed, ref } from 'vue'
+70 -15
View File
@@ -1,4 +1,20 @@
<!-- src/layout/AppRailPanel.vue Painel expansível do Layout 2 -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/AppRailPanel.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<!-- Painel expansível do Layout 2 -->
<script setup>
import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useRouter, useRoute } from 'vue-router'
@@ -8,6 +24,9 @@ import { useLayout } from './composables/layout'
import { useEntitlementsStore } from '@/stores/entitlementsStore'
import { useMenuBadges } from '@/composables/useMenuBadges'
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue'
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue'
const menuStore = useMenuStore()
const { layoutState } = useLayout()
const entitlements = useEntitlementsStore()
@@ -84,6 +103,15 @@ function closePanel () {
layoutState.railPanelOpen = false
}
// ── QuickCreate (Pacientes) ───────────────────────────────
const createPopover = ref(null)
const quickDialog = ref(false)
function openQuickCreate (event, item) {
createPopover.value?.toggle(event)
}
function onQuickCreate () { quickDialog.value = true }
// ── Busca (todo o menu) ──────────────────────────────────────
const query = ref('')
const showResults = ref(false)
@@ -387,24 +415,51 @@ async function goToResult (r) {
</div>
<!-- Item folha -->
<button
v-else
class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-[9px] border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-left text-[0.83rem] font-medium transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
:class="{
'!bg-[color-mix(in_srgb,var(--primary-color)_10%,transparent)] !text-[var(--primary-color)] !font-semibold': isActive(item),
'opacity-55': isLocked(item)
}"
@click="navigate(item)"
>
<i v-if="item.icon" :class="item.icon" class="text-[1rem] shrink-0 opacity-75" />
<span class="flex-1">{{ item.label }}</span>
<span v-if="isLocked(item)" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
<span v-if="menuBadgeLabel(item)" class="text-[0.62rem] font-bold px-1.5 py-px rounded-full bg-[var(--primary-color)] text-white leading-none">{{ menuBadgeLabel(item) }}</span>
</button>
<div v-else class="flex items-center gap-1">
<button
class="flex-1 flex items-center gap-2.5 px-2.5 py-2 rounded-[9px] border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-left text-[0.83rem] font-medium transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
:class="{
'!bg-[color-mix(in_srgb,var(--primary-color)_10%,transparent)] !text-[var(--primary-color)] !font-semibold': isActive(item),
'opacity-55': isLocked(item)
}"
@click="navigate(item)"
>
<i v-if="item.icon" :class="item.icon" class="text-[1rem] shrink-0 opacity-75" />
<span class="flex-1">{{ item.label }}</span>
<span v-if="isLocked(item)" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
<span v-if="menuBadgeLabel(item)" class="text-[0.62rem] font-bold px-1.5 py-px rounded-full bg-[var(--primary-color)] text-white leading-none">{{ menuBadgeLabel(item) }}</span>
</button>
<button
v-if="item.quickCreate"
class="w-6 h-6 shrink-0 rounded-md border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-xs transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
@click.stop="openQuickCreate($event, item)"
title="Novo paciente"
>
<i class="pi pi-plus" />
</button>
</div>
</template>
</template>
</nav>
<!-- PatientCreatePopover (shared) -->
<PatientCreatePopover
ref="createPopover"
@quick-create="onQuickCreate"
/>
<!-- Cadastro Rápido Dialog -->
<ComponentCadastroRapido
v-model="quickDialog"
title="Cadastro Rápido"
table-name="patients"
name-field="nome_completo"
email-field="email_principal"
phone-field="telefone"
:extra-payload="{ status: 'Ativo' }"
@created="quickDialog = false"
/>
</aside>
</Transition>
</template>
+88 -15
View File
@@ -1,4 +1,20 @@
<!-- src/layout/AppRailSidebar.vue Drawer mobile para Layout Rail -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/AppRailSidebar.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<!-- Drawer mobile para Layout Rail -->
<script setup>
import { computed, ref, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
@@ -8,6 +24,9 @@ import { useLayout } from './composables/layout'
import { useEntitlementsStore } from '@/stores/entitlementsStore'
import { useMenuBadges } from '@/composables/useMenuBadges'
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue'
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue'
const menuStore = useMenuStore()
const { layoutState, hideMobileMenu } = useLayout()
const entitlements = useEntitlementsStore()
@@ -244,6 +263,15 @@ function navigate (item) {
// Fecha ao navegar
watch(() => route.path, () => hideMobileMenu())
// ── QuickCreate (Pacientes) ───────────────────────────────
const createPopover = ref(null)
const quickDialog = ref(false)
function openQuickCreate (event, item) {
createPopover.value?.toggle(event)
}
function onQuickCreate () { quickDialog.value = true }
</script>
<template>
@@ -404,24 +432,51 @@ watch(() => route.path, () => hideMobileMenu())
</template>
<!-- Item folha -->
<button
v-else
class="rs__item"
:class="{
'rs__item--active': isActive(item),
'rs__item--locked': isLocked(item)
}"
@click="navigate(item)"
>
<i v-if="item.icon" :class="item.icon" class="rs__item-icon" />
<span>{{ item.label }}</span>
<span v-if="isLocked(item)" class="rs__pro">PRO</span>
<span v-if="menuBadgeLabel(item)" class="rs__badge">{{ menuBadgeLabel(item) }}</span>
</button>
<div v-else class="flex items-center gap-1">
<button
class="rs__item flex-1"
:class="{
'rs__item--active': isActive(item),
'rs__item--locked': isLocked(item)
}"
@click="navigate(item)"
>
<i v-if="item.icon" :class="item.icon" class="rs__item-icon" />
<span>{{ item.label }}</span>
<span v-if="isLocked(item)" class="rs__pro">PRO</span>
<span v-if="menuBadgeLabel(item)" class="rs__badge">{{ menuBadgeLabel(item) }}</span>
</button>
<button
v-if="item.quickCreate"
class="rs__quick-add"
@click.stop="openQuickCreate($event, item)"
title="Novo paciente"
>
<i class="pi pi-plus" />
</button>
</div>
</template>
</div>
</template>
</nav>
<!-- PatientCreatePopover (shared) -->
<PatientCreatePopover
ref="createPopover"
@quick-create="onQuickCreate"
/>
<!-- Cadastro Rápido Dialog -->
<ComponentCadastroRapido
v-model="quickDialog"
title="Cadastro Rápido"
table-name="patients"
name-field="nome_completo"
email-field="email_principal"
phone-field="telefone"
:extra-payload="{ status: 'Ativo' }"
@created="quickDialog = false"
/>
</aside>
</Transition>
</template>
@@ -693,6 +748,24 @@ watch(() => route.path, () => hideMobileMenu())
color: #fff;
line-height: 1;
}
.rs__quick-add {
width: 24px;
height: 24px;
flex-shrink: 0;
border-radius: 6px;
border: none;
background: transparent;
color: var(--text-color-secondary);
cursor: pointer;
display: grid;
place-items: center;
font-size: 0.7rem;
transition: background 0.13s, color 0.13s;
}
.rs__quick-add:hover {
background: var(--surface-ground);
color: var(--text-color);
}
/* ── Slide-in da esquerda ────────────────────────────────── */
.rs-slide-enter-active,
+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/layout/AppSidebar.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { useLayout } from '@/layout/composables/layout'
import { onBeforeUnmount, ref, watch } from 'vue'
+16 -2
View File
@@ -1,4 +1,19 @@
<!-- src/layout/AppTopbar.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/AppTopbar.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, ref, onMounted, provide, nextTick, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
@@ -539,7 +554,6 @@ onMounted(async () => {
</script>
<template>
<Toast />
<header class="rail-topbar">
<!-- Esquerda -->
<div class="rail-topbar__left">
+89 -199
View File
@@ -1,4 +1,19 @@
<!-- src/layout/ConfiguracoesPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/ConfiguracoesPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -6,6 +21,8 @@ import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const showMenu = ref(false)
// ── Hero sticky ────────────────────────────────────────────
const headerEl = ref(null)
const headerSentinelRef = ref(null)
@@ -111,6 +128,10 @@ function ir(to) {
}
onMounted(() => {
requestAnimationFrame(() => {
showMenu.value = true
})
const rootMargin = `${document.querySelector('.l2-main') ? '0px' : '-56px'} 0px 0px 0px`
_observer = new IntersectionObserver(
([entry]) => { headerStuck.value = !entry.isIntersecting },
@@ -124,21 +145,29 @@ onBeforeUnmount(() => { _observer?.disconnect() })
<template>
<!-- Sentinel -->
<div ref="headerSentinelRef" class="cfg-sentinel" />
<div ref="headerSentinelRef" class="h-px" />
<!-- Hero compacto padrão Compromissos -->
<div ref="headerEl" class="cfg-hero mx-3 md:mx-4 mb-3" :class="{ 'cfg-hero--stuck': headerStuck }">
<div class="cfg-hero__blobs" aria-hidden="true">
<div class="cfg-hero__blob cfg-hero__blob--1" />
<div class="cfg-hero__blob cfg-hero__blob--2" />
<!-- Hero compacto -->
<div
ref="headerEl"
class="sticky top-[var(--layout-sticky-top,56px)] z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-3 py-2.5 mx-3 md:mx-4 mb-3"
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
>
<!-- Blobs decorativos -->
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-emerald-300/10" />
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-indigo-500/[0.09]" />
</div>
<div class="cfg-hero__inner">
<div class="relative z-[1] flex items-center gap-3">
<!-- Brand -->
<div class="cfg-hero__brand">
<div class="cfg-hero__icon"><i class="pi pi-cog text-base" /></div>
<div class="flex items-center gap-2 flex-shrink-0">
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/[0.12] text-[var(--p-primary-500,#6366f1)]">
<i class="pi pi-cog text-base" />
</div>
<div class="min-w-0 lg:block">
<div class="cfg-hero__title">Configurações</div>
<div class="cfg-hero__sub">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Configurações</div>
<div class="text-xs text-[var(--text-color-secondary)]">
<span v-if="activeSecao">
<i :class="activeSecao.icon" class="text-xs mr-1 opacity-60" />{{ activeSecao.label }}
</span>
@@ -148,52 +177,78 @@ onBeforeUnmount(() => { _observer?.disconnect() })
</div>
<!-- Ações -->
<div class="cfg-hero__actions">
<div class="flex items-center gap-2 ml-auto">
<Button icon="pi pi-arrow-left" severity="secondary" outlined class="h-9 w-9 rounded-full" v-tooltip.bottom="'Voltar'" @click="router.back()" />
</div>
</div>
</div>
<!-- Stats: seções como cards clicáveis -->
<!-- Cards de seção (stats row) -->
<div class="flex flex-wrap gap-2 px-3 md:px-4 mb-3">
<button
v-for="s in secoes"
:key="s.key"
class="cfg-sec-card"
:class="{ 'cfg-sec-card--active': activeTo === s.to }"
class="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] cursor-pointer whitespace-nowrap transition-[border-color,background,box-shadow] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
:class="
activeTo === s.to
? 'cfg-sec-card--active'
: 'hover:border-indigo-500/40'
"
@click="ir(s.to)"
>
<i :class="s.icon" class="cfg-sec-card__icon" />
<span class="cfg-sec-card__label">{{ s.label }}</span>
<i
:class="[s.icon, activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color-secondary)] opacity-75']"
class="text-[0.78rem]"
/>
<span
class="text-[0.78rem] font-semibold"
:class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color)]'"
>{{ s.label }}</span>
</button>
</div>
<!-- Layout: sidebar + conteúdo -->
<div class="flex flex-col xl:flex-row gap-3 px-3 md:px-4 pb-5 items-start">
<!-- Sidebar: lista de seções (oculto no mobile temos os cards acima) -->
<div class="hidden xl:flex flex-col gap-1 w-[260px] shrink-0 cfg-sidebar-col">
<div class="cfg-sidebar-wrap">
<div class="cfg-sidebar-head">
<i class="pi pi-cog text-xs opacity-60" />
<!-- Sidebar (oculto no mobile) -->
<div
class="hidden xl:flex flex-col gap-1 w-[300px] shrink-0"
style="position: sticky; top: calc(var(--layout-sticky-top, 56px) + 58px); align-self: flex-start;"
>
<div class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] overflow-hidden">
<!-- Cabeçalho -->
<div class="flex items-center gap-1.5 px-3.5 py-2.5 border-b border-[var(--surface-border)] text-[0.7rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65">
<i class="pi pi-cog" />
<span>Seções</span>
</div>
<div class="flex flex-col gap-0.5">
<!-- Itens -->
<TransitionGroup name="menu" tag="div" class="flex flex-col gap-0.5">
<button
v-for="s in secoes"
:key="s.key"
class="cfg-nav-item"
:class="{ 'cfg-nav-item--active': activeTo === s.to }"
v-for="(s, i) in showMenu ? secoes : []"
:key="s.key"
:style="{ transitionDelay: `${i * 50}ms` }"
class="flex items-center gap-2.5 px-3.5 py-2.5 border-b last:border-b-0 bg-transparent cursor-pointer w-full text-left transition-colors duration-[120ms] hover:bg-[var(--surface-hover)]"
:class="activeTo === s.to ? 'cfg-nav-item--active border border-[var(--primary-color,#6366f1)] last:border-b-1 last:rounded-bl-[6px] last:rounded-br-[6px]' : ''"
@click="ir(s.to)"
>
<i :class="s.icon" class="cfg-nav-item__icon" />
<div class="cfg-nav-item__body">
<span class="cfg-nav-item__label">{{ s.label }}</span>
<span class="cfg-nav-item__desc">{{ s.desc }}</span>
<i
:class="[s.icon, activeTo === s.to ? 'text-[var(--primary-color,#6366f1)] opacity-100' : 'text-[var(--text-color-secondary)] opacity-60']"
class="text-[0.85rem] flex-shrink-0 w-4 text-center"
/>
<div class="flex-1 min-w-0 flex flex-col gap-px ">
<span
class="text-[0.90rem] font-semibold truncate"
:class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color)]'"
>{{ s.label }}</span>
<span class="text-[0.88rem] text-[var(--text-color-secondary)] opacity-70">{{ s.desc }}</span>
</div>
<i class="pi pi-chevron-right cfg-nav-item__arrow" />
<i
class="pi pi-chevron-right text-[0.6rem] flex-shrink-0"
:class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)] opacity-60' : 'text-[var(--text-color-secondary)] opacity-30'"
/>
</button>
</div>
</TransitionGroup>
</div>
</div>
@@ -206,178 +261,13 @@ onBeforeUnmount(() => { _observer?.disconnect() })
</template>
<style scoped>
/* ── Hero ─────────────────────────────────────────────── */
.cfg-sentinel { height: 1px; }
.cfg-hero {
position: sticky;
top: var(--layout-sticky-top, 56px);
z-index: 20;
overflow: hidden;
border-radius: 6px;
border: 1px solid var(--surface-border);
background: var(--surface-card);
padding: 10px 12px;
}
.cfg-hero--stuck {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.cfg-hero__blobs { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.cfg-hero__blob { position: absolute; border-radius: 50%; filter: blur(60px); }
.cfg-hero__blob--1 { width: 16rem; height: 16rem; top: -4rem; right: -2rem; background: rgba(52,211,153,0.10); }
.cfg-hero__blob--2 { width: 18rem; height: 18rem; top: 0; left: -4rem; background: rgba(99,102,241,0.09); }
.cfg-hero__inner {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 0.75rem;
}
.cfg-hero__brand { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.cfg-hero__icon {
display: grid; place-items: center;
width: 2.25rem; height: 2.25rem; border-radius: 6px; flex-shrink: 0;
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
color: var(--p-primary-500, #6366f1);
}
.cfg-hero__title { font-size: 1rem; font-weight: 700; letter-spacing: -0.02em; color: var(--text-color); }
.cfg-hero__sub { font-size: 0.75rem; color: var(--text-color-secondary); }
.cfg-hero__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
/* Breadcrumb seção ativa */
.cfg-breadcrumb { display: flex; align-items: center; gap: 0.5rem; }
.cfg-breadcrumb__active {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.25rem 0.75rem; border-radius: 999px;
border: 1px solid var(--primary-color, #6366f1);
background: color-mix(in srgb, var(--primary-color) 8%, transparent);
color: var(--primary-color, #6366f1);
font-size: 0.8rem; font-weight: 600;
}
/* ── Cards de seção (stats row) ────────────────────────── */
.cfg-sec-card {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.875rem;
border-radius: 6px;
border: 1px solid var(--surface-border);
background: var(--surface-card);
cursor: pointer;
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
white-space: nowrap;
}
.cfg-sec-card:hover {
border-color: color-mix(in srgb, var(--primary-color) 40%, transparent);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.cfg-sec-card--active {
border-color: var(--primary-color, #6366f1);
background: color-mix(in srgb, var(--primary-color) 8%, var(--surface-card));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 12%, transparent);
}
.cfg-sec-card__icon {
font-size: 0.78rem;
color: var(--text-color-secondary);
opacity: 0.75;
}
.cfg-sec-card--active .cfg-sec-card__icon {
color: var(--primary-color, #6366f1);
opacity: 1;
}
.cfg-sec-card__label {
font-size: 0.78rem;
font-weight: 600;
color: var(--text-color);
}
.cfg-sec-card--active .cfg-sec-card__label {
color: var(--primary-color, #6366f1);
}
/* ── Sidebar col sticky ───────────────────────────────── */
.cfg-sidebar-col {
position: sticky;
top: calc(var(--layout-sticky-top, 56px) + 58px);
align-self: flex-start;
}
/* ── Sidebar nav ──────────────────────────────────────── */
.cfg-sidebar-wrap {
border: 1px solid var(--surface-border);
border-radius: 6px;
background: var(--surface-card);
overflow: hidden;
}
.cfg-sidebar-head {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.625rem 0.875rem;
border-bottom: 1px solid var(--surface-border);
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-color-secondary);
opacity: 0.65;
}
.cfg-nav-item {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.875rem;
border: none;
background: transparent;
cursor: pointer;
transition: background 0.12s;
width: 100%;
text-align: left;
border-bottom: 1px solid var(--surface-border);
}
.cfg-nav-item:last-child { border-bottom: none; }
.cfg-nav-item:hover { background: var(--surface-hover); }
.cfg-nav-item--active {
background: color-mix(in srgb, var(--primary-color) 6%, var(--surface-card));
}
.cfg-nav-item__icon {
font-size: 0.85rem;
color: var(--text-color-secondary);
opacity: 0.6;
flex-shrink: 0;
width: 16px;
text-align: center;
}
.cfg-nav-item--active .cfg-nav-item__icon {
color: var(--primary-color, #6366f1);
opacity: 1;
}
.cfg-nav-item__body {
flex: 1; min-width: 0;
display: flex; flex-direction: column; gap: 1px;
}
.cfg-nav-item__label {
font-size: 0.82rem;
font-weight: 600;
color: var(--text-color);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cfg-nav-item--active .cfg-nav-item__label {
color: var(--primary-color, #6366f1);
}
.cfg-nav-item__desc {
font-size: 0.7rem;
color: var(--text-color-secondary);
opacity: 0.7;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cfg-nav-item__arrow {
font-size: 0.6rem;
color: var(--text-color-secondary);
opacity: 0.3;
flex-shrink: 0;
}
.cfg-nav-item--active .cfg-nav-item__arrow {
color: var(--primary-color, #6366f1);
opacity: 0.6;
}
</style>
+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/layout/areas/AdminLayout.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template><AppLayout area="admin" /></template>
<script setup>
import AppLayout from '../AppLayout.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/layout/areas/PortalLayout.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template><AppLayout area="portal" /></template>
<script setup>
import AppLayout from '../AppLayout.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/layout/areas/TherapistLayout.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template><AppLayout area="therapist" /></template>
<script setup>
import AppLayout from '../AppLayout.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/layout/composables/layout.js
| Data: 2026
| Local: São Carlos/SP — Brasil
|--------------------------------------------------------------------------
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { computed, reactive, ref, onMounted, onBeforeUnmount } from 'vue'
// ── resolve variant salvo no localStorage ───────────────────
+16 -1
View File
@@ -1,4 +1,19 @@
<!-- src/layout/concepcoes/ex-header-conceitual.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/concepcoes/ex-header-conceitual.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<!-- ===========================================================
TEMPLATE DE REFERÊNCIA Hero Header Sticky
Padrão utilizado em: AgendaTerapeutaPage, ProfilePage
+107 -23
View File
@@ -1,20 +1,37 @@
<!-- src/layout/configuracoes/BloqueiosPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/BloqueiosPage.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'
import { useTenantStore } from '@/stores/tenantStore'
import { useToast } from 'primevue/usetoast'
import { useConfirm } from 'primevue/useconfirm'
import { useFeriados } from '@/composables/useFeriados'
import DatePicker from 'primevue/datepicker'
const toast = useToast()
const confirm = useConfirm()
const tenantStore = useTenantStore()
// ── Feriados (nacionais + municipais) ───────────────────────
const { nacionais, municipais, todos, loading: loadingF, load: loadFeriados, criar: criarFeriado, remover: removerFeriado, isDuplicata } = useFeriados()
// ── Estado bloqueios ────────────────────────────────────────
const loadingB = ref(false)
const loadingB = ref(true)
const saving = ref(false)
const ownerId = ref(null)
@@ -164,13 +181,23 @@ async function salvarFeriado () {
}
}
async function excluirFeriado (id) {
try {
await removerFeriado(id)
toast.add({ severity: 'success', summary: 'Removido', life: 1500 })
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
}
function excluirFeriado (id) {
confirm.require({
message: 'Deseja remover este feriado municipal?',
header: 'Confirmar remoção',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancelar',
acceptLabel: 'Remover',
acceptClass: 'p-button-danger',
accept: async () => {
try {
await removerFeriado(id)
toast.add({ severity: 'success', summary: 'Removido', life: 1500 })
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
}
}
})
}
// ── Bloqueio CRUD ─────────────────────────────────────────────
@@ -236,15 +263,26 @@ async function salvarBloqueio () {
}
}
async function excluirBloqueio (id) {
try {
const { error } = await supabase.from('agenda_bloqueios').delete().eq('id', id)
if (error) throw error
bloqueios.value = bloqueios.value.filter(b => b.id !== id)
toast.add({ severity: 'success', summary: 'Removido', life: 1500 })
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
}
function excluirBloqueio (id) {
const b = bloqueios.value.find(x => x.id === id)
confirm.require({
message: b?.titulo ? `Remover o bloqueio "${b.titulo}"?` : 'Deseja remover este bloqueio?',
header: 'Confirmar remoção',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancelar',
acceptLabel: 'Remover',
acceptClass: 'p-button-danger',
accept: async () => {
try {
const { error } = await supabase.from('agenda_bloqueios').delete().eq('id', id)
if (error) throw error
bloqueios.value = bloqueios.value.filter(b => b.id !== id)
toast.add({ severity: 'success', summary: 'Removido', life: 1500 })
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
}
}
})
}
// ── fmtPeriodo ────────────────────────────────────────────────
@@ -262,10 +300,12 @@ function fmtPeriodo (b) {
}
const loading = computed(() => loadingF.value || loadingB.value)
</script>
<template>
<Toast />
<ConfirmDialog />
<div class="flex flex-col gap-3">
@@ -304,10 +344,50 @@ const loading = computed(() => loadingF.value || loadingB.value)
</div>
</div>
<!-- Loading -->
<div v-if="loading" class="flex items-center justify-center py-16">
<i class="pi pi-spinner pi-spin text-2xl opacity-40" />
</div>
<!-- SKELETON -->
<template v-if="loading">
<!-- Grupo skeleton: Feriados Nacionais frases aqui -->
<div class="blk-group">
<div class="blk-group__head">
<Skeleton shape="circle" size="1.6rem" />
<Skeleton width="9rem" height="0.85rem" />
<Skeleton width="2rem" height="1.2rem" border-radius="999px" />
</div>
<AppLoadingPhrases action="Carregando bloqueios e feriados..." containerClass="py-10" />
</div>
<!-- Grupo skeleton: Feriados Municipais -->
<div class="blk-group">
<div class="blk-group__head">
<Skeleton shape="circle" size="1.6rem" />
<Skeleton width="10rem" height="0.85rem" />
<Skeleton width="2rem" height="1.2rem" border-radius="999px" />
</div>
<div class="blk-list">
<div v-for="i in 3" :key="i" class="blk-item">
<Skeleton width="4rem" height="0.75rem" />
<Skeleton :width="`${8 + (i % 2) * 5}rem`" height="0.75rem" />
</div>
</div>
</div>
<!-- Grupo skeleton: Bloqueios -->
<div class="blk-group">
<div class="blk-group__head">
<Skeleton shape="circle" size="1.6rem" />
<Skeleton width="5rem" height="0.85rem" />
<Skeleton width="2rem" height="1.2rem" border-radius="999px" />
</div>
<div class="blk-list">
<div v-for="i in 4" :key="i" class="blk-item">
<Skeleton width="4rem" height="0.75rem" />
<Skeleton :width="`${7 + (i % 3) * 3}rem`" height="0.75rem" />
</div>
</div>
</div>
</template>
<template v-else>
@@ -380,6 +460,9 @@ const loading = computed(() => loadingF.value || loadingB.value)
</div>
</div>
<!-- Bloco pós-carregamento -->
<LoadedPhraseBlock />
</template>
</div>
@@ -559,4 +642,5 @@ const loading = computed(() => loadingF.value || loadingB.value)
/* ── Dialog labels ──────────────────────────────── */
.blk-label { font-size: 0.75rem; color: var(--text-color-secondary); font-weight: 500; }
</style>
@@ -1,4 +1,19 @@
<!-- src/layout/configuracoes/ConfiguracoesAgendaPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesAgendaPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, ref, watch, onMounted, nextTick } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -70,6 +85,9 @@ const jornadaPorDia = ref({
0: { inicio: '08:00', fim: '18:00' }
})
// Snapshot dos horários por dia do modo "diferente" — preservado ao alternar modos
const jornadaPorDiaSnapshot = ref(null)
// ── Pausas ──────────────────────────────────────────────────────
const pausasGlobais = ref([])
const pausasPorDia = ref({ 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 0: [] })
@@ -224,6 +242,14 @@ function getPausasForDay (dayValue) {
// ── Toggle igual/diferente ─────────────────────────────────────
function switchToIgual () {
// Salva o estado atual de "diferente" no snapshot antes de sair
jornadaPorDiaSnapshot.value = {}
selectedDays.value.forEach(d => {
if (jornadaPorDia.value[d.value])
jornadaPorDiaSnapshot.value[d.value] = { ...jornadaPorDia.value[d.value] }
})
// jornadaStart/jornadaEnd ficam intocados — eram os valores do modo "igual"
if (isValidHHMM(jornadaStart.value) && isValidHHMM(jornadaEnd.value)) {
// Sync apenas SegSex; Sáb e Dom mantêm horário próprio
selectedDays.value
@@ -241,12 +267,18 @@ function switchToIgual () {
}
function switchToDiferente () {
// Inicializa cada dia com o horário global atual e as pausas globais
if (isValidHHMM(jornadaStart.value) && isValidHHMM(jornadaEnd.value)) {
selectedDays.value.forEach(d => {
jornadaPorDia.value[d.value] = { inicio: jornadaStart.value, fim: jornadaEnd.value }
})
}
selectedDays.value.forEach(d => {
const isWeekend = d.value === 6 || d.value === 0
const snap = jornadaPorDiaSnapshot.value?.[d.value]
if (snap && isValidHHMM(snap.inicio) && isValidHHMM(snap.fim)) {
// Restaura do snapshot (vale para todos os dias)
jornadaPorDia.value[d.value] = { ...snap }
} else if (!isWeekend) {
// Dia de semana sem snapshot: inicia com 08:0018:00
jornadaPorDia.value[d.value] = { inicio: '08:00', fim: '18:00' }
}
// Sáb/Dom sem snapshot: mantém o valor atual (já configurado no modo "igual")
})
selectedDays.value.forEach(d => {
pausasPorDia.value[d.value] = pausasGlobais.value.map(p => ({ ...p, id: newId() }))
})
@@ -284,15 +316,29 @@ function hydrateWizardFromRegras (dbRegras) {
workDays.value = map
jornadaPorDia.value = byDay
const first = actives[0]
const allSame = actives.every(r =>
String(r.hora_inicio||'').slice(0,5) === String(first.hora_inicio||'').slice(0,5) &&
String(r.hora_fim ||'').slice(0,5) === String(first.hora_fim ||'').slice(0,5)
)
// Usa apenas dias de semana (15) para determinar allSame e o horário padrão.
// Sáb (6) e Dom (0) têm horários próprios e não devem afetar o modo "igual para todos".
const weekdayActives = actives.filter(r => r.dia_semana >= 1 && r.dia_semana <= 5)
const ref = weekdayActives.length ? weekdayActives[0] : actives[0]
const allSame = weekdayActives.length >= 2
? weekdayActives.every(r =>
String(r.hora_inicio||'').slice(0,5) === String(ref.hora_inicio||'').slice(0,5) &&
String(r.hora_fim ||'').slice(0,5) === String(ref.hora_fim ||'').slice(0,5)
)
: true
jornadaIgualTodos.value = allSame
jornadaStart.value = String(first.hora_inicio||'').slice(0,5) || '08:00'
jornadaEnd.value = String(first.hora_fim ||'').slice(0,5) || '18:00'
jornadaStart.value = String(ref.hora_inicio||'').slice(0,5) || '08:00'
jornadaEnd.value = String(ref.hora_fim ||'').slice(0,5) || '18:00'
// Se carregou em modo "diferente", popula o snapshot para preservar ao alternar modos
if (!allSame) {
jornadaPorDiaSnapshot.value = {}
Object.keys(byDay).forEach(k => {
jornadaPorDiaSnapshot.value[k] = { ...byDay[k] }
})
}
regras.value = actives.map(r => ({
...r,
@@ -482,7 +528,7 @@ async function saveJornada () {
cfg.value.setup_clinica_concluido = true
cfg.value.jornada_igual_todos = igualTodos
toast.add({ severity: 'success', summary: 'Jornada salva', detail: 'Horários de trabalho atualizados.', life: 1800 })
toast.add({ severity: 'success', summary: 'Jornada salva', detail: 'Horários de trabalho atualizados.', life: 3500 })
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao salvar jornada.', life: 3500 })
} finally {
@@ -785,20 +831,76 @@ const jornadaEndDate = computed({
</script>
<template>
<Toast />
<div v-if="loading" class="flex items-center justify-center py-24">
<i class="pi pi-spinner pi-spin text-3xl opacity-40" />
<!-- SKELETON -->
<div v-if="loading" class="flex flex-col xl:flex-row gap-4">
<!-- Coluna esquerda skeleton -->
<div class="flex flex-col gap-3 xl:w-[58%]">
<!-- Subheader skeleton -->
<div class="flex items-center gap-3 px-1 py-2">
<Skeleton shape="circle" size="2rem" />
<div class="flex flex-col gap-1.5">
<Skeleton width="9rem" height="0.85rem" />
<Skeleton width="14rem" height="0.75rem" />
</div>
</div>
<!-- Card skeleton (×3) frases dentro do primeiro -->
<div
v-for="i in 3"
:key="i"
class="rounded-[8px] border border-[var(--surface-border)] bg-[var(--surface-card)] shadow-sm"
>
<div class="flex items-center gap-3 px-4 py-4">
<Skeleton shape="circle" size="2.2rem" />
<div class="flex-1 flex flex-col gap-1.5">
<Skeleton :width="i === 1 ? '11rem' : i === 2 ? '9rem' : '13rem'" height="0.85rem" />
<Skeleton :width="i === 1 ? '18rem' : i === 2 ? '12rem' : '10rem'" height="0.7rem" />
</div>
<Skeleton width="6rem" height="1.4rem" border-radius="999px" />
<Skeleton shape="circle" size="1rem" />
</div>
<AppLoadingPhrases
v-if="i === 1"
action="Carregando configurações da agenda..."
containerClass="py-8"
/>
</div>
</div>
<!-- Coluna direita: skeleton puro -->
<div class="xl:w-[42%] xl:self-start">
<div class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm">
<!-- Header skeleton -->
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border)]">
<Skeleton width="8rem" height="0.85rem" />
<div class="flex gap-1">
<Skeleton v-for="j in 5" :key="j" width="2.2rem" height="1.6rem" border-radius="999px" />
</div>
</div>
<!-- Legenda skeleton -->
<div class="flex gap-3 px-4 py-2 border-b border-[var(--surface-border)]">
<Skeleton v-for="k in 3" :key="k" width="4.5rem" height="0.7rem" />
</div>
<!-- Calendário skeleton -->
<div class="flex flex-col gap-2 p-4">
<Skeleton v-for="l in 10" :key="l" :width="`${70 + (l % 3) * 10}%`" height="2.2rem" />
</div>
</div>
</div>
</div>
<div v-else class="flex flex-col xl:flex-row gap-4">
<Transition name="fade-up" appear>
<div v-if="!loading" class="flex flex-col xl:flex-row gap-4">
<!-- COLUNA ESQUERDA: CARDS -->
<div class="flex flex-col gap-3 xl:w-[58%]">
<div class="anim-child [--delay:0ms] flex flex-col gap-3 xl:w-[58%]">
<!-- Subheader -->
<div class="cfg-subheader">
<i class="pi pi-calendar cfg-subheader__icon" />
<i class="pi pi-calendar w-10 h-10 rounded-md cfg-subheader__icon" />
<div class="min-w-0">
<div class="cfg-subheader__title">Agenda</div>
<div class="cfg-subheader__sub">Horários semanais, duração e intervalo padrão</div>
@@ -810,7 +912,7 @@ const jornadaEndDate = computed({
<!-- Cabeçalho clicável -->
<button class="cfg-card__header" @click="expandedCard = expandedCard === 'jornada' ? null : 'jornada'">
<div class="cfg-card__icon-wrap">
<div class="cfg-card__icon-wrap w-10 h-10 rounded-md">
<i class="pi pi-calendar text-lg" />
</div>
<div class="flex-1 min-w-0 text-left">
@@ -994,7 +1096,7 @@ const jornadaEndDate = computed({
<!-- Pausas -->
<div v-if="selectedDays.length > 0" class="mb-5">
<div class="cfg-label mb-2">Pausas (opcional)</div>
<div class="text-sm text-[var(--text-color-secondary)] mb-3">Ex.: almoço, intervalo fixo.</div>
<div class="text-sm text-[var(--text-color-secondary)] mb-3">Ex.: almoço, jantar, etc.</div>
<div v-if="jornadaIgualTodos !== false">
<PausasChipsEditor v-model="pausasGlobais" />
@@ -1026,7 +1128,7 @@ const jornadaEndDate = computed({
<div class="cfg-card" :class="{ 'cfg-card--open': expandedCard === 'ritmo' }">
<button class="cfg-card__header" @click="expandedCard = expandedCard === 'ritmo' ? null : 'ritmo'">
<div class="cfg-card__icon-wrap">
<div class="cfg-card__icon-wrap w-10 h-10 rounded-md">
<i class="pi pi-stopwatch text-lg" />
</div>
<div class="flex-1 min-w-0 text-left">
@@ -1109,7 +1211,7 @@ const jornadaEndDate = computed({
<div class="cfg-card" :class="{ 'cfg-card--open': expandedCard === 'online' }">
<button class="cfg-card__header" @click="expandedCard = expandedCard === 'online' ? null : 'online'">
<div class="cfg-card__icon-wrap">
<div class="cfg-card__icon-wrap w-10 h-10 rounded-md">
<i class="pi pi-globe text-lg" />
</div>
<div class="flex-1 min-w-0 text-left">
@@ -1247,38 +1349,43 @@ const jornadaEndDate = computed({
</div>
</div>
</div>
<!-- Bloco pós-carregamento -->
<LoadedPhraseBlock />
</div>
<!-- COLUNA DIREITA: PREVIEW -->
<div class="xl:w-[42%] xl:sticky xl:top-4 xl:self-start">
<div class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm">
<div class="anim-child [--delay:120ms] xl:w-[42%] xl:top-4 xl:self-start">
<div class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm agenda-altura">
<!-- Header do preview -->
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border)]">
<div class="font-semibold text-sm">Preview da agenda</div>
<div class="flex gap-1">
<button
v-for="d in selectedDays"
:key="d.value"
class="day-chip day-chip--sm"
:class="(jornadaIgualTodos !== false || previewDay === d.value) ? 'day-chip--active' : ''"
@click="previewDay = d.value"
>
{{ d.short }}
</button>
<span v-if="!selectedDays.length" class="text-xs text-[var(--text-color-secondary)]">
Nenhum dia selecionado
</span>
</div>
</div>
<div class="sticky top-0 z-10 bg-white">
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border)]">
<div class="font-semibold text-sm">Preview da agenda</div>
<div class="flex gap-1">
<button
v-for="d in selectedDays"
:key="d.value"
class="day-chip day-chip--sm"
:class="(jornadaIgualTodos !== false || previewDay === d.value) ? 'day-chip--active' : ''"
@click="previewDay = d.value"
>
{{ d.short }}
</button>
<span v-if="!selectedDays.length" class="text-xs text-[var(--text-color-secondary)]">
Nenhum dia selecionado
</span>
</div>
</div>
<!-- Legenda -->
<div class="flex gap-3 px-4 py-2 border-b border-[var(--surface-border)] text-xs text-[var(--text-color-secondary)]">
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--green-100)] inline-block border border-green-300"></span> Jornada</span>
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--red-200)] inline-block border border-red-300"></span> Pausa</span>
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--primary-color)] inline-block"></span> Sessão</span>
<span v-if="cfg.online_ativo" class="flex items-center gap-1">🌐 Online</span>
</div>
<!-- Legenda -->
<div class="flex gap-3 px-4 py-2 border-b border-[var(--surface-border)] text-xs text-[var(--text-color-secondary)]">
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--green-100)] inline-block border border-green-300"></span> Jornada</span>
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--red-200)] inline-block border border-red-300"></span> Pausa</span>
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--primary-color)] inline-block"></span> Sessão</span>
<span v-if="cfg.online_ativo" class="flex items-center gap-1">🌐 Online</span>
</div>
</div>
<!-- FullCalendar -->
<div v-if="previewDay != null && !loading" class="p-2">
@@ -1291,6 +1398,7 @@ const jornadaEndDate = computed({
</div>
</div>
</Transition>
</template>
<style>
@@ -1338,16 +1446,6 @@ const jornadaEndDate = computed({
}
.cfg-card__header:hover { background: var(--surface-hover); }
.cfg-card__icon-wrap {
width: 2.5rem;
height: 2.5rem;
border-radius: .875rem;
border: 1px solid var(--surface-border);
background: var(--surface-ground);
display: grid;
place-items: center;
flex-shrink: 0;
}
.cfg-card__title {
font-size: .9375rem;
font-weight: 600;
@@ -1517,52 +1615,4 @@ const jornadaEndDate = computed({
.toggle-switch--on .toggle-switch__thumb {
transform: translateX(1.25rem);
}
/* ── Subheader de seção ──────────────────────────────── */
.cfg-subheader {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.875rem 1rem;
border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 30%, transparent);
background: linear-gradient(
135deg,
color-mix(in srgb, var(--primary-color, #6366f1) 12%, var(--surface-card)) 0%,
color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card)) 60%,
var(--surface-card) 100%
);
overflow: hidden;
position: relative;
}
/* Brilho sutil no canto */
.cfg-subheader::before {
content: '';
position: absolute;
top: -20px; right: -20px;
width: 80px; height: 80px;
border-radius: 50%;
background: color-mix(in srgb, var(--primary-color, #6366f1) 15%, transparent);
filter: blur(20px);
pointer-events: none;
}
.cfg-subheader__icon {
display: grid; place-items: center;
width: 2rem; height: 2rem;
border-radius: 6px; flex-shrink: 0;
background: color-mix(in srgb, var(--primary-color, #6366f1) 20%, transparent);
color: var(--primary-color, #6366f1);
font-size: 0.85rem;
}
.cfg-subheader__title {
font-size: 0.95rem;
font-weight: 700;
color: var(--primary-color, #6366f1);
letter-spacing: -0.01em;
}
.cfg-subheader__sub {
font-size: 0.75rem;
color: var(--text-color-secondary);
opacity: 0.85;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,19 @@
<!-- src/layout/configuracoes/ConfiguracoesConveniosPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesConveniosPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -237,7 +252,6 @@ onMounted(async () => {
</script>
<template>
<Toast />
<div class="flex flex-col gap-3">
@@ -253,10 +267,24 @@ onMounted(async () => {
</div>
</div>
<!-- Loading -->
<div v-if="pageLoading || loading" class="flex justify-center py-10">
<ProgressSpinner style="width:40px;height:40px" />
</div>
<!-- SKELETON -->
<template v-if="pageLoading || loading">
<div class="cfg-wrap">
<div class="cfg-wrap__head">
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
<Skeleton width="9rem" height="12px" />
</div>
<div v-for="n in 3" :key="n" class="flex items-center gap-3 px-4 py-3 border-b border-[var(--surface-border)] last:border-b-0">
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
<div class="flex flex-col gap-2 flex-1">
<Skeleton :width="n === 1 ? '12rem' : n === 2 ? '9rem' : '11rem'" height="11px" />
<Skeleton width="5rem" height="10px" />
</div>
<Skeleton width="3rem" height="1.4rem" border-radius="999px" />
</div>
</div>
<AppLoadingPhrases action="Carregando convênios..." containerClass="py-6" />
</template>
<template v-else>
@@ -458,6 +486,8 @@ onMounted(async () => {
</span>
</Message>
<LoadedPhraseBlock />
</template>
</div>
</template>
@@ -1,4 +1,19 @@
<!-- src/layout/configuracoes/ConfiguracoesDescontosPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesDescontosPage.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'
@@ -182,7 +197,6 @@ onMounted(async () => {
</script>
<template>
<Toast />
<div class="flex flex-col gap-3">
@@ -198,10 +212,23 @@ onMounted(async () => {
</div>
</div>
<!-- Loading -->
<div v-if="pageLoading || loading" class="flex justify-center py-10">
<ProgressSpinner style="width:40px;height:40px" />
</div>
<!-- SKELETON -->
<template v-if="pageLoading || loading">
<div class="cfg-wrap">
<div class="cfg-wrap__head">
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
<Skeleton width="11rem" height="12px" />
</div>
<div v-for="n in 3" :key="n" class="flex items-center gap-3 px-4 py-3 border-b border-[var(--surface-border)] last:border-b-0">
<div class="flex flex-col gap-2 flex-1">
<Skeleton :width="n % 2 === 0 ? '14rem' : '10rem'" height="11px" />
<Skeleton width="8rem" height="10px" />
</div>
<Skeleton width="4rem" height="1.4rem" border-radius="999px" />
</div>
</div>
<AppLoadingPhrases action="Carregando descontos por paciente..." containerClass="py-6" />
</template>
<template v-else>
@@ -352,6 +379,8 @@ onMounted(async () => {
</span>
</Message>
<LoadedPhraseBlock />
</template>
</div>
</template>
@@ -1,4 +1,19 @@
<!-- src/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
@@ -349,9 +364,23 @@ onMounted(async () => {
/>
</div>
<div v-if="loading" class="flex justify-center py-10">
<ProgressSpinner />
</div>
<!-- SKELETON -->
<template v-if="loading">
<div class="flex flex-col gap-2">
<div v-for="n in 5" :key="n" class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] px-4 py-3 flex gap-3 items-center">
<Skeleton width="4.5rem" height="1.4rem" border-radius="999px" />
<div class="flex flex-col gap-1.5 flex-1">
<Skeleton :width="n % 2 === 0 ? '14rem' : '10rem'" height="11px" />
<Skeleton :width="n % 3 === 0 ? '20rem' : '16rem'" height="10px" />
</div>
<div class="flex gap-1">
<Skeleton width="2rem" height="2rem" border-radius="6px" />
<Skeleton width="2rem" height="2rem" border-radius="6px" />
</div>
</div>
</div>
<AppLoadingPhrases action="Carregando templates de e-mail..." containerClass="py-6" />
</template>
<!-- Lista -->
<div v-else class="flex flex-col gap-2">
@@ -392,6 +421,7 @@ onMounted(async () => {
/>
</div>
</div>
<LoadedPhraseBlock />
</div>
<!-- Dialog Layout Global (Header/Footer) -->
@@ -1,4 +1,19 @@
<!-- src/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.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'
@@ -143,7 +158,6 @@ onMounted(async () => {
</script>
<template>
<Toast />
<div class="flex flex-col gap-3">
@@ -156,10 +170,20 @@ onMounted(async () => {
</div>
</div>
<!-- Loading -->
<div v-if="pageLoading || loading" class="flex justify-center py-10">
<ProgressSpinner style="width:40px;height:40px" />
</div>
<!-- SKELETON -->
<template v-if="pageLoading || loading">
<div v-for="n in 3" :key="n" class="cfg-wrap">
<div class="cfg-wrap__head">
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
<Skeleton :width="n === 1 ? '13rem' : n === 2 ? '11rem' : '15rem'" height="12px" />
<Skeleton width="4rem" height="1.4rem" border-radius="999px" class="ml-auto" />
</div>
<div class="px-4 py-3">
<Skeleton width="16rem" height="10px" />
</div>
</div>
<AppLoadingPhrases action="Carregando exceções financeiras..." containerClass="py-6" />
</template>
<template v-else>
@@ -266,6 +290,8 @@ onMounted(async () => {
</span>
</Message>
<LoadedPhraseBlock />
</template>
</div>
</template>
@@ -1,10 +1,27 @@
<!-- src/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.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'
import { useConfirm } from 'primevue/useconfirm'
import { supabase } from '@/lib/supabase/client'
const toast = useToast()
const toast = useToast()
const confirm = useConfirm()
// ── Constantes ────────────────────────────────────────────────
const AVATAR_BUCKET = 'avatars'
@@ -49,6 +66,7 @@ const ESTADOS = [
// ── Estado ────────────────────────────────────────────────────
const tenantId = ref(null)
const recordId = ref(null)
const loading = ref(true)
const saving = ref(false)
const loadingCep = ref(false)
@@ -165,24 +183,41 @@ async function buscarCep() {
// ── Redes sociais ─────────────────────────────────────────────
function addRede() { form.value.redes_sociais.push({ rede: null, url: '' }) }
function removeRede(i) { form.value.redes_sociais.splice(i, 1) }
function removeRede(i) {
const rede = form.value.redes_sociais[i]
const label = REDES_OPTIONS.find(o => o.value === rede?.rede)?.label || rede?.rede || 'esta rede social'
confirm.require({
message: `Deseja remover ${label}?`,
header: 'Remover rede social',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancelar',
acceptLabel: 'Remover',
acceptClass: 'p-button-danger',
accept: () => form.value.redes_sociais.splice(i, 1),
})
}
// ── Load / Save ───────────────────────────────────────────────
async function load() {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
tenantId.value = user.id
const { data } = await supabase
.from('company_profiles')
.select('*')
.eq('tenant_id', user.id)
.maybeSingle()
if (data) {
recordId.value = data.id
Object.keys(form.value).forEach(k => {
if (data[k] !== undefined && data[k] !== null) form.value[k] = data[k]
})
if (data.logo_url) logoPreview.value = data.logo_url
loading.value = true
try {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
tenantId.value = user.id
const { data } = await supabase
.from('company_profiles')
.select('*')
.eq('tenant_id', user.id)
.maybeSingle()
if (data) {
recordId.value = data.id
Object.keys(form.value).forEach(k => {
if (data[k] !== undefined && data[k] !== null) form.value[k] = data[k]
})
if (data.logo_url) logoPreview.value = data.logo_url
}
} finally {
loading.value = false
}
}
@@ -267,6 +302,7 @@ onMounted(load)
</script>
<template>
<ConfirmDialog />
<div class="flex flex-col gap-4">
<!-- Subheader -->
@@ -282,7 +318,35 @@ onMounted(load)
</div>
</div>
<!-- SKELETON -->
<template v-if="loading">
<div class="flex gap-4 items-start">
<!-- Esqueleto formulário -->
<div class="form-col flex flex-col gap-4">
<div v-for="n in 3" :key="n" class="cfg-card">
<div class="cfg-card__head">
<Skeleton width="1rem" height="1rem" border-radius="4px" />
<Skeleton :width="n === 1 ? '7rem' : n === 2 ? '9rem' : '8rem'" height="12px" />
</div>
<div class="flex flex-col gap-3 p-4">
<div class="grid grid-cols-2 gap-3">
<Skeleton height="2.5rem" border-radius="6px" />
<Skeleton height="2.5rem" border-radius="6px" />
</div>
<Skeleton height="2.5rem" border-radius="6px" />
</div>
</div>
</div>
<!-- Esqueleto preview -->
<div class="preview-col">
<Skeleton height="320px" border-radius="12px" />
</div>
</div>
<AppLoadingPhrases action="Carregando dados da empresa..." containerClass="py-6" />
</template>
<!-- Corpo: formulário + preview -->
<template v-else>
<div class="flex gap-4 items-start">
<!-- Formulário (60%) -->
@@ -432,6 +496,8 @@ onMounted(load)
<Button label="Salvar dados da empresa" icon="pi pi-check" :loading="saving" @click="save" />
</div>
<LoadedPhraseBlock />
</div>
<!-- Preview (40%) -->
@@ -533,6 +599,8 @@ onMounted(load)
</div>
</template>
</div>
</template>
@@ -1,4 +1,19 @@
<!-- src/layout/configuracoes/ConfiguracoesPagamentoPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesPagamentoPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted } from 'vue'
import { supabase } from '@/lib/supabase/client'
@@ -177,11 +192,32 @@ onMounted(load)
</script>
<template>
<Toast />
<div v-if="loading" class="flex items-center gap-2 p-6 text-[var(--text-color-secondary)]">
<i class="pi pi-spin pi-spinner" /> Carregando
</div>
<!-- SKELETON -->
<template v-if="loading">
<div class="flex flex-col gap-4">
<!-- Skeleton subheader -->
<div class="cfg-subheader">
<Skeleton width="2rem" height="2rem" border-radius="6px" />
<div class="flex flex-col gap-1.5 flex-1">
<Skeleton width="7rem" height="13px" />
<Skeleton width="18rem" height="11px" />
</div>
</div>
<!-- Skeleton cards de pagamento -->
<div v-for="n in 4" :key="n" class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
<div class="flex items-center gap-3 p-4">
<Skeleton width="40px" height="40px" border-radius="6px" />
<div class="flex flex-col gap-1.5 flex-1">
<Skeleton :width="n % 2 === 0 ? '8rem' : '10rem'" height="12px" />
<Skeleton :width="n % 3 === 0 ? '16rem' : '12rem'" height="10px" />
</div>
<Skeleton width="3rem" height="1.4rem" border-radius="999px" />
</div>
</div>
<AppLoadingPhrases action="Carregando formas de pagamento..." containerClass="py-6" />
</div>
</template>
<div v-else class="flex flex-col gap-4">
@@ -550,6 +586,8 @@ onMounted(load)
</div>
</div>
<LoadedPhraseBlock />
</div>
</template>
@@ -1,4 +1,19 @@
<!-- src/layout/configuracoes/ConfiguracoesPrecificacaoPage.vue -->
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesPrecificacaoPage.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'
@@ -146,7 +161,6 @@ onMounted(async () => {
</script>
<template>
<Toast />
<div class="flex flex-col gap-3">
@@ -162,10 +176,24 @@ onMounted(async () => {
</div>
</div>
<!-- Loading -->
<div v-if="pageLoading || loading" class="flex justify-center py-10">
<ProgressSpinner style="width:40px;height:40px" />
</div>
<!-- SKELETON -->
<template v-if="pageLoading || loading">
<div class="cfg-wrap">
<div class="cfg-wrap__head">
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
<Skeleton width="9rem" height="12px" />
</div>
<div v-for="n in 3" :key="n" class="flex items-center gap-3 px-4 py-3 border-b border-[var(--surface-border)] last:border-b-0">
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
<div class="flex flex-col gap-2 flex-1">
<Skeleton :width="n === 1 ? '10rem' : n === 2 ? '8rem' : '12rem'" height="11px" />
<Skeleton width="6rem" height="10px" />
</div>
<Skeleton width="3.5rem" height="1.4rem" border-radius="999px" />
</div>
</div>
<AppLoadingPhrases action="Carregando serviços e precificação..." containerClass="py-6" />
</template>
<template v-else>
@@ -298,6 +326,8 @@ onMounted(async () => {
<span class="text-sm">Os serviços cadastrados aqui aparecem para seleção ao criar ou editar um evento na agenda.</span>
</Message>
<LoadedPhraseBlock />
</template>
</div>
</template>