Layout 100%, Notificações, SetupWizard
This commit is contained in:
@@ -422,215 +422,272 @@ onMounted(fetchMeuPlanoClinic)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 md:p-6">
|
||||
<Toast />
|
||||
<Toast />
|
||||
|
||||
<!-- Topbar padrão -->
|
||||
<div class="mb-4 rounded-3xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-4 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-2xl font-semibold leading-none">Meu plano</div>
|
||||
<small class="text-color-secondary mt-1">
|
||||
Plano da clínica (tenant) e recursos habilitados.
|
||||
</small>
|
||||
<!-- Sentinel -->
|
||||
<div class="h-px" />
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
HERO sticky
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<section
|
||||
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"
|
||||
: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>
|
||||
|
||||
<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="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Meu Plano</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Plano da clínica (tenant) e recursos habilitados</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-wrap justify-end">
|
||||
<Button
|
||||
label="Alterar plano"
|
||||
icon="pi pi-arrow-up-right"
|
||||
:loading="loading"
|
||||
@click="goUpgradeClinic"
|
||||
/>
|
||||
<Button
|
||||
label="Atualizar"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="loading"
|
||||
@click="fetchMeuPlanoClinic"
|
||||
/>
|
||||
<!-- Ações desktop -->
|
||||
<div class="hidden sm: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 flex-shrink-0" :loading="loading" title="Atualizar" @click="fetchMeuPlanoClinic" />
|
||||
<Button label="Alterar plano" icon="pi pi-arrow-up-right" class="rounded-full" @click="goUpgradeClinic" />
|
||||
</div>
|
||||
|
||||
<!-- Ações mobile -->
|
||||
<div class="flex sm: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 flex-shrink-0" :loading="loading" @click="fetchMeuPlanoClinic" />
|
||||
<Button label="Upgrade" icon="pi pi-arrow-up-right" size="small" class="rounded-full" @click="goUpgradeClinic" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
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)]'"
|
||||
>{{ statusLabelPrettyComputed }}</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-4 pb-8">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Card resumo -->
|
||||
<Card class="rounded-[2rem] overflow-hidden">
|
||||
<template #content>
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xl md:text-2xl font-semibold leading-tight">
|
||||
{{ planName }}
|
||||
</div>
|
||||
<!-- Empty state: sem assinatura -->
|
||||
<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>
|
||||
<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">Nenhuma assinatura foi encontrada para este tenant.</div>
|
||||
</div>
|
||||
<Button label="Ver planos" icon="pi pi-arrow-up-right" class="rounded-full mt-1" @click="goUpgradeClinic" />
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-color-secondary mt-1">
|
||||
<span v-if="priceLabel">{{ priceLabel }}</span>
|
||||
<span v-else>Preço não encontrado para este intervalo.</span>
|
||||
</div>
|
||||
<!-- Conteúdo com assinatura -->
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<Tag :value="statusLabelPrettyComputed" :severity="statusSeverity(subscription?.status)" />
|
||||
<Tag
|
||||
v-if="subscription?.cancel_at_period_end"
|
||||
severity="warning"
|
||||
value="Cancelamento agendado"
|
||||
rounded
|
||||
/>
|
||||
<Tag
|
||||
v-else-if="subscription"
|
||||
severity="success"
|
||||
value="Renovação automática"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-sm text-color-secondary">
|
||||
<b>Período:</b> {{ periodLabel }}
|
||||
</div>
|
||||
|
||||
<div v-if="cancelHint" class="mt-2 text-sm text-color-secondary">
|
||||
{{ cancelHint }}
|
||||
</div>
|
||||
|
||||
<div v-if="plan?.description" class="mt-3 text-sm opacity-80 max-w-3xl">
|
||||
{{ plan.description }}
|
||||
</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 v-if="subscription" class="flex flex-col items-end gap-2">
|
||||
<small class="text-color-secondary">subscription_id</small>
|
||||
<code class="text-xs opacity-80 break-all">
|
||||
{{ subscription.id }}
|
||||
</code>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag :value="statusLabelPrettyComputed" :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>
|
||||
</div>
|
||||
|
||||
<div v-if="!subscription" class="mt-4 rounded-2xl border border-[var(--surface-border)] p-4 text-sm text-color-secondary">
|
||||
Nenhuma assinatura encontrada para este tenant.
|
||||
<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="cancelHint" 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">Atenção</span>
|
||||
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ cancelHint }}</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>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Divider class="my-6" />
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- ✅ Features agrupadas -->
|
||||
<Card class="rounded-[2rem] overflow-hidden">
|
||||
<template #title>Seu plano inclui</template>
|
||||
<template #content>
|
||||
<div v-if="!subscription" class="text-color-secondary">
|
||||
Sem assinatura.
|
||||
<!-- ── Features agrupadas ─────────────────────── -->
|
||||
<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>
|
||||
|
||||
<div v-else-if="!features.length" class="text-color-secondary">
|
||||
Nenhuma feature vinculada a este plano.
|
||||
</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="space-y-5">
|
||||
<div
|
||||
v-for="g in groupedFeatures"
|
||||
:key="g.module"
|
||||
class="rounded-2xl border border-[var(--surface-border)] overflow-hidden"
|
||||
>
|
||||
<div class="px-4 py-3 bg-[var(--surface-50)] border-b border-[var(--surface-border)] flex items-center justify-between">
|
||||
<div class="font-semibold">
|
||||
{{ moduleLabel(g.module) }}
|
||||
<div v-else class="flex flex-col gap-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-[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>
|
||||
<Tag :value="`${g.items.length}`" severity="secondary" rounded />
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<ul class="m-0 p-0 list-none space-y-3">
|
||||
<li
|
||||
<!-- Grid 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="rounded-2xl border border-[var(--surface-border)] p-3"
|
||||
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"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="pi pi-check mt-1 text-sm text-color-secondary"></i>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium break-words">{{ f.key }}</div>
|
||||
<div class="text-sm text-color-secondary mt-1" v-if="f.description">
|
||||
{{ f.description }}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-color-secondary">
|
||||
Agrupamento automático por prefixo da key (ex.: <b>agenda.*</b>, <b>patients.*</b>, etc.).
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<!-- ✅ Histórico auditável -->
|
||||
<Card class="rounded-[2rem] overflow-hidden">
|
||||
<template #title>Histórico</template>
|
||||
<template #content>
|
||||
<div v-if="!subscription" class="text-color-secondary">
|
||||
Sem histórico (não há assinatura).
|
||||
</div>
|
||||
|
||||
<div v-else-if="!events.length" class="text-color-secondary">
|
||||
Sem eventos registrados.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="ev in events"
|
||||
:key="ev.id"
|
||||
class="rounded-2xl border border-[var(--surface-border)] p-3"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<Tag
|
||||
:value="eventLabel(ev.event_type)"
|
||||
:severity="eventSeverity(ev.event_type)"
|
||||
rounded
|
||||
/>
|
||||
<span class="text-sm text-color-secondary">
|
||||
por <b>{{ displayUser(ev.created_by) }}</b>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- De → Para (quando existir) -->
|
||||
<div v-if="ev.old_plan_id || ev.new_plan_id" class="mt-2 text-sm">
|
||||
<span class="text-color-secondary">Plano:</span>
|
||||
<span class="font-medium ml-2">{{ planKeyOrName(ev.old_plan_id) }}</span>
|
||||
<i class="pi pi-arrow-right text-color-secondary mx-2" />
|
||||
<span class="font-medium">{{ planKeyOrName(ev.new_plan_id) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="ev.reason" class="mt-2 text-sm opacity-80">
|
||||
{{ ev.reason }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-color-secondary">
|
||||
{{ fmtDate(ev.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-color-secondary" v-if="ev.metadata">
|
||||
<pre class="m-0 whitespace-pre-wrap break-words">{{ prettyMeta(ev.metadata) }}</pre>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-60">
|
||||
Agrupamento automático por prefixo da key (ex.: <b>agenda.*</b>, <b>patients.*</b>).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-xs text-color-secondary">
|
||||
Mostrando até 50 eventos (mais recentes).
|
||||
<!-- ── 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>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<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-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>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* (intencionalmente vazio) */
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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 há 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>
|
||||
|
||||
@@ -254,169 +254,221 @@ onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 md:p-6">
|
||||
<Toast />
|
||||
<Toast />
|
||||
|
||||
<!-- ✅ Topbar padrão -->
|
||||
<div class="mb-4 rounded-3xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-4 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-2xl font-semibold leading-none">Upgrade do terapeuta</div>
|
||||
<small class="text-color-secondary mt-1">
|
||||
Escolha seu plano pessoal (Modelo A).
|
||||
</small>
|
||||
<!-- Sentinel -->
|
||||
<div class="h-px" />
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
HERO sticky
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<section
|
||||
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"
|
||||
: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>
|
||||
|
||||
<div class="relative z-[1] flex flex-col gap-2.5">
|
||||
|
||||
<!-- Linha 1: brand + busca + ações -->
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<!-- 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-arrow-up-right text-base" />
|
||||
</div>
|
||||
<div class="min-w-0 hidden sm:block">
|
||||
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Upgrade do Terapeuta</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Escolha seu plano pessoal (Modelo A)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-wrap justify-end">
|
||||
<Button
|
||||
label="Voltar"
|
||||
icon="pi pi-arrow-left"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="saving"
|
||||
@click="goBack"
|
||||
/>
|
||||
<Button
|
||||
label="Atualizar"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="loading"
|
||||
:disabled="saving"
|
||||
@click="loadData"
|
||||
/>
|
||||
<!-- Busca desktop -->
|
||||
<div class="hidden md:flex flex-1 min-w-[180px] max-w-xs">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="q" placeholder="Buscar plano..." class="w-full" autocomplete="off" :disabled="loading || saving" />
|
||||
</IconField>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="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 flex-shrink-0" :loading="loading" :disabled="saving" title="Atualizar" @click="loadData" />
|
||||
<Button label="Voltar" icon="pi pi-arrow-left" severity="secondary" outlined class="rounded-full" :disabled="saving" @click="goBack" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap items-center gap-3">
|
||||
<!-- Linha 2: busca mobile + seletor de intervalo -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- Busca mobile -->
|
||||
<div class="flex md:hidden flex-1 min-w-[160px]">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="q" placeholder="Buscar plano..." class="w-full" autocomplete="off" :disabled="loading || saving" />
|
||||
</IconField>
|
||||
</div>
|
||||
|
||||
<!-- Intervalo chips -->
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<span class="text-[1rem] text-[var(--text-color-secondary)] opacity-70 hidden sm:inline">Preço:</span>
|
||||
<button
|
||||
v-for="opt in intervalOptions"
|
||||
:key="opt.value"
|
||||
class="inline-flex items-center gap-1.5 px-3.5 py-1 rounded-full text-[1rem] font-semibold border-[1.5px] cursor-pointer transition-all duration-150 select-none"
|
||||
:class="billingInterval === opt.value
|
||||
? 'bg-[var(--primary-color,#6366f1)] border-[var(--primary-color,#6366f1)] text-white'
|
||||
: 'border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] text-[var(--text-color-secondary)] hover:border-indigo-300 hover:text-[var(--text-color)]'"
|
||||
:disabled="loading || saving"
|
||||
@click="billingInterval = opt.value"
|
||||
>{{ opt.label }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Plano atual -->
|
||||
<Tag
|
||||
v-if="currentSub"
|
||||
:value="`Plano atual: ${currentSub.plan_key} • ${intervalLabel(currentSub.interval)} • ${currentSub.status}`"
|
||||
:value="`Atual: ${currentSub.plan_key} · ${intervalLabel(currentSub.interval)}`"
|
||||
severity="success"
|
||||
rounded
|
||||
/>
|
||||
<Tag
|
||||
v-else
|
||||
value="Você ainda não tem um plano pessoal."
|
||||
severity="warning"
|
||||
rounded
|
||||
/>
|
||||
<Tag v-else value="Sem plano pessoal" severity="warning" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 ml-auto">
|
||||
<small class="text-color-secondary">Exibição de preço</small>
|
||||
<SelectButton
|
||||
v-model="billingInterval"
|
||||
:options="intervalOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:disabled="loading || saving"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
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.35rem] font-bold leading-none text-[var(--text-color)]">{{ filteredPlans.length }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75">Planos disponíveis</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.1rem] font-bold leading-none text-[var(--text-color)] truncate">{{ currentSub?.plan_key || '—' }}</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] border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)]">
|
||||
<div class="text-[1.1rem] font-bold leading-none text-[var(--text-color)]">{{ billingInterval === 'month' ? 'Mensal' : 'Anual' }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75">Exibição de preço</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
PLANOS
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="px-3 md:px-4 pb-8">
|
||||
|
||||
<!-- Loading skeleton -->
|
||||
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
<div
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] p-4"
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="h-4 w-2/5 rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
|
||||
<div class="h-3 w-3/5 rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
|
||||
<div class="h-8 w-1/3 rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Busca padrão FloatLabel -->
|
||||
<Card class="rounded-[2rem] overflow-hidden mb-4">
|
||||
<template #content>
|
||||
<div class="flex flex-wrap items-center gap-3 justify-between">
|
||||
<div class="flex flex-col">
|
||||
<div class="font-semibold">Planos disponíveis</div>
|
||||
<small class="text-color-secondary">
|
||||
Filtre por nome/key/descrição e selecione.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-[420px]">
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="q" id="therapist_upgrade_search" class="w-full pr-10" variant="filled" />
|
||||
</IconField>
|
||||
<label for="therapist_upgrade_search">Buscar plano...</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-else-if="!filteredPlans.length"
|
||||
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-box text-3xl opacity-30" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<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>
|
||||
<div class="font-bold text-[1rem] text-[var(--text-color)] mb-1">Nenhum plano encontrado</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] max-w-xs leading-relaxed">Tente limpar o filtro de busca.</div>
|
||||
</div>
|
||||
<Button label="Limpar busca" icon="pi pi-filter-slash" severity="secondary" outlined class="rounded-full mt-1" @click="q = ''" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<!-- ✅ Cards estilo vitrine -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 mt-4">
|
||||
<Card
|
||||
<!-- Grid de planos -->
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
<div
|
||||
v-for="p in filteredPlans"
|
||||
:key="p.id"
|
||||
:class="[
|
||||
'rounded-[2rem] overflow-hidden border border-[var(--surface-border)]',
|
||||
currentSub?.plan_id === p.id ? 'ring-1 ring-emerald-500/25 md:-translate-y-1 md:scale-[1.01]' : ''
|
||||
]"
|
||||
class="rounded-md border bg-[var(--surface-card,#fff)] overflow-hidden flex flex-col transition-shadow duration-150 hover:shadow-[0_4px_18px_rgba(0,0,0,0.07)]"
|
||||
:class="currentSub?.plan_id === p.id
|
||||
? 'border-emerald-400/40 ring-1 ring-emerald-500/20'
|
||||
: 'border-[var(--surface-border,#e2e8f0)]'"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold truncate">{{ p.name || p.key }}</div>
|
||||
<small class="text-color-secondary">{{ p.key }}</small>
|
||||
</div>
|
||||
|
||||
<Tag v-if="currentSub?.plan_id === p.id" severity="success" value="Atual" rounded />
|
||||
<!-- Cabeçalho do card -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border,#e2e8f0)]">
|
||||
<div class="min-w-0">
|
||||
<div class="font-bold text-[0.9rem] text-[var(--text-color)] truncate">{{ p.name || p.key }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">{{ p.key }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<Tag v-if="currentSub?.plan_id === p.id" severity="success" value="Atual" />
|
||||
</div>
|
||||
|
||||
<template #content>
|
||||
<div class="text-sm text-color-secondary" v-if="p.description">
|
||||
{{ p.description }}
|
||||
<!-- Corpo do card -->
|
||||
<div class="p-4 flex flex-col gap-4 flex-1">
|
||||
<!-- Descrição -->
|
||||
<div v-if="p.description" class="text-[1rem] text-[var(--text-color-secondary)]">{{ p.description }}</div>
|
||||
|
||||
<!-- Preço -->
|
||||
<div>
|
||||
<div class="text-[2rem] font-bold leading-none text-[var(--text-color)]">{{ priceLabelForCard(p) }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1 opacity-70">Alternar mensal/anual no topo para comparar.</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="text-4xl font-semibold leading-none">
|
||||
{{ priceLabelForCard(p) }}
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-color-secondary mt-1">
|
||||
Alternar mensal/anual no topo para comparar.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex gap-2 flex-wrap">
|
||||
<!-- Ações -->
|
||||
<div class="flex flex-col gap-2 mt-auto">
|
||||
<Button
|
||||
:label="billingInterval === 'month' ? 'Escolher mensal' : 'Escolher anual'"
|
||||
icon="pi pi-check"
|
||||
class="rounded-full w-full"
|
||||
:loading="saving"
|
||||
:disabled="loading || saving"
|
||||
@click="choosePlan(p, billingInterval)"
|
||||
/>
|
||||
|
||||
<Button
|
||||
label="Mensal"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="loading || saving"
|
||||
@click="choosePlan(p, 'month')"
|
||||
/>
|
||||
|
||||
<Button
|
||||
label="Anual"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="loading || saving"
|
||||
@click="choosePlan(p, 'year')"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
label="Mensal"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full flex-1"
|
||||
:disabled="loading || saving"
|
||||
@click="choosePlan(p, 'month')"
|
||||
/>
|
||||
<Button
|
||||
label="Anual"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full flex-1"
|
||||
:disabled="loading || saving"
|
||||
@click="choosePlan(p, 'year')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-color-secondary">
|
||||
<span v-if="priceFor(p.id, billingInterval)">
|
||||
Preço ativo encontrado para {{ intervalLabel(billingInterval) }}.
|
||||
</span>
|
||||
<span v-else>
|
||||
Sem preço ativo para {{ intervalLabel(billingInterval) }}.
|
||||
</span>
|
||||
<!-- Status do preço -->
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-60">
|
||||
<span v-if="priceFor(p.id, billingInterval)">Preço ativo encontrado para {{ intervalLabel(billingInterval) }}.</span>
|
||||
<span v-else>Sem preço ativo para {{ intervalLabel(billingInterval) }}.</span>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!filteredPlans.length && !loading" class="mt-4 text-sm text-color-secondary">
|
||||
Nenhum plano encontrado.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* (intencionalmente vazio) */
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- src/views/pages/upgrade/UpgradePage.vue -->
|
||||
<!-- src/views/pages/billing/UpgradePage.vue -->
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -311,174 +311,292 @@ watch(() => tenantStore.user?.id, () => { fetchAll() })
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="p-4 md:p-6 lg:p-8">
|
||||
<!-- HERO -->
|
||||
<div class="mb-5 overflow-hidden rounded-[2rem] border border-[var(--surface-border)] bg-[var(--surface-card)]">
|
||||
<div class="relative p-5 md:p-7">
|
||||
<div class="pointer-events-none absolute inset-0 opacity-80">
|
||||
<div class="absolute -top-20 -right-24 h-80 w-80 rounded-full bg-indigo-400/10 blur-3xl" />
|
||||
<div class="absolute top-12 -left-24 h-96 w-96 rounded-full bg-emerald-400/10 blur-3xl" />
|
||||
<div class="absolute -bottom-20 right-32 h-80 w-80 rounded-full bg-fuchsia-400/10 blur-3xl" />
|
||||
</div>
|
||||
<!-- Sentinel -->
|
||||
<div class="h-px" />
|
||||
|
||||
<div class="relative flex flex-col gap-4">
|
||||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div class="min-w-0">
|
||||
<div class="text-2xl md:text-3xl font-semibold leading-tight">Atualize seu plano</div>
|
||||
<div class="mt-1 text-sm md:text-base text-[var(--text-color-secondary)]">
|
||||
Contexto: <b>{{ isTherapist ? 'Terapeuta' : 'Clínica' }}</b>
|
||||
<span class="mx-2 opacity-50">•</span>
|
||||
Plano atual: <b>{{ currentPlanKey || '—' }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
HERO sticky
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<section
|
||||
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"
|
||||
: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 class="absolute w-56 h-56 -bottom-8 right-1/4 rounded-full blur-[55px] bg-fuchsia-400/[0.07]" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<Button label="Voltar" icon="pi pi-arrow-left" severity="secondary" outlined :disabled="upgrading" @click="goBack" />
|
||||
<Button label="Assinatura" icon="pi pi-credit-card" severity="secondary" outlined :disabled="upgrading" @click="goBilling" />
|
||||
<Button label="Recarregar" icon="pi pi-refresh" severity="secondary" outlined :loading="loading" :disabled="upgrading" @click="fetchAll" />
|
||||
<div class="relative z-[1] flex flex-col gap-2.5">
|
||||
|
||||
<!-- Linha 1: brand + busca + ações -->
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<!-- 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-sparkles text-base" />
|
||||
</div>
|
||||
<div class="min-w-0 hidden sm:block">
|
||||
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Atualize seu plano</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Contexto: <b>{{ isTherapist ? 'Terapeuta' : 'Clínica' }}</b>
|
||||
<span class="mx-1.5 opacity-40">·</span>
|
||||
Plano atual: <b>{{ currentPlanKey || '—' }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- recurso bloqueado -->
|
||||
<div
|
||||
v-if="requestedFeatureLabel"
|
||||
class="relative overflow-hidden rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4"
|
||||
>
|
||||
<div class="relative flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag severity="warning" value="Recurso bloqueado" />
|
||||
<div class="font-semibold truncate">{{ requestedFeatureLabel }}</div>
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-[var(--text-color-secondary)]">
|
||||
Esse recurso depende da feature <b>{{ requestedFeature }}</b>.
|
||||
</div>
|
||||
</div>
|
||||
<!-- Busca desktop -->
|
||||
<div class="hidden md:flex flex-1 min-w-[180px] max-w-xs">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="q" placeholder="Buscar plano..." class="w-full" autocomplete="off" :disabled="loading || upgrading" />
|
||||
</IconField>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="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 flex-shrink-0" :loading="loading" :disabled="upgrading" title="Recarregar" @click="fetchAll" />
|
||||
<Button label="Assinatura" icon="pi pi-credit-card" severity="secondary" outlined class="rounded-full hidden sm:inline-flex" :disabled="upgrading" @click="goBilling" />
|
||||
<Button label="Voltar" icon="pi pi-arrow-left" severity="secondary" outlined class="rounded-full" :disabled="upgrading" @click="goBack" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Linha 2: busca mobile + chips de intervalo -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- Busca mobile -->
|
||||
<div class="flex md:hidden flex-1 min-w-[160px]">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="q" placeholder="Buscar plano..." class="w-full" autocomplete="off" :disabled="loading || upgrading" />
|
||||
</IconField>
|
||||
</div>
|
||||
|
||||
<!-- Intervalo chips -->
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<span class="text-[1rem] text-[var(--text-color-secondary)] opacity-70 hidden sm:inline">Preço:</span>
|
||||
<button
|
||||
v-for="opt in intervalOptions"
|
||||
:key="opt.value"
|
||||
class="inline-flex items-center gap-1.5 px-3.5 py-1 rounded-full text-[1rem] font-semibold border-[1.5px] cursor-pointer transition-all duration-150 select-none"
|
||||
:class="billingInterval === opt.value
|
||||
? 'bg-[var(--primary-color,#6366f1)] border-[var(--primary-color,#6366f1)] text-white'
|
||||
: 'border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] text-[var(--text-color-secondary)] hover:border-indigo-300 hover:text-[var(--text-color)]'"
|
||||
:disabled="loading || upgrading"
|
||||
@click="billingInterval = opt.value"
|
||||
>{{ opt.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
BANNER: recurso bloqueado
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<Transition name="up-banner">
|
||||
<div
|
||||
v-if="requestedFeatureLabel && !loading"
|
||||
class="mx-3 md:mx-4 mb-3 flex items-center gap-3 px-4 py-3 rounded-md border border-amber-300/60 bg-amber-50"
|
||||
>
|
||||
<div class="grid place-items-center w-8 h-8 rounded-md bg-amber-400/20 text-amber-600 flex-shrink-0">
|
||||
<i class="pi pi-lock text-[0.95rem]" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="font-semibold text-[1rem] text-amber-800">Recurso bloqueado: {{ requestedFeatureLabel }}</span>
|
||||
<span class="hidden sm:inline text-[1rem] text-amber-700 opacity-80 ml-1">Esse recurso depende da feature <b>{{ requestedFeature }}</b>. Escolha um plano que a inclua.</span>
|
||||
</div>
|
||||
<Button
|
||||
label="Ver planos"
|
||||
icon="pi pi-arrow-down"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
class="rounded-full flex-shrink-0"
|
||||
@click="() => document.getElementById('plans-grid')?.scrollIntoView({ behavior: 'smooth', block: 'start' })"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
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.35rem] font-bold leading-none text-[var(--text-color)]">{{ sortedPlans.length }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75">Planos disponíveis</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.1rem] font-bold leading-none text-[var(--text-color)] truncate">{{ currentPlanKey || '—' }}</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] border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)]">
|
||||
<div class="text-[1.1rem] font-bold leading-none text-[var(--text-color)]">{{ isTherapist ? 'Terapeuta' : 'Clínica' }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75">Contexto</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">Features no sistema</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
PLANOS
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="px-3 md:px-4 pb-8">
|
||||
|
||||
<!-- Loading skeleton -->
|
||||
<div v-if="loading" class="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="n in 2"
|
||||
:key="n"
|
||||
class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] p-4"
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="h-5 w-2/5 rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
|
||||
<div class="h-3 w-3/5 rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<div v-for="i in 4" :key="i" class="h-3 w-4/5 rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-else-if="!sortedPlans.length"
|
||||
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-box 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>
|
||||
<div class="font-bold text-[1rem] text-[var(--text-color)] mb-1">Nenhum plano encontrado</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] max-w-xs leading-relaxed">Tente limpar o filtro de busca.</div>
|
||||
</div>
|
||||
<Button label="Limpar busca" icon="pi pi-filter-slash" severity="secondary" outlined class="rounded-full mt-1" @click="q = ''" />
|
||||
</div>
|
||||
|
||||
<!-- Grid de planos -->
|
||||
<div v-else id="plans-grid" class="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="p in sortedPlans"
|
||||
:key="p.id"
|
||||
class="relative overflow-hidden rounded-md border bg-[var(--surface-card,#fff)] flex flex-col"
|
||||
:class="String(p.key).toLowerCase() === 'pro'
|
||||
? 'border-[var(--primary-color,#6366f1)]/30'
|
||||
: 'border-[var(--surface-border,#e2e8f0)]'"
|
||||
>
|
||||
<!-- Blobs decorativos (plano PRO) -->
|
||||
<div v-if="String(p.key).toLowerCase() === 'pro'" class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
|
||||
<div class="absolute -top-20 -right-16 w-72 h-72 rounded-full bg-[var(--primary-color,#6366f1)]/10 blur-[60px]" />
|
||||
<div class="absolute -bottom-20 left-8 w-72 h-72 rounded-full bg-emerald-400/[0.08] blur-[60px]" />
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho do card -->
|
||||
<div class="relative z-[1] 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="text-[0.9rem]"
|
||||
:class="String(p.key).toLowerCase() === 'pro' ? 'pi pi-sparkles text-[var(--primary-color,#6366f1)]' : 'pi pi-leaf text-emerald-500 opacity-70'"
|
||||
/>
|
||||
<span class="font-bold text-[0.95rem] text-[var(--text-color)]">Plano {{ String(p.key || '').toUpperCase() }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag v-if="currentPlanId === p.id" value="Atual" severity="secondary" />
|
||||
<Tag v-else-if="String(p.key).toLowerCase() === 'pro'" value="Recomendado" severity="success" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Corpo do card -->
|
||||
<div class="relative z-[1] p-4 flex flex-col gap-4 flex-1">
|
||||
|
||||
<!-- Descrição + preço -->
|
||||
<div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mb-2">
|
||||
<template v-if="String(p.key).toLowerCase() === 'free'">O essencial para começar, sem travar seu fluxo.</template>
|
||||
<template v-else-if="String(p.key).toLowerCase() === 'pro'">Para automatizar, reduzir ruído e ganhar previsibilidade.</template>
|
||||
<template v-else>{{ p.description || p.key }}</template>
|
||||
</div>
|
||||
<div class="text-[1.6rem] font-bold leading-none text-[var(--text-color)]">{{ priceLabelForPlan(p.id) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Benefits list -->
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] p-3">
|
||||
<ul class="list-none p-0 m-0 flex flex-col gap-2.5">
|
||||
<li v-for="(b, i) in planBenefits(p.id)" :key="i" class="flex items-start gap-2">
|
||||
<i
|
||||
class="text-[1rem] mt-0.5 flex-shrink-0"
|
||||
:class="b.ok ? 'pi pi-check-circle text-emerald-500' : 'pi pi-times-circle text-[var(--text-color-secondary)] opacity-40'"
|
||||
/>
|
||||
<span
|
||||
class="text-[1rem]"
|
||||
:class="b.ok ? 'text-[var(--text-color)]' : 'text-[var(--text-color-secondary)]'"
|
||||
>{{ b.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="h-px bg-[var(--surface-border,#e2e8f0)] my-3" />
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button
|
||||
label="Ver planos"
|
||||
icon="pi pi-arrow-down"
|
||||
v-if="currentPlanId !== p.id"
|
||||
:label="`Mudar para ${String(p.key || '').toUpperCase()}`"
|
||||
icon="pi pi-arrow-up"
|
||||
class="w-full rounded-full"
|
||||
:loading="upgrading"
|
||||
:disabled="upgrading || loading"
|
||||
@click="changePlan(p.id)"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
label="Você já está neste plano"
|
||||
icon="pi pi-check"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="() => document.getElementById('plans-grid')?.scrollIntoView({ behavior: 'smooth', block: 'start' })"
|
||||
class="w-full rounded-full"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- busca + intervalo -->
|
||||
<div class="flex flex-col md:flex-row md:items-end md:justify-between gap-3">
|
||||
<div class="w-full md:w-[420px]">
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="q" id="upgrade_search" class="w-full pr-10" variant="filled" :disabled="loading || upgrading" />
|
||||
</IconField>
|
||||
<label for="upgrade_search">Buscar plano...</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col items-start md:items-end gap-2">
|
||||
<small class="text-[var(--text-color-secondary)]">Exibição de preço</small>
|
||||
<SelectButton v-model="billingInterval" :options="intervalOptions" optionLabel="label" optionValue="value" :disabled="loading || upgrading" />
|
||||
<Button
|
||||
v-if="String(p.key).toLowerCase() !== 'free'"
|
||||
label="Falar com suporte"
|
||||
icon="pi pi-comments"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="w-full rounded-full"
|
||||
:disabled="upgrading"
|
||||
@click="contactSupport"
|
||||
/>
|
||||
<div class="text-center text-[1rem] text-[var(--text-color-secondary)] opacity-60">
|
||||
Cancele quando quiser. Sem burocracia.
|
||||
</div>
|
||||
<div v-if="!subscription?.id" class="text-center text-[1rem] text-amber-500">
|
||||
⚠ Sem assinatura ativa — clique em <b>Assinatura</b> para ativar/criar.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PLANOS -->
|
||||
<div id="plans-grid" class="grid grid-cols-12 gap-4 md:gap-6">
|
||||
<div v-for="p in sortedPlans" :key="p.id" class="col-span-12 lg:col-span-6">
|
||||
<div
|
||||
:class="String(p.key).toLowerCase() === 'pro'
|
||||
? 'relative overflow-hidden rounded-[1.75rem] border border-primary/40 bg-[var(--surface-card)]'
|
||||
: 'relative overflow-hidden rounded-[1.75rem] border border-[var(--surface-border)] bg-[var(--surface-card)]'"
|
||||
>
|
||||
<div v-if="String(p.key).toLowerCase() === 'pro'" class="pointer-events-none absolute inset-0 opacity-80">
|
||||
<div class="absolute -top-24 -right-28 h-96 w-96 rounded-full bg-primary/10 blur-3xl" />
|
||||
<div class="absolute -bottom-28 left-12 h-96 w-96 rounded-full bg-emerald-400/10 blur-3xl" />
|
||||
</div>
|
||||
|
||||
<Card class="relative border-0">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<i :class="String(p.key).toLowerCase() === 'pro' ? 'pi pi-sparkles opacity-80' : 'pi pi-leaf opacity-70'" />
|
||||
<span class="text-xl font-semibold">Plano {{ String(p.key || '').toUpperCase() }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag v-if="currentPlanId === p.id" value="Atual" severity="secondary" />
|
||||
<Tag v-else-if="String(p.key).toLowerCase() === 'pro'" value="Recomendado" severity="success" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #subtitle>
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<span class="text-[var(--text-color-secondary)]">
|
||||
<template v-if="String(p.key).toLowerCase() === 'free'">O essencial para começar, sem travar seu fluxo.</template>
|
||||
<template v-else-if="String(p.key).toLowerCase() === 'pro'">Para automatizar, reduzir ruído e ganhar previsibilidade.</template>
|
||||
<template v-else>Plano: {{ p.key }}</template>
|
||||
</span>
|
||||
<span class="text-sm font-semibold">{{ priceLabelForPlan(p.id) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
|
||||
<ul class="list-none p-0 m-0 flex flex-col gap-3">
|
||||
<li v-for="(b, i) in planBenefits(p.id)" :key="i" class="flex items-start gap-2">
|
||||
<i :class="b.ok ? 'pi pi-check-circle text-emerald-500' : 'pi pi-times-circle opacity-50'" class="mt-0.5" />
|
||||
<span :class="b.ok ? '' : 'text-[var(--text-color-secondary)]'">{{ b.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Divider class="my-4" />
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<Button
|
||||
v-if="currentPlanId !== p.id"
|
||||
:label="`Mudar para ${String(p.key || '').toUpperCase()}`"
|
||||
icon="pi pi-arrow-up"
|
||||
size="large"
|
||||
class="w-full"
|
||||
:loading="upgrading"
|
||||
:disabled="upgrading || loading"
|
||||
@click="changePlan(p.id)"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
label="Você já está neste plano"
|
||||
icon="pi pi-check"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="w-full"
|
||||
disabled
|
||||
/>
|
||||
<Button
|
||||
v-if="String(p.key).toLowerCase() !== 'free'"
|
||||
label="Falar com suporte"
|
||||
icon="pi pi-comments"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="w-full"
|
||||
:disabled="upgrading"
|
||||
@click="contactSupport"
|
||||
/>
|
||||
<div class="text-center text-xs text-[var(--text-color-secondary)]">
|
||||
Cancele quando quiser. Sem burocracia.
|
||||
</div>
|
||||
<div v-if="!subscription?.id" class="text-center text-xs text-amber-500">
|
||||
⚠ Sem assinatura ativa — clique em <b>Assinatura</b> para ativar/criar.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-xs text-[var(--text-color-secondary)]">
|
||||
<div class="mt-4 text-[1rem] text-[var(--text-color-secondary)] opacity-60">
|
||||
Alguns recursos PRO podem depender de configuração inicial (ex.: SMS exige provedor).
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.up-banner-enter-active,
|
||||
.up-banner-leave-active { transition: all 0.25s ease; overflow: hidden; }
|
||||
.up-banner-enter-from,
|
||||
.up-banner-leave-to { opacity: 0; max-height: 0; margin-bottom: 0; }
|
||||
.up-banner-enter-to,
|
||||
.up-banner-leave-from { opacity: 1; max-height: 80px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user