Copyright, Financeiro, Lançamentos, aprimoramentos de ui
This commit is contained in:
@@ -1,3 +1,19 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 — já 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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 ───────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 Seg–Sex; 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:00–18: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 (1–5) 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>
|
||||
|
||||
Reference in New Issue
Block a user