Layout 100%, Notificações, SetupWizard
This commit is contained in:
@@ -407,424 +407,341 @@ onBeforeUnmount(() => {
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Info decorativa (scrolls away naturalmente) -->
|
||||
<div class="flex items-center gap-3 px-4 pb-3">
|
||||
<div class="dash-hero__icon shrink-0">
|
||||
<i class="pi pi-chart-bar text-2xl" />
|
||||
</div>
|
||||
<small class="text-color-secondary">
|
||||
Visão estratégica (receita e distribuição) + saúde de consistência (entitlements).
|
||||
</small>
|
||||
</div>
|
||||
<!-- Sentinel -->
|
||||
<div ref="sentinelRef" class="h-px" />
|
||||
|
||||
<!-- sentinel -->
|
||||
<div ref="sentinelRef" style="height: 1px; pointer-events: none;" />
|
||||
|
||||
<!-- hero -->
|
||||
<!-- Hero sticky -->
|
||||
<div
|
||||
ref="heroRef"
|
||||
class="dash-hero"
|
||||
:class="{ 'dash-hero--stuck': heroStuck }"
|
||||
class="sticky mx-3 md:mx-4 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
|
||||
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
|
||||
>
|
||||
<div class="dash-hero__blob dash-hero__blob--1" />
|
||||
<div class="dash-hero__blob dash-hero__blob--2" />
|
||||
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
||||
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-20 bg-indigo-400/10" />
|
||||
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-emerald-400/10" />
|
||||
<div class="absolute rounded-full blur-[70px] w-72 h-72 -bottom-20 right-24 bg-fuchsia-400/10" />
|
||||
</div>
|
||||
<div class="relative z-10 flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="min-w-0">
|
||||
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Central de Controle do SaaS</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Visão estratégica (receita e distribuição) + saúde de consistência (entitlements).</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xl font-bold leading-none">Central de Controle do SaaS</span>
|
||||
|
||||
<!-- desktop actions -->
|
||||
<div class="hidden xl:flex items-center gap-2">
|
||||
<SelectButton
|
||||
v-model="intervalView"
|
||||
:options="intervalOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<Button
|
||||
label="Recarregar"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="loading"
|
||||
@click="loadStats"
|
||||
/>
|
||||
<Button
|
||||
label="Assinaturas"
|
||||
icon="pi pi-credit-card"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="loading"
|
||||
@click="router.push('/saas/subscriptions')"
|
||||
/>
|
||||
<Button
|
||||
label="Eventos"
|
||||
icon="pi pi-history"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="loading"
|
||||
@click="router.push('/saas/subscription-events')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- mobile -->
|
||||
<div class="flex xl:hidden">
|
||||
<Button
|
||||
label="Ações"
|
||||
icon="pi pi-ellipsis-v"
|
||||
severity="warn"
|
||||
outlined
|
||||
@click="(e) => mobileMenuRef.toggle(e)"
|
||||
/>
|
||||
<Menu ref="mobileMenuRef" :model="heroMenuItems" popup />
|
||||
</div>
|
||||
</div>
|
||||
<!-- desktop actions -->
|
||||
<div class="hidden xl:flex items-center gap-2 shrink-0">
|
||||
<SelectButton
|
||||
v-model="intervalView"
|
||||
:options="intervalOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<Button
|
||||
label="Recarregar"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="loading"
|
||||
@click="loadStats"
|
||||
/>
|
||||
<Button
|
||||
label="Assinaturas"
|
||||
icon="pi pi-credit-card"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="loading"
|
||||
@click="router.push('/saas/subscriptions')"
|
||||
/>
|
||||
<Button
|
||||
label="Eventos"
|
||||
icon="pi pi-history"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="loading"
|
||||
@click="router.push('/saas/subscription-events')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- mobile -->
|
||||
<div class="flex xl:hidden shrink-0">
|
||||
<Button
|
||||
label="Ações"
|
||||
icon="pi pi-ellipsis-v"
|
||||
severity="warn"
|
||||
outlined
|
||||
@click="(e) => mobileMenuRef.toggle(e)"
|
||||
/>
|
||||
<Menu ref="mobileMenuRef" :model="heroMenuItems" popup />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- content -->
|
||||
<div class="px-4 pb-4">
|
||||
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
|
||||
<!-- KPIs -->
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Ativas</span>
|
||||
<Tag value="active" severity="success" rounded />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="text-4xl font-semibold">{{ totalActive }}</div>
|
||||
<small class="text-color-secondary">assinaturas em status <b>active</b></small>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 h-full">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Ativas</div>
|
||||
<Tag value="active" severity="success" rounded />
|
||||
</div>
|
||||
<div class="text-4xl font-semibold">{{ totalActive }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">assinaturas em status <b>active</b></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Canceladas</span>
|
||||
<Tag value="canceled" severity="danger" rounded />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="text-4xl font-semibold">{{ totalCanceled }}</div>
|
||||
<small class="text-color-secondary">assinaturas em status <b>canceled</b></small>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 h-full">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Canceladas</div>
|
||||
<Tag value="canceled" severity="danger" rounded />
|
||||
</div>
|
||||
<div class="text-4xl font-semibold">{{ totalCanceled }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">assinaturas em status <b>canceled</b></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ intervalView === 'year' ? 'ARR' : 'MRR' }}</span>
|
||||
<Tag :value="intervalView === 'year' ? 'anual' : 'mensal'" severity="secondary" rounded />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="text-3xl font-semibold">{{ moneyBRLFromCents(revenueCents) }}</div>
|
||||
<small class="text-color-secondary">normalizado (mensal ↔ anual)</small>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 h-full">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)]">{{ intervalView === 'year' ? 'ARR' : 'MRR' }}</div>
|
||||
<Tag :value="intervalView === 'year' ? 'anual' : 'mensal'" severity="secondary" rounded />
|
||||
</div>
|
||||
<div class="text-3xl font-semibold">{{ moneyBRLFromCents(revenueCents) }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">normalizado (mensal ↔ anual)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>ARPA</span>
|
||||
<Tag value="média" severity="secondary" rounded />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="text-3xl font-semibold">{{ moneyBRLFromCents(arpaCents) }}</div>
|
||||
<small class="text-color-secondary">média por assinatura ativa</small>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 h-full">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)]">ARPA</div>
|
||||
<Tag value="média" severity="secondary" rounded />
|
||||
</div>
|
||||
<div class="text-3xl font-semibold">{{ moneyBRLFromCents(arpaCents) }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">média por assinatura ativa</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intenções + Health + Chart -->
|
||||
<div class="grid grid-cols-12 gap-4 mt-4">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<!-- Intenções -->
|
||||
<div class="col-span-12 md:col-span-4">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Intenções de assinatura</span>
|
||||
<Tag :value="intentsLoading ? 'carregando' : 'últimas'" severity="secondary" rounded />
|
||||
</div>
|
||||
</template>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 h-full">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Intenções de assinatura</div>
|
||||
<Tag :value="intentsLoading ? 'carregando' : 'últimas'" severity="secondary" rounded />
|
||||
</div>
|
||||
|
||||
<template #content>
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-4 rounded-xl border border-[var(--surface-border)] p-3">
|
||||
<div class="text-xs text-color-secondary">Total</div>
|
||||
<div class="text-2xl font-semibold">{{ totalIntents }}</div>
|
||||
</div>
|
||||
<div class="col-span-4 rounded-xl border border-[var(--surface-border)] p-3">
|
||||
<div class="text-xs text-color-secondary">New</div>
|
||||
<div class="text-2xl font-semibold">{{ totalIntentsNew }}</div>
|
||||
</div>
|
||||
<div class="col-span-4 rounded-xl border border-[var(--surface-border)] p-3">
|
||||
<div class="text-xs text-color-secondary">Paid</div>
|
||||
<div class="text-2xl font-semibold">{{ totalIntentsPaid }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-4 rounded-md border border-[var(--surface-border)] p-3">
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Total</div>
|
||||
<div class="text-2xl font-semibold">{{ totalIntents }}</div>
|
||||
</div>
|
||||
<div class="col-span-4 rounded-md border border-[var(--surface-border)] p-3">
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">New</div>
|
||||
<div class="text-2xl font-semibold">{{ totalIntentsNew }}</div>
|
||||
</div>
|
||||
<div class="col-span-4 rounded-md border border-[var(--surface-border)] p-3">
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Paid</div>
|
||||
<div class="text-2xl font-semibold">{{ totalIntentsPaid }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider class="my-3" />
|
||||
|
||||
<div v-if="intentsLoading" class="text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Carregando intenções…
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="!intents.length" class="text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Nenhuma intenção encontrada.
|
||||
</div>
|
||||
|
||||
<Divider class="my-3" />
|
||||
|
||||
<div v-if="intentsLoading" class="text-color-secondary text-sm">
|
||||
Carregando intenções…
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="!intents.length" class="text-color-secondary text-sm">
|
||||
Nenhuma intenção encontrada.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="(it, idx) in intents"
|
||||
:key="idx"
|
||||
class="flex items-start justify-between gap-3 rounded-xl border border-[var(--surface-border)] p-3"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium truncate">
|
||||
{{ maskEmail(it.email) }}
|
||||
</div>
|
||||
<div class="text-xs text-color-secondary mt-1">
|
||||
{{ it.plan_key || '—' }} • {{ intervalLabel(it.interval) }} •
|
||||
<span class="font-mono">{{ it.tenant_id ? String(it.tenant_id).slice(0, 8) + '…' : '—' }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-color-secondary mt-1">
|
||||
{{ fmtDate(it.created_at) }}
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="(it, idx) in intents"
|
||||
:key="idx"
|
||||
class="flex items-start justify-between gap-3 rounded-md border border-[var(--surface-border)] p-3"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium truncate">
|
||||
{{ maskEmail(it.email) }}
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
<Tag :value="it.status || '—'" :severity="statusSeverity(it.status)" rounded />
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
|
||||
{{ it.plan_key || '—' }} • {{ intervalLabel(it.interval) }} •
|
||||
<span class="font-mono">{{ it.tenant_id ? String(it.tenant_id).slice(0, 8) + '…' : '—' }}</span>
|
||||
</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
|
||||
{{ fmtDate(it.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap mt-3">
|
||||
<Button
|
||||
label="Atualizar"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
:loading="intentsLoading || loading"
|
||||
@click="loadIntents"
|
||||
/>
|
||||
<Button
|
||||
label="Ver eventos"
|
||||
icon="pi pi-history"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
:disabled="loading"
|
||||
@click="openIntentEvents"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-color-secondary text-xs mt-3">
|
||||
Mostrando {{ intentsLimit }} itens mais recentes de <span class="font-mono">subscription_intents</span>.
|
||||
<div class="shrink-0">
|
||||
<Tag :value="it.status || '—'" :severity="statusSeverity(it.status)" rounded />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<div class="flex gap-2 flex-wrap mt-3">
|
||||
<Button
|
||||
label="Atualizar"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
:loading="intentsLoading || loading"
|
||||
@click="loadIntents"
|
||||
/>
|
||||
<Button
|
||||
label="Ver eventos"
|
||||
icon="pi pi-history"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
:disabled="loading"
|
||||
@click="openIntentEvents"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-3">
|
||||
Mostrando {{ intentsLimit }} itens mais recentes de <span class="font-mono">subscription_intents</span>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Health -->
|
||||
<div class="col-span-12 md:col-span-4">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Saúde do sistema</span>
|
||||
<Tag :severity="healthSeverity" :value="healthLabel" rounded />
|
||||
</div>
|
||||
</template>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 h-full">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Saúde do sistema</div>
|
||||
<Tag :severity="healthSeverity" :value="healthLabel" rounded />
|
||||
</div>
|
||||
|
||||
<template #content>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="text-4xl font-semibold">{{ totalMismatches }}</div>
|
||||
<small class="text-color-secondary text-right">
|
||||
divergências entre plano (esperado) e entitlements (atual)
|
||||
</small>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="text-4xl font-semibold">{{ totalMismatches }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] text-right">
|
||||
divergências entre plano (esperado) e entitlements (atual)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-color-secondary text-sm mt-2">
|
||||
{{ healthHint }}
|
||||
</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-2">
|
||||
{{ healthHint }}
|
||||
</div>
|
||||
|
||||
<Divider class="my-3" />
|
||||
<Divider class="my-3" />
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
v-if="totalMismatches > 0"
|
||||
label="Corrigir tudo"
|
||||
icon="pi pi-refresh"
|
||||
severity="danger"
|
||||
:loading="loading"
|
||||
@click="askFixAll"
|
||||
/>
|
||||
<Button
|
||||
label="Ver divergências"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="loading"
|
||||
@click="router.push('/saas/subscription-health')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
v-if="totalMismatches > 0"
|
||||
label="Corrigir tudo"
|
||||
icon="pi pi-refresh"
|
||||
severity="danger"
|
||||
:loading="loading"
|
||||
@click="askFixAll"
|
||||
/>
|
||||
<Button
|
||||
label="Ver divergências"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:disabled="loading"
|
||||
@click="router.push('/saas/subscription-health')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-color-secondary text-xs mt-3" v-if="lastUpdatedAt">
|
||||
Atualizado em {{ fmtDate(lastUpdatedAt) }}
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-3" v-if="lastUpdatedAt">
|
||||
Atualizado em {{ fmtDate(lastUpdatedAt) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="col-span-12 md:col-span-4">
|
||||
<Card class="h-full">
|
||||
<template #title>{{ intervalView === 'year' ? 'ARR por plano' : 'MRR por plano' }}</template>
|
||||
<template #content>
|
||||
<div style="height: 260px;">
|
||||
<Chart type="bar" :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 h-full">
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-3">{{ intervalView === 'year' ? 'ARR por plano' : 'MRR por plano' }}</div>
|
||||
<div style="height: 260px;">
|
||||
<Chart type="bar" :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Breakdown table (com ações) -->
|
||||
<div class="mt-4">
|
||||
<Card>
|
||||
<template #title>Distribuição por plano</template>
|
||||
<template #content>
|
||||
<DataTable :value="breakdown" stripedRows responsiveLayout="scroll" emptyMessage="Sem dados para exibir.">
|
||||
<Column field="plan_key" header="Plano" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ data.plan_key }}</span>
|
||||
<small class="text-color-secondary">
|
||||
{{ data.plan_active ? 'ativo no catálogo' : 'inativo no catálogo' }}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-3">Distribuição por plano</div>
|
||||
<DataTable :value="breakdown" stripedRows responsiveLayout="scroll" emptyMessage="Sem dados para exibir.">
|
||||
<Column field="plan_key" header="Plano" style="min-width: 14rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ data.plan_key }}</span>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">
|
||||
{{ data.plan_active ? 'ativo no catálogo' : 'inativo no catálogo' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Público" style="width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<Tag
|
||||
:value="planTargetLabel(data.plan_target)"
|
||||
:severity="planTargetSeverity(data.plan_target)"
|
||||
rounded
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Público" style="width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<Tag
|
||||
:value="planTargetLabel(data.plan_target)"
|
||||
:severity="planTargetSeverity(data.plan_target)"
|
||||
rounded
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Ativas" style="width: 8rem">
|
||||
<template #body="{ data }">{{ data.active_count }}</template>
|
||||
</Column>
|
||||
<Column header="Ativas" style="width: 8rem">
|
||||
<template #body="{ data }">{{ data.active_count }}</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Canceladas" style="width: 10rem">
|
||||
<template #body="{ data }">{{ data.canceled_count }}</template>
|
||||
</Column>
|
||||
<Column header="Canceladas" style="width: 10rem">
|
||||
<template #body="{ data }">{{ data.canceled_count }}</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Preço (ref.)" style="min-width: 12rem">
|
||||
<template #body="{ data }">{{ moneyBRLFromCents(data.price_cents) }}</template>
|
||||
</Column>
|
||||
<Column header="Preço (ref.)" style="min-width: 12rem">
|
||||
<template #body="{ data }">{{ moneyBRLFromCents(data.price_cents) }}</template>
|
||||
</Column>
|
||||
|
||||
<Column :header="intervalView === 'year' ? 'ARR' : 'MRR'" style="min-width: 12rem">
|
||||
<template #body="{ data }">{{ moneyBRLFromCents(data.revenue_cents) }}</template>
|
||||
</Column>
|
||||
<Column :header="intervalView === 'year' ? 'ARR' : 'MRR'" style="min-width: 12rem">
|
||||
<template #body="{ data }">{{ moneyBRLFromCents(data.revenue_cents) }}</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Ações" style="width: 16rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex gap-2 justify-end flex-wrap">
|
||||
<Button
|
||||
label="Abrir vitrine"
|
||||
icon="pi pi-external-link"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
:disabled="!data.plan_key || data.plan_key === '(sem plano)'"
|
||||
@click="openPlanPublic(data.plan_key)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
:disabled="!data.plan_key || data.plan_key === '(sem plano)'"
|
||||
v-tooltip.top="'Abrir catálogo interno do plano'"
|
||||
@click="openPlanCatalog(data.plan_key)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
<Column header="Ações" style="width: 16rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex gap-2 justify-end flex-wrap">
|
||||
<Button
|
||||
label="Abrir vitrine"
|
||||
icon="pi pi-external-link"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
:disabled="!data.plan_key || data.plan_key === '(sem plano)'"
|
||||
@click="openPlanPublic(data.plan_key)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
:disabled="!data.plan_key || data.plan_key === '(sem plano)'"
|
||||
v-tooltip.top="'Abrir catálogo interno do plano'"
|
||||
@click="openPlanCatalog(data.plan_key)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<div class="text-color-secondary text-sm mt-3">
|
||||
Nota: "Preço (ref.)" e "MRR/ARR" são normalizados usando o preço ativo.
|
||||
Se só existir anual, MRR = anual/12; se só existir mensal, ARR = mensal*12.
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-3">
|
||||
Nota: "Preço (ref.)" e "MRR/ARR" são normalizados usando o preço ativo.
|
||||
Se só existir anual, MRR = anual/12; se só existir mensal, ARR = mensal*12.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Hero */
|
||||
.dash-hero {
|
||||
position: sticky;
|
||||
top: var(--layout-sticky-top, 56px);
|
||||
z-index: 20;
|
||||
overflow: hidden;
|
||||
border-radius: 1rem;
|
||||
margin: 1rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: linear-gradient(135deg, var(--surface-card) 0%, var(--surface-section) 100%);
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, .08);
|
||||
}
|
||||
.dash-hero--stuck {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.dash-hero__blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: .15;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dash-hero__blob--1 {
|
||||
width: 220px; height: 220px;
|
||||
top: -60px; right: 80px;
|
||||
background: radial-gradient(circle, #2dd4bf, transparent 70%);
|
||||
}
|
||||
.dash-hero__blob--2 {
|
||||
width: 160px; height: 160px;
|
||||
bottom: -40px; right: 260px;
|
||||
background: radial-gradient(circle, #60a5fa, transparent 70%);
|
||||
}
|
||||
.dash-hero__icon {
|
||||
width: 2.75rem; height: 2.75rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: .75rem;
|
||||
background: var(--primary-100, rgba(99,102,241,.1));
|
||||
color: var(--primary-color, #6366f1);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user