Layout 100%, Notificações, SetupWizard

This commit is contained in:
Leonardo
2026-03-17 21:08:14 -03:00
parent 84d65e49c0
commit 66f67cd40f
77 changed files with 35823 additions and 15023 deletions

View File

@@ -302,236 +302,261 @@ onBeforeUnmount(() => { _observer?.disconnect() })
<Toast />
<!-- Sentinel -->
<div ref="headerSentinelRef" class="mplan-sentinel" />
<div ref="headerSentinelRef" class="h-px" />
<!-- Hero sticky -->
<div ref="headerEl" class="mplan-hero mx-3 md:mx-5 mb-4" :class="{ 'mplan-hero--stuck': headerStuck }">
<div class="mplan-hero__blobs" aria-hidden="true">
<div class="mplan-hero__blob mplan-hero__blob--1" />
<div class="mplan-hero__blob mplan-hero__blob--2" />
<!--
HERO sticky
-->
<section
ref="headerEl"
class="sticky mx-3 md:mx-4 mb-3 z-20 overflow-hidden rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] px-3 py-2.5 transition-[border-radius] duration-200"
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
>
<!-- Blobs -->
<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-indigo-500/10" />
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-emerald-400/[0.08]" />
</div>
<!-- Row 1 -->
<div class="mplan-hero__row1">
<div class="mplan-hero__brand">
<div class="mplan-hero__icon"><i class="pi pi-credit-card text-lg" /></div>
<div class="min-w-0">
<div class="relative z-[1] flex items-center gap-3">
<!-- Brand -->
<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/10 text-indigo-500">
<i class="pi pi-credit-card text-base" />
</div>
<div class="min-w-0 hidden sm:block">
<div class="flex items-center gap-2 flex-wrap">
<div class="mplan-hero__title">Meu Plano</div>
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Meu Plano</div>
<Tag v-if="subscription" :value="statusLabel(subscription.status)" :severity="statusSeverity(subscription.status)" />
</div>
<div class="mplan-hero__sub">Plano pessoal do terapeuta gerencie sua assinatura</div>
<div class="text-[1rem] text-[var(--text-color-secondary)]">Plano pessoal do terapeuta gerencie sua assinatura</div>
</div>
</div>
<!-- Desktop (1200px) -->
<div class="hidden xl:flex items-center gap-2 shrink-0">
<!-- Ações desktop ( xl) -->
<div class="hidden xl:flex items-center gap-1 flex-shrink-0 ml-auto">
<Button icon="pi pi-refresh" severity="secondary" outlined class="h-9 w-9 rounded-full" :loading="loading" title="Atualizar" @click="fetchMeuPlanoTherapist" />
<Button label="Alterar plano" icon="pi pi-arrow-up-right" class="rounded-full" @click="goUpgrade" />
</div>
<!-- Mobile (<1200px) -->
<div class="flex xl:hidden items-center gap-2 shrink-0">
<!-- Ações mobile (< xl) -->
<div class="flex xl:hidden items-center gap-1 flex-shrink-0 ml-auto">
<Button icon="pi pi-refresh" severity="secondary" outlined class="h-9 w-9 rounded-full" :loading="loading" @click="fetchMeuPlanoTherapist" />
<Button label="Alterar plano" icon="pi pi-arrow-up-right" size="small" class="rounded-full" @click="goUpgrade" />
</div>
</div>
</section>
<!-- Divider -->
<Divider class="mplan-hero__divider my-2" />
<!-- Row 2: resumo rápido (oculto no mobile) -->
<div class="mplan-hero__row2">
<div v-if="loading" class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)]">
<i class="pi pi-spin pi-spinner text-xs" /> Carregando
</div>
<template v-else-if="subscription">
<div class="flex flex-wrap items-center gap-3">
<span class="font-semibold text-sm text-[var(--text-color)]">{{ planName }}</span>
<span v-if="priceLabel" class="text-sm text-[var(--text-color-secondary)]">{{ priceLabel }}</span>
<span class="text-xs text-[var(--text-color-secondary)] border border-[var(--surface-border)] rounded-full px-3 py-1">
Período: {{ periodLabel }}
</span>
<Tag v-if="subscription.cancel_at_period_end" severity="warning" value="Cancelamento agendado" />
<Tag v-else severity="success" value="Renovação automática" />
</div>
</template>
<div v-else class="text-sm text-[var(--text-color-secondary)]">Nenhuma assinatura ativa.</div>
<!--
QUICK-STATS
-->
<div class="flex flex-wrap gap-2 px-3 md:px-4 mb-3">
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[80px] border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)]">
<div class="text-[1.1rem] font-bold leading-none text-[var(--text-color)] truncate">{{ planName }}</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75">Plano atual</div>
</div>
<div
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[80px] transition-colors duration-150"
:class="subscription?.status === 'active' ? 'border-green-500/25 bg-green-500/5' : 'border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)]'"
>
<div
class="text-[1.1rem] font-bold leading-none truncate"
:class="subscription?.status === 'active' ? 'text-green-500' : 'text-[var(--text-color)]'"
>{{ subscription ? statusLabel(subscription.status) : '—' }}</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75">Status</div>
</div>
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[80px] border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)]">
<div class="text-[1.35rem] font-bold leading-none text-[var(--text-color)]">{{ features.length }}</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75">Recursos</div>
</div>
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[80px] border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)]">
<div class="text-[1.35rem] font-bold leading-none text-[var(--text-color)]">{{ events.length }}</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75">Eventos</div>
</div>
</div>
<!-- Conteúdo -->
<div class="px-3 md:px-5 mb-5 flex flex-col gap-4">
<!--
CONTEÚDO
-->
<div class="px-3 md:px-4 pb-8 flex flex-col gap-3">
<!-- Loading skeleton -->
<div v-if="loading" class="flex flex-col gap-3">
<div
v-for="n in 3"
:key="n"
class="flex items-center gap-4 p-4 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)]"
>
<div class="w-10 h-10 rounded-full flex-shrink-0 bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
<div class="flex flex-col gap-2 flex-1">
<div class="h-3.5 w-3/5 rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
<div class="h-2.5 w-2/5 rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
</div>
</div>
</div>
<!-- Sem assinatura -->
<div v-if="!loading && !subscription" class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-10 text-center">
<div class="mx-auto mb-3 grid h-12 w-12 place-items-center rounded-2xl bg-[var(--primary-color)]/10 text-[var(--primary-color)]">
<i class="pi pi-credit-card text-xl" />
<div
v-else-if="!subscription"
class="flex flex-col items-center justify-center gap-4 rounded-md border-2 border-dashed border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] py-16 px-6 text-center"
>
<div class="relative">
<div class="grid place-items-center w-16 h-16 rounded-2xl bg-[var(--surface-card,#fff)] border border-[var(--surface-border,#e2e8f0)] shadow-sm text-[var(--text-color-secondary)]">
<i class="pi pi-credit-card text-3xl opacity-30" />
</div>
<div class="absolute -top-2 -right-2 w-7 h-7 rounded-full bg-[var(--surface-card,#fff)] border border-[var(--surface-border,#e2e8f0)] shadow-sm grid place-items-center">
<i class="pi pi-times text-[0.65rem] text-[var(--text-color-secondary)] opacity-50" />
</div>
</div>
<div class="font-semibold">Nenhuma assinatura encontrada</div>
<div class="mt-1 text-sm text-[var(--text-color-secondary)]">Escolha um plano para começar a usar todos os recursos.</div>
<div class="mt-4">
<Button label="Ver planos" icon="pi pi-arrow-up-right" class="rounded-full" @click="goUpgrade" />
<div>
<div class="font-bold text-[1rem] text-[var(--text-color)] mb-1">Nenhuma assinatura encontrada</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] max-w-xs leading-relaxed">Escolha um plano para começar a usar todos os recursos.</div>
</div>
<Button label="Ver planos" icon="pi pi-arrow-up-right" class="rounded-full mt-1" @click="goUpgrade" />
</div>
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<template v-else>
<!-- Seu plano inclui: features compactas -->
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
<div class="px-5 py-4 border-b border-[var(--surface-border)] flex items-center justify-between gap-3">
<div>
<div class="font-semibold text-[var(--text-color)]">Seu plano inclui</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-0.5">Recursos disponíveis na sua assinatura atual</div>
<!-- Assinatura atual -->
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border,#e2e8f0)]">
<div class="flex items-center gap-2">
<i class="pi pi-credit-card text-[var(--text-color-secondary)] opacity-60" />
<span class="font-semibold text-[1rem]">Assinatura atual</span>
</div>
<div class="flex items-center gap-2">
<Tag :value="statusLabel(subscription.status)" :severity="statusSeverity(subscription.status)" />
<Tag v-if="subscription.cancel_at_period_end" severity="warning" value="Cancelamento agendado" />
<Tag v-else severity="success" value="Renovação automática" />
</div>
<Tag v-if="features.length" :value="`${features.length}`" severity="secondary" />
</div>
<div class="px-4 py-3 flex flex-wrap gap-x-8 gap-y-3">
<div class="flex flex-col gap-0.5">
<span class="text-[0.68rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-60">Plano</span>
<span class="text-[0.9rem] font-semibold text-[var(--text-color)]">{{ planName }}</span>
</div>
<div class="flex flex-col gap-0.5">
<span class="text-[0.68rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-60">Valor</span>
<span class="text-[1rem] text-[var(--text-color)]">{{ priceLabel || 'Preço não encontrado para este intervalo.' }}</span>
</div>
<div class="flex flex-col gap-0.5">
<span class="text-[0.68rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-60">Período</span>
<span class="text-[1rem] text-[var(--text-color)]">{{ periodLabel }}</span>
</div>
<div v-if="plan?.description" class="flex flex-col gap-0.5 w-full">
<span class="text-[0.68rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-60">Descrição</span>
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ plan.description }}</span>
</div>
<div class="flex flex-col gap-0.5">
<span class="text-[0.68rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-60">ID da assinatura</span>
<code class="text-[0.75rem] text-[var(--text-color-secondary)] break-all font-mono select-all">{{ subscription.id }}</code>
</div>
</div>
</div>
<div class="p-5">
<div v-if="!subscription" class="text-sm text-[var(--text-color-secondary)]">Sem assinatura.</div>
<div v-else-if="!features.length" class="text-sm text-[var(--text-color-secondary)]">Nenhuma feature vinculada a este plano.</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div v-else class="space-y-5">
<div v-for="g in groupedFeatures" :key="g.module">
<!-- Cabeçalho do módulo -->
<div class="flex items-center gap-2 mb-2">
<span class="text-xs font-bold uppercase tracking-wider text-[var(--text-color-secondary)] opacity-50">
{{ moduleLabel(g.module) }}
</span>
<div class="flex-1 h-px bg-[var(--surface-border)]" />
<span class="text-xs text-[var(--text-color-secondary)]">{{ g.items.length }}</span>
</div>
<!-- Seu plano inclui -->
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border,#e2e8f0)]">
<div class="flex items-center gap-2">
<i class="pi pi-check-circle text-[var(--text-color-secondary)] opacity-60" />
<span class="font-semibold text-[1rem]">Seu plano inclui</span>
</div>
<span
v-if="features.length"
class="inline-flex items-center justify-center min-w-[22px] h-[22px] px-1.5 rounded-full bg-[var(--primary-color,#6366f1)] text-white text-[1rem] font-bold"
>{{ features.length }}</span>
</div>
<!-- Grid compacto de features -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
<div
v-for="f in g.items"
:key="f.key"
class="flex items-start gap-2 py-1 px-2 rounded-lg hover:bg-[var(--surface-ground)] transition-colors"
:title="f.description || f.key"
>
<i class="pi pi-check-circle text-emerald-500 text-sm mt-0.5 shrink-0" />
<div class="min-w-0">
<div class="text-sm font-medium truncate text-[var(--text-color)]">{{ f.key }}</div>
<div v-if="f.description" class="text-xs text-[var(--text-color-secondary)] leading-snug truncate">{{ f.description }}</div>
<div class="p-4">
<div v-if="!features.length" class="text-[1rem] text-[var(--text-color-secondary)]">Nenhuma feature vinculada a este plano.</div>
<div v-else class="flex flex-col gap-5">
<div v-for="g in groupedFeatures" :key="g.module">
<div class="flex items-center gap-2 mb-2">
<span class="text-[0.68rem] font-bold uppercase tracking-[0.07em] text-[var(--text-color-secondary)] opacity-50">{{ moduleLabel(g.module) }}</span>
<div class="flex-1 h-px bg-[var(--surface-border,#e2e8f0)]" />
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ g.items.length }}</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
<div
v-for="f in g.items"
:key="f.key"
class="flex items-start gap-2 py-1 px-2 rounded-md hover:bg-[var(--surface-ground,#f8fafc)] transition-colors"
:title="f.description || f.key"
>
<i class="pi pi-check-circle text-emerald-500 text-[1rem] mt-0.5 flex-shrink-0" />
<div class="min-w-0">
<div class="text-[1rem] font-medium truncate text-[var(--text-color)]">{{ f.key }}</div>
<div v-if="f.description" class="text-[1rem] text-[var(--text-color-secondary)] leading-snug truncate">{{ f.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Histórico -->
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
<div class="px-5 py-4 border-b border-[var(--surface-border)] flex items-center justify-between gap-3">
<div>
<div class="font-semibold text-[var(--text-color)]">Histórico</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-0.5">Últimos 50 eventos da assinatura</div>
</div>
<Tag v-if="events.length" :value="`${events.length}`" severity="secondary" />
</div>
<div class="p-5">
<div v-if="!subscription" class="text-sm text-[var(--text-color-secondary)]">Sem histórico (não assinatura).</div>
<div v-else-if="!events.length" class="py-8 text-center">
<i class="pi pi-history text-2xl text-[var(--text-color-secondary)] opacity-30 mb-2 block" />
<div class="text-sm text-[var(--text-color-secondary)]">Sem eventos registrados.</div>
<!-- Histórico -->
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border,#e2e8f0)]">
<div class="flex items-center gap-2">
<i class="pi pi-history text-[var(--text-color-secondary)] opacity-60" />
<span class="font-semibold text-[1rem]">Histórico</span>
</div>
<span
v-if="events.length"
class="inline-flex items-center justify-center min-w-[22px] h-[22px] px-1.5 rounded-full bg-[var(--primary-color,#6366f1)] text-white text-[1rem] font-bold"
>{{ events.length }}</span>
</div>
<div v-else class="space-y-2">
<div
v-for="ev in events"
:key="ev.id"
class="rounded-xl border border-[var(--surface-border)] p-3 hover:bg-[var(--surface-ground)] transition-colors"
>
<div class="flex items-start justify-between gap-3 flex-wrap">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 flex-wrap">
<Tag :value="eventLabel(ev.event_type)" :severity="eventSeverity(ev.event_type)" />
<span class="text-xs text-[var(--text-color-secondary)]">
por <b>{{ displayUser(ev.created_by) }}</b>
</span>
</div>
<div class="p-4">
<div v-if="!events.length" class="py-8 text-center">
<i class="pi pi-history text-2xl text-[var(--text-color-secondary)] opacity-20 mb-2 block" />
<div class="text-[1rem] text-[var(--text-color-secondary)]">Sem eventos registrados.</div>
</div>
<div v-if="ev.old_plan_id || ev.new_plan_id" class="mt-1.5 text-xs text-[var(--text-color-secondary)] flex items-center gap-1.5 flex-wrap">
<span class="font-medium text-[var(--text-color)]">{{ planKeyOrName(ev.old_plan_id) }}</span>
<i class="pi pi-arrow-right text-xs opacity-50" />
<span class="font-medium text-[var(--text-color)]">{{ planKeyOrName(ev.new_plan_id) }}</span>
</div>
<div v-if="ev.reason" class="mt-1 text-xs text-[var(--text-color-secondary)] opacity-70">{{ ev.reason }}</div>
<div v-if="ev.metadata" class="mt-1.5">
<pre class="m-0 text-xs text-[var(--text-color-secondary)] whitespace-pre-wrap break-words opacity-60">{{ prettyMeta(ev.metadata) }}</pre>
<div v-else class="flex flex-col gap-2">
<div
v-for="ev in events"
:key="ev.id"
class="rounded-md border border-[var(--surface-border,#e2e8f0)] p-3 hover:bg-[var(--surface-ground,#f8fafc)] transition-colors duration-100"
>
<div class="flex items-start justify-between gap-3 flex-wrap">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 flex-wrap">
<Tag :value="eventLabel(ev.event_type)" :severity="eventSeverity(ev.event_type)" />
<span class="text-[1rem] text-[var(--text-color-secondary)]">
por <b>{{ displayUser(ev.created_by) }}</b>
</span>
</div>
<div v-if="ev.old_plan_id || ev.new_plan_id" class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)] flex items-center gap-1.5 flex-wrap">
<span class="font-medium text-[var(--text-color)]">{{ planKeyOrName(ev.old_plan_id) }}</span>
<i class="pi pi-arrow-right text-[1rem] opacity-50" />
<span class="font-medium text-[var(--text-color)]">{{ planKeyOrName(ev.new_plan_id) }}</span>
</div>
<div v-if="ev.reason" class="mt-1 text-[1rem] text-[var(--text-color-secondary)] opacity-70">{{ ev.reason }}</div>
<div v-if="ev.metadata" class="mt-1.5">
<pre class="m-0 text-[1rem] text-[var(--text-color-secondary)] whitespace-pre-wrap break-words opacity-60">{{ prettyMeta(ev.metadata) }}</pre>
</div>
</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] flex-shrink-0">{{ fmtDate(ev.created_at) }}</div>
</div>
<div class="text-xs text-[var(--text-color-secondary)] shrink-0">{{ fmtDate(ev.created_at) }}</div>
</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-60 mt-1">
Mostrando até 50 eventos (mais recentes).
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Rodapé: subscription ID -->
<div v-if="subscription" class="text-xs text-[var(--text-color-secondary)] flex items-center gap-2 flex-wrap">
<span>ID da assinatura:</span>
<code class="font-mono select-all">{{ subscription.id }}</code>
</div>
</template>
</div>
</template>
<style scoped>
.mplan-sentinel { height: 1px; }
.mplan-hero {
position: sticky;
top: var(--layout-sticky-top, 56px);
z-index: 20;
overflow: hidden;
border-radius: 1.75rem;
border: 1px solid var(--surface-border);
background: var(--surface-card);
padding: 1.25rem 1.5rem;
}
.mplan-hero--stuck {
margin-left: 0; margin-right: 0;
border-top-left-radius: 0; border-top-right-radius: 0;
}
.mplan-hero__blobs { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.mplan-hero__blob { position: absolute; border-radius: 50%; filter: blur(70px); }
.mplan-hero__blob--1 { width: 18rem; height: 18rem; top: -4rem; right: -3rem; background: rgba(99,102,241,0.10); }
.mplan-hero__blob--2 { width: 20rem; height: 20rem; top: 0.5rem; left: -5rem; background: rgba(16,185,129,0.08); }
.mplan-hero__row1 {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 1rem;
}
.mplan-hero__brand {
display: flex; align-items: center; gap: 0.75rem;
flex: 1; min-width: 0;
}
.mplan-hero__icon {
display: grid; place-items: center;
width: 2.5rem; height: 2.5rem; border-radius: 0.875rem; flex-shrink: 0;
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
color: var(--p-primary-500, #6366f1);
}
.mplan-hero__title { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.02em; color: var(--text-color); }
.mplan-hero__sub { font-size: 0.78rem; color: var(--text-color-secondary); margin-top: 2px; }
.mplan-hero__row2 {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
}
@media (max-width: 767px) {
.mplan-hero__divider,
.mplan-hero__row2 { display: none; }
}
/* (intencionalmente vazio) */
</style>