carousel, agenda arquivados, agenda cor, agenda arquivados, grupos pacientes, pacientes arquivados - desativados, sessoes verificadas, ajuste notificações, Prontuario, Agenda Animation, Menu Profile, bagdes Profile, Offline
This commit is contained in:
@@ -156,7 +156,7 @@ function isEnabled (planId, featureId) {
|
||||
|
||||
/**
|
||||
* ✅ Toggle agora NÃO salva no banco.
|
||||
* Apenas altera o estado local (links) e marca como “pendente”.
|
||||
* Apenas altera o estado local (links) e marca como "pendente".
|
||||
*/
|
||||
function toggleLocal (planId, featureId, nextValue) {
|
||||
if (loading.value || saving.value) return
|
||||
|
||||
@@ -433,91 +433,91 @@ onBeforeUnmount(() => {
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Sentinel -->
|
||||
<div ref=”heroSentinelRef” class=”h-px” />
|
||||
<div ref="heroSentinelRef" class="h-px" />
|
||||
|
||||
<!-- Hero sticky -->
|
||||
<div
|
||||
ref=”heroEl”
|
||||
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)' }”
|
||||
ref="heroEl"
|
||||
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=”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 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>
|
||||
|
||||
<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)]”>Planos e preços</div>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-0.5”>Catálogo de planos do SaaS.</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)]">Planos e preços</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Catálogo de planos do SaaS.</div>
|
||||
</div>
|
||||
|
||||
<!-- Ações desktop (≥ 1200px) -->
|
||||
<div class=”hidden xl:flex items-center gap-2 flex-wrap”>
|
||||
<div class="hidden xl:flex items-center gap-2 flex-wrap">
|
||||
<SelectButton
|
||||
v-model=”targetFilter”
|
||||
:options=”targetFilterOptions”
|
||||
optionLabel=”label”
|
||||
optionValue=”value”
|
||||
size=”small”
|
||||
v-model="targetFilter"
|
||||
:options="targetFilterOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
size="small"
|
||||
/>
|
||||
<Button label=”Atualizar” icon=”pi pi-refresh” severity=”secondary” outlined size=”small” :loading=”loading” :disabled=”saving” @click=”fetchAll” />
|
||||
<Button label=”Adicionar plano” icon=”pi pi-plus” size=”small” :disabled=”saving” @click=”openCreate” />
|
||||
<Button label="Atualizar" icon="pi pi-refresh" severity="secondary" outlined size="small" :loading="loading" :disabled="saving" @click="fetchAll" />
|
||||
<Button label="Adicionar plano" icon="pi pi-plus" size="small" :disabled="saving" @click="openCreate" />
|
||||
</div>
|
||||
|
||||
<!-- Ações mobile (< 1200px) -->
|
||||
<div class=”flex xl:hidden”>
|
||||
<div class="flex xl:hidden">
|
||||
<Button
|
||||
label=”Ações”
|
||||
icon=”pi pi-ellipsis-v”
|
||||
severity=”warn”
|
||||
size=”small”
|
||||
aria-haspopup=”true”
|
||||
aria-controls=”plans_hero_menu”
|
||||
@click=”(e) => heroMenuRef.toggle(e)”
|
||||
label="Ações"
|
||||
icon="pi pi-ellipsis-v"
|
||||
severity="warn"
|
||||
size="small"
|
||||
aria-haspopup="true"
|
||||
aria-controls="plans_hero_menu"
|
||||
@click="(e) => heroMenuRef.toggle(e)"
|
||||
/>
|
||||
<Menu ref=”heroMenuRef” id=”plans_hero_menu” :model=”heroMenuItems” :popup=”true” />
|
||||
<Menu ref="heroMenuRef" id="plans_hero_menu" :model="heroMenuItems" :popup="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- content -->
|
||||
<div class=”px-3 md:px-4 pb-8 flex flex-col gap-4”>
|
||||
<DataTable :value=”filteredRows” dataKey=”id” :loading=”loading” stripedRows responsiveLayout=”scroll”>
|
||||
<Column field=”name” header=”Nome” sortable style=”min-width: 14rem” />
|
||||
<Column field=”key” header=”Key” sortable />
|
||||
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
|
||||
<DataTable :value="filteredRows" dataKey="id" :loading="loading" stripedRows responsiveLayout="scroll">
|
||||
<Column field="name" header="Nome" sortable style="min-width: 14rem" />
|
||||
<Column field="key" header="Key" sortable />
|
||||
|
||||
<Column field=”target” header=”Público” sortable style=”width: 10rem”>
|
||||
<template #body=”{ data }”>
|
||||
<span class=”font-medium”>{{ formatTargetLabel(data.target) }}</span>
|
||||
<Column field="target" header="Público" sortable style="width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<span class="font-medium">{{ formatTargetLabel(data.target) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header=”Mensal” sortable style=”width: 12rem”>
|
||||
<template #body=”{ data }”>
|
||||
<span class=”font-medium”>{{ formatBRLFromCents(data.monthly_cents) }}</span>
|
||||
<Column header="Mensal" sortable style="width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<span class="font-medium">{{ formatBRLFromCents(data.monthly_cents) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header=”Anual” sortable style=”width: 12rem”>
|
||||
<template #body=”{ data }”>
|
||||
<span class=”font-medium”>{{ formatBRLFromCents(data.yearly_cents) }}</span>
|
||||
<Column header="Anual" sortable style="width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<span class="font-medium">{{ formatBRLFromCents(data.yearly_cents) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column v-if=”hasCreatedAt” field=”created_at” header=”Criado em” sortable />
|
||||
<Column v-if="hasCreatedAt" field="created_at" header="Criado em" sortable />
|
||||
|
||||
<Column header=”Ações” style=”width: 12rem”>
|
||||
<template #body=”{ data }”>
|
||||
<div class=”flex gap-2”>
|
||||
<Button icon=”pi pi-pencil” severity=”secondary” outlined @click=”openEdit(data)” />
|
||||
<Column header="Ações" style="width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex gap-2">
|
||||
<Button icon="pi pi-pencil" severity="secondary" outlined @click="openEdit(data)" />
|
||||
<Button
|
||||
icon=”pi pi-trash”
|
||||
severity=”danger”
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
outlined
|
||||
:disabled=”isDeleteLockedRow(data)”
|
||||
:title=”isDeleteLockedRow(data) ? 'Plano padrão do sistema não pode ser removido.' : 'Excluir plano'”
|
||||
@click=”askDelete(data)”
|
||||
:disabled="isDeleteLockedRow(data)"
|
||||
:title="isDeleteLockedRow(data) ? 'Plano padrão do sistema não pode ser removido.' : 'Excluir plano'"
|
||||
@click="askDelete(data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -526,137 +526,137 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:visible=”showDlg”
|
||||
v-model:visible="showDlg"
|
||||
modal
|
||||
:draggable=”false”
|
||||
:header=”isEdit ? 'Editar plano' : 'Novo plano'”
|
||||
:style=”{ width: '620px' }”
|
||||
:draggable="false"
|
||||
:header="isEdit ? 'Editar plano' : 'Novo plano'"
|
||||
:style="{ width: '620px' }"
|
||||
>
|
||||
<div class=”flex flex-col gap-4”>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label class=”block mb-2”>Público do plano</label>
|
||||
<label class="block mb-2">Público do plano</label>
|
||||
<SelectButton
|
||||
v-model=”form.target”
|
||||
:options=”targetOptions”
|
||||
optionLabel=”label”
|
||||
optionValue=”value”
|
||||
class=”w-full”
|
||||
:disabled=”isTargetLocked || saving”
|
||||
v-model="form.target"
|
||||
:options="targetOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
:disabled="isTargetLocked || saving"
|
||||
/>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-1”>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
|
||||
Planos já existentes não mudam de público. Isso evita inconsistência no catálogo.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FloatLabel variant=”on” class=”w-full”>
|
||||
<IconField class=”w-full”>
|
||||
<InputIcon class=”pi pi-tag” />
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-tag" />
|
||||
<InputText
|
||||
v-model=”form.key”
|
||||
id=”plan_key”
|
||||
class=”w-full pr-10”
|
||||
variant=”filled”
|
||||
placeholder=”ex.: clinic_pro”
|
||||
:disabled=”(isCorePlanEditing || saving)”
|
||||
@blur=”form.key = slugifyKey(form.key)”
|
||||
v-model="form.key"
|
||||
id="plan_key"
|
||||
class="w-full pr-10"
|
||||
variant="filled"
|
||||
placeholder="ex.: clinic_pro"
|
||||
:disabled="(isCorePlanEditing || saving)"
|
||||
@blur="form.key = slugifyKey(form.key)"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”plan_key”>Key</label>
|
||||
<label for="plan_key">Key</label>
|
||||
</FloatLabel>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] -mt-3”>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] -mt-3">
|
||||
Key é técnica e estável (slug). Planos padrão do sistema têm a key protegida.
|
||||
</div>
|
||||
|
||||
<FloatLabel variant=”on” class=”w-full”>
|
||||
<IconField class=”w-full”>
|
||||
<InputIcon class=”pi pi-bookmark” />
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-bookmark" />
|
||||
<InputText
|
||||
v-model=”form.name”
|
||||
id=”plan_name”
|
||||
class=”w-full pr-10”
|
||||
variant=”filled”
|
||||
placeholder=”ex.: Clínica PRO”
|
||||
:disabled=”saving”
|
||||
v-model="form.name"
|
||||
id="plan_name"
|
||||
class="w-full pr-10"
|
||||
variant="filled"
|
||||
placeholder="ex.: Clínica PRO"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”plan_name”>Nome</label>
|
||||
<label for="plan_name">Nome</label>
|
||||
</FloatLabel>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] -mt-3”>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] -mt-3">
|
||||
Nome interno para administração. (Nome público vem de <b>plan_public</b>.)
|
||||
</div>
|
||||
|
||||
<div class=”grid grid-cols-1 md:grid-cols-2 gap-4”>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<FloatLabel variant=”on” class=”w-full”>
|
||||
<IconField class=”w-full”>
|
||||
<InputIcon class=”pi pi-money-bill” />
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-money-bill" />
|
||||
<InputNumber
|
||||
v-model=”form.price_monthly”
|
||||
inputId=”price_monthly”
|
||||
class=”w-full”
|
||||
inputClass=”w-full pr-10”
|
||||
variant=”filled”
|
||||
mode=”decimal”
|
||||
:minFractionDigits=”2”
|
||||
:maxFractionDigits=”2”
|
||||
placeholder=”ex.: 49,90”
|
||||
:disabled=”saving”
|
||||
v-model="form.price_monthly"
|
||||
inputId="price_monthly"
|
||||
class="w-full"
|
||||
inputClass="w-full pr-10"
|
||||
variant="filled"
|
||||
mode="decimal"
|
||||
:minFractionDigits="2"
|
||||
:maxFractionDigits="2"
|
||||
placeholder="ex.: 49,90"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”price_monthly”>Preço mensal (R$)</label>
|
||||
<label for="price_monthly">Preço mensal (R$)</label>
|
||||
</FloatLabel>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-1”>Deixe vazio para “sem preço definido”.</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">Deixe vazio para "sem preço definido".</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FloatLabel variant=”on” class=”w-full”>
|
||||
<IconField class=”w-full”>
|
||||
<InputIcon class=”pi pi-calendar” />
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-calendar" />
|
||||
<InputNumber
|
||||
v-model=”form.price_yearly”
|
||||
inputId=”price_yearly”
|
||||
class=”w-full”
|
||||
inputClass=”w-full pr-10”
|
||||
variant=”filled”
|
||||
mode=”decimal”
|
||||
:minFractionDigits=”2”
|
||||
:maxFractionDigits=”2”
|
||||
placeholder=”ex.: 490,00”
|
||||
:disabled=”saving”
|
||||
v-model="form.price_yearly"
|
||||
inputId="price_yearly"
|
||||
class="w-full"
|
||||
inputClass="w-full pr-10"
|
||||
variant="filled"
|
||||
mode="decimal"
|
||||
:minFractionDigits="2"
|
||||
:maxFractionDigits="2"
|
||||
placeholder="ex.: 490,00"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”price_yearly”>Preço anual (R$)</label>
|
||||
<label for="price_yearly">Preço anual (R$)</label>
|
||||
</FloatLabel>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-1”>Deixe vazio para “sem preço definido”.</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">Deixe vazio para "sem preço definido".</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- max_supervisees: só para planos de supervisor -->
|
||||
<div v-if=”form.target === 'supervisor'”>
|
||||
<FloatLabel variant=”on” class=”w-full”>
|
||||
<IconField class=”w-full”>
|
||||
<InputIcon class=”pi pi-users” />
|
||||
<div v-if="form.target === 'supervisor'">
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-users" />
|
||||
<InputNumber
|
||||
v-model=”form.max_supervisees”
|
||||
inputId=”max_supervisees”
|
||||
class=”w-full”
|
||||
inputClass=”w-full pr-10”
|
||||
variant=”filled”
|
||||
:useGrouping=”false”
|
||||
:min=”1”
|
||||
placeholder=”ex.: 3”
|
||||
:disabled=”saving”
|
||||
v-model="form.max_supervisees"
|
||||
inputId="max_supervisees"
|
||||
class="w-full"
|
||||
inputClass="w-full pr-10"
|
||||
variant="filled"
|
||||
:useGrouping="false"
|
||||
:min="1"
|
||||
placeholder="ex.: 3"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”max_supervisees”>Limite de supervisionados</label>
|
||||
<label for="max_supervisees">Limite de supervisionados</label>
|
||||
</FloatLabel>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-1”>Número máximo de terapeutas que podem ser supervisionados neste plano.</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">Número máximo de terapeutas que podem ser supervisionados neste plano.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label=”Cancelar” severity=”secondary” outlined @click=”showDlg = false” :disabled=”saving” />
|
||||
<Button :label=”isEdit ? 'Salvar' : 'Criar'” icon=”pi pi-check” :loading=”saving” @click=”save” />
|
||||
<Button label="Cancelar" severity="secondary" outlined @click="showDlg = false" :disabled="saving" />
|
||||
<Button :label="isEdit ? 'Salvar' : 'Criar'" icon="pi pi-check" :loading="saving" @click="save" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -48,7 +48,7 @@ const targetOptions = [
|
||||
const previewPricePolicy = ref('hide') // 'hide' | 'consult'
|
||||
const previewPolicyOptions = [
|
||||
{ label: 'Ocultar sem preço', value: 'hide' },
|
||||
{ label: 'Mostrar “Sob consulta”', value: 'consult' }
|
||||
{ label: 'Mostrar "Sob consulta"', value: 'consult' }
|
||||
]
|
||||
|
||||
function normalizeTarget (row) {
|
||||
@@ -450,148 +450,148 @@ onBeforeUnmount(() => {
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Sentinel -->
|
||||
<div ref=”heroSentinelRef” class=”h-px” />
|
||||
<div ref="heroSentinelRef" class="h-px" />
|
||||
|
||||
<!-- Hero sticky -->
|
||||
<div
|
||||
ref=”heroEl”
|
||||
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)' }”
|
||||
ref="heroEl"
|
||||
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=”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-emerald-400/10” />
|
||||
<div class=”absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-indigo-400/10” />
|
||||
<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-emerald-400/10" />
|
||||
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-indigo-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)]”>Vitrine de Planos</div>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-0.5”>Configure como os planos aparecem na página pública.</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)]">Vitrine de Planos</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Configure como os planos aparecem na página pública.</div>
|
||||
</div>
|
||||
|
||||
<!-- Ações desktop (≥ 1200px) -->
|
||||
<div class=”hidden xl:flex items-center gap-2 flex-wrap”>
|
||||
<SelectButton v-model=”targetFilter” :options=”targetOptions” optionLabel=”label” optionValue=”value” size=”small” :disabled=”loading || saving || bulletSaving” />
|
||||
<Button label=”Recarregar” icon=”pi pi-refresh” severity=”secondary” outlined size=”small” :loading=”loading” :disabled=”saving || bulletSaving” @click=”fetchAll” />
|
||||
<div class="hidden xl:flex items-center gap-2 flex-wrap">
|
||||
<SelectButton v-model="targetFilter" :options="targetOptions" optionLabel="label" optionValue="value" size="small" :disabled="loading || saving || bulletSaving" />
|
||||
<Button label="Recarregar" icon="pi pi-refresh" severity="secondary" outlined size="small" :loading="loading" :disabled="saving || bulletSaving" @click="fetchAll" />
|
||||
</div>
|
||||
|
||||
<!-- Ações mobile (< 1200px) -->
|
||||
<div class=”flex xl:hidden”>
|
||||
<div class="flex xl:hidden">
|
||||
<Button
|
||||
label=”Ações”
|
||||
icon=”pi pi-ellipsis-v”
|
||||
severity=”warn”
|
||||
size=”small”
|
||||
aria-haspopup=”true”
|
||||
aria-controls=”showcase_hero_menu”
|
||||
@click=”(e) => heroMenuRef.toggle(e)”
|
||||
label="Ações"
|
||||
icon="pi pi-ellipsis-v"
|
||||
severity="warn"
|
||||
size="small"
|
||||
aria-haspopup="true"
|
||||
aria-controls="showcase_hero_menu"
|
||||
@click="(e) => heroMenuRef.toggle(e)"
|
||||
/>
|
||||
<Menu ref=”heroMenuRef” id=”showcase_hero_menu” :model=”heroMenuItems” :popup=”true” />
|
||||
<Menu ref="heroMenuRef" id="showcase_hero_menu" :model="heroMenuItems" :popup="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- content -->
|
||||
<div class=”px-3 md:px-4 pb-8 flex flex-col gap-4”>
|
||||
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
|
||||
|
||||
<!-- Search -->
|
||||
<div>
|
||||
<FloatLabel variant=”on” class=”w-full md:w-80”>
|
||||
<IconField class=”w-full”>
|
||||
<InputIcon class=”pi pi-search” />
|
||||
<InputText v-model=”q” id=”plans_public_search” class=”w-full pr-10” variant=”filled” :disabled=”loading || saving || bulletSaving” />
|
||||
<FloatLabel variant="on" class="w-full md:w-80">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="q" id="plans_public_search" class="w-full pr-10" variant="filled" :disabled="loading || saving || bulletSaving" />
|
||||
</IconField>
|
||||
<label for=”plans_public_search”>Buscar plano</label>
|
||||
<label for="plans_public_search">Buscar plano</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Popover global (reutilizado) -->
|
||||
<Popover ref=”bulletsPop”>
|
||||
<div class=”w-[340px] max-w-[80vw]”>
|
||||
<div class=”text-[1rem] font-semibold mb-2”>{{ popPlanTitle }}</div>
|
||||
<Popover ref="bulletsPop">
|
||||
<div class="w-[340px] max-w-[80vw]">
|
||||
<div class="text-[1rem] font-semibold mb-2">{{ popPlanTitle }}</div>
|
||||
|
||||
<div v-if=”!popBullets?.length” class=”text-[1rem] text-[var(--text-color-secondary)]”>
|
||||
<div v-if="!popBullets?.length" class="text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Nenhum benefício configurado.
|
||||
</div>
|
||||
|
||||
<ul v-else class=”m-0 pl-4 space-y-2”>
|
||||
<li v-for=”b in popBullets” :key=”b.id” class=”text-[1rem] leading-snug”>
|
||||
<span :class=”b.highlight ? 'font-semibold' : ''”>
|
||||
<ul v-else class="m-0 pl-4 space-y-2">
|
||||
<li v-for="b in popBullets" :key="b.id" class="text-[1rem] leading-snug">
|
||||
<span :class="b.highlight ? 'font-semibold' : ''">
|
||||
{{ b.text }}
|
||||
</span>
|
||||
<div v-if=”b.highlight” class=”inline ml-2 text-[1rem] text-[var(--text-color-secondary)]”>(destaque)</div>
|
||||
<div v-if="b.highlight" class="inline ml-2 text-[1rem] text-[var(--text-color-secondary)]">(destaque)</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<DataTable :value=”tableRows” dataKey=”plan_id” :loading=”loading” stripedRows responsiveLayout=”scroll”>
|
||||
<Column header=”Plano” style=”min-width: 18rem”>
|
||||
<template #body=”{ data }”>
|
||||
<div class=”flex flex-col”>
|
||||
<span class=”font-semibold”>{{ data.public_name || data.plan_name || data.plan_key }}</span>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)]”>
|
||||
<DataTable :value="tableRows" dataKey="plan_id" :loading="loading" stripedRows responsiveLayout="scroll">
|
||||
<Column header="Plano" style="min-width: 18rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-semibold">{{ data.public_name || data.plan_name || data.plan_key }}</span>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">
|
||||
{{ data.plan_key }} • {{ data.plan_name || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header=”Público” style=”width: 10rem”>
|
||||
<template #body=”{ data }”>
|
||||
<Tag :value=”targetLabel(normalizeTarget(data))” :severity=”targetSeverity(normalizeTarget(data))” rounded />
|
||||
<Column header="Público" style="width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="targetLabel(normalizeTarget(data))" :severity="targetSeverity(normalizeTarget(data))" rounded />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header=”Mensal” style=”width: 12rem”>
|
||||
<template #body=”{ data }”>
|
||||
<span class=”font-medium”>{{ formatBRLFromCents(data.monthly_cents) }}</span>
|
||||
<Column header="Mensal" style="width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<span class="font-medium">{{ formatBRLFromCents(data.monthly_cents) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header=”Anual” style=”width: 12rem”>
|
||||
<template #body=”{ data }”>
|
||||
<span class=”font-medium”>{{ formatBRLFromCents(data.yearly_cents) }}</span>
|
||||
<Column header="Anual" style="width: 12rem">
|
||||
<template #body="{ data }">
|
||||
<span class="font-medium">{{ formatBRLFromCents(data.yearly_cents) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field=”badge” header=”Badge” style=”min-width: 12rem” />
|
||||
<Column field="badge" header="Badge" style="min-width: 12rem" />
|
||||
|
||||
<Column header=”Visível” style=”width: 8rem”>
|
||||
<template #body=”{ data }”>
|
||||
<Column header="Visível" style="width: 8rem">
|
||||
<template #body="{ data }">
|
||||
<span>{{ data.is_visible ? 'Sim' : 'Não' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header=”Destaque” style=”width: 9rem”>
|
||||
<template #body=”{ data }”>
|
||||
<Column header="Destaque" style="width: 9rem">
|
||||
<template #body="{ data }">
|
||||
<span>{{ data.is_featured ? 'Sim' : 'Não' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field=”sort_order” header=”Ordem” style=”width: 8rem” />
|
||||
<Column field="sort_order" header="Ordem" style="width: 8rem" />
|
||||
|
||||
<Column header=”Ações” style=”width: 14rem”>
|
||||
<template #body=”{ data }”>
|
||||
<div class=”flex gap-2 justify-end”>
|
||||
<Column header="Ações" style="width: 14rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button
|
||||
severity=”secondary”
|
||||
severity="secondary"
|
||||
outlined
|
||||
size=”small”
|
||||
:disabled=”loading || saving || bulletSaving”
|
||||
@click=”(e) => openBulletsPopover(e, data)”
|
||||
size="small"
|
||||
:disabled="loading || saving || bulletSaving"
|
||||
@click="(e) => openBulletsPopover(e, data)"
|
||||
>
|
||||
<i class=”pi pi-list mr-2” />
|
||||
<span class=”font-medium”>{{ data.bullets?.length || 0 }}</span>
|
||||
<i class="pi pi-list mr-2" />
|
||||
<span class="font-medium">{{ data.bullets?.length || 0 }}</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon=”pi pi-pencil”
|
||||
severity=”secondary”
|
||||
icon="pi pi-pencil"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size=”small”
|
||||
:disabled=”loading || saving || bulletSaving”
|
||||
@click=”openEdit(data)”
|
||||
size="small"
|
||||
:disabled="loading || saving || bulletSaving"
|
||||
@click="openEdit(data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -599,53 +599,53 @@ onBeforeUnmount(() => {
|
||||
</DataTable>
|
||||
|
||||
<!-- PREVIEW PÚBLICO (conceitual) -->
|
||||
<div class=”rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden”>
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
<!-- Hero -->
|
||||
<div class=”relative p-6 md:p-10”>
|
||||
<div class=”absolute inset-0 opacity-40 pointer-events-none bg-[radial-gradient(ellipse_at_top,rgba(16,185,129,0.18),transparent_55%)]” />
|
||||
<div class=”relative”>
|
||||
<div class=”flex flex-col md:flex-row md:items-end md:justify-between gap-6”>
|
||||
<div class=”max-w-2xl”>
|
||||
<div class=”flex items-center gap-2 mb-3 flex-wrap”>
|
||||
<div class="relative p-6 md:p-10">
|
||||
<div class="absolute inset-0 opacity-40 pointer-events-none bg-[radial-gradient(ellipse_at_top,rgba(16,185,129,0.18),transparent_55%)]" />
|
||||
<div class="relative">
|
||||
<div class="flex flex-col md:flex-row md:items-end md:justify-between gap-6">
|
||||
<div class="max-w-2xl">
|
||||
<div class="flex items-center gap-2 mb-3 flex-wrap">
|
||||
<Tag
|
||||
:value=”targetFilter === 'all' ? 'Vitrine (Todos)' : `Vitrine (${targetLabel(targetFilter)})`”
|
||||
:severity=”targetFilter === 'therapist' ? 'success' : (targetFilter === 'clinic' ? 'info' : 'secondary')”
|
||||
:value="targetFilter === 'all' ? 'Vitrine (Todos)' : `Vitrine (${targetLabel(targetFilter)})`"
|
||||
:severity="targetFilter === 'therapist' ? 'success' : (targetFilter === 'clinic' ? 'info' : 'secondary')"
|
||||
rounded
|
||||
/>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)]”>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Ajuste nomes, descrições, badges e benefícios — e veja o resultado aqui.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=”text-3xl md:text-5xl font-semibold leading-tight”>
|
||||
<div class="text-3xl md:text-5xl font-semibold leading-tight">
|
||||
Um plano não é preço.<br />
|
||||
É promessa organizada.
|
||||
</div>
|
||||
|
||||
<div class=”text-[var(--text-color-secondary)] mt-3”>
|
||||
<div class="text-[var(--text-color-secondary)] mt-3">
|
||||
A vitrine é o lugar onde o produto deixa de ser tabela e vira escolha.
|
||||
Clareza, contraste e uma hierarquia que guia o olhar — sem ruído.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=”flex flex-col items-start md:items-end gap-4”>
|
||||
<div class=”flex flex-col gap-2”>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)]”>Cobrança</div>
|
||||
<div class="flex flex-col items-start md:items-end gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Cobrança</div>
|
||||
<SelectButton
|
||||
v-model=”billingInterval”
|
||||
:options=”intervalOptions”
|
||||
optionLabel=”label”
|
||||
optionValue=”value”
|
||||
v-model="billingInterval"
|
||||
:options="intervalOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=”flex flex-col gap-2”>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)]”>Planos sem preço</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Planos sem preço</div>
|
||||
<SelectButton
|
||||
v-model=”previewPricePolicy”
|
||||
:options=”previewPolicyOptions”
|
||||
optionLabel=”label”
|
||||
optionValue=”value”
|
||||
v-model="previewPricePolicy"
|
||||
:options="previewPolicyOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -654,101 +654,101 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<!-- Cards -->
|
||||
<div class=”p-6 md:p-10 pt-0”>
|
||||
<div v-if=”!previewPlans.length” class=”text-[1rem] text-[var(--text-color-secondary)]”>
|
||||
<div class="p-6 md:p-10 pt-0">
|
||||
<div v-if="!previewPlans.length" class="text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Nenhum plano visível para este filtro.
|
||||
</div>
|
||||
|
||||
<div v-else class=”mt-6 grid grid-cols-1 md:grid-cols-3 gap-6”>
|
||||
<div v-else class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for=”p in previewPlans”
|
||||
:key=”p.plan_id”
|
||||
:class=”[
|
||||
v-for="p in previewPlans"
|
||||
:key="p.plan_id"
|
||||
:class="[
|
||||
'relative rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden',
|
||||
'shadow-sm transition-transform',
|
||||
p.is_featured ? 'md:-translate-y-2 md:scale-[1.02] ring-1 ring-emerald-500/25' : ''
|
||||
]”
|
||||
]"
|
||||
>
|
||||
<div class=”h-2 w-full opacity-50 bg-[var(--surface-100)]” />
|
||||
<div class="h-2 w-full opacity-50 bg-[var(--surface-100)]" />
|
||||
|
||||
<div class=”p-6”>
|
||||
<div class=”flex items-center justify-between gap-3”>
|
||||
<div class=”flex items-center gap-2 flex-wrap”>
|
||||
<Tag :value=”targetLabel(normalizeTarget(p))” :severity=”targetSeverity(normalizeTarget(p))” rounded />
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<Tag :value="targetLabel(normalizeTarget(p))" :severity="targetSeverity(normalizeTarget(p))" rounded />
|
||||
<Tag
|
||||
v-if=”p.badge || p.is_featured”
|
||||
:value=”p.badge || 'Destaque'”
|
||||
:severity=”p.is_featured ? 'success' : 'secondary'”
|
||||
v-if="p.badge || p.is_featured"
|
||||
:value="p.badge || 'Destaque'"
|
||||
:severity="p.is_featured ? 'success' : 'secondary'"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)]”>{{ p.plan_key }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">{{ p.plan_key }}</div>
|
||||
</div>
|
||||
|
||||
<div class=”mt-4”>
|
||||
<template v-if=”priceDisplayForPreview(p).kind === 'paid'”>
|
||||
<div class=”text-4xl font-semibold leading-none”>
|
||||
<div class="mt-4">
|
||||
<template v-if="priceDisplayForPreview(p).kind === 'paid'">
|
||||
<div class="text-4xl font-semibold leading-none">
|
||||
{{ priceDisplayForPreview(p).main }}
|
||||
</div>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-1”>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
|
||||
{{ priceDisplayForPreview(p).sub }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if=”priceDisplayForPreview(p).kind === 'free'”>
|
||||
<div class=”text-4xl font-semibold leading-none”>
|
||||
<template v-else-if="priceDisplayForPreview(p).kind === 'free'">
|
||||
<div class="text-4xl font-semibold leading-none">
|
||||
{{ priceDisplayForPreview(p).main }}
|
||||
</div>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-1”>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
|
||||
{{ billingInterval === 'year' ? 'no anual' : 'no mensal' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class=”text-2xl font-semibold leading-none”>
|
||||
<div class="text-2xl font-semibold leading-none">
|
||||
{{ priceDisplayForPreview(p).main }}
|
||||
</div>
|
||||
<div class=”text-[1rem] text-[var(--text-color-secondary)] mt-1”>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">
|
||||
Fale com a equipe para montar o plano ideal.
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class=”text-[var(--text-color-secondary)] mt-3 min-h-[44px]”>
|
||||
<div class="text-[var(--text-color-secondary)] mt-3 min-h-[44px]">
|
||||
{{ p.public_description || '—' }}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class=”mt-5 w-full”
|
||||
:label=”p.is_featured ? 'Começar agora' : 'Selecionar plano'”
|
||||
:severity=”p.is_featured ? 'success' : 'secondary'”
|
||||
:outlined=”!p.is_featured”
|
||||
class="mt-5 w-full"
|
||||
:label="p.is_featured ? 'Começar agora' : 'Selecionar plano'"
|
||||
:severity="p.is_featured ? 'success' : 'secondary'"
|
||||
:outlined="!p.is_featured"
|
||||
/>
|
||||
|
||||
<div class=”mt-6”>
|
||||
<div class=”border-t border-dashed border-[var(--surface-border)]” />
|
||||
<div class="mt-6">
|
||||
<div class="border-t border-dashed border-[var(--surface-border)]" />
|
||||
</div>
|
||||
|
||||
<ul v-if=”p.bullets?.length” class=”mt-4 space-y-2”>
|
||||
<li v-for=”b in p.bullets” :key=”b.id” class=”flex items-start gap-2”>
|
||||
<i class=”pi pi-check mt-1 text-[1rem] text-[var(--text-color-secondary)]”></i>
|
||||
<span :class=”['text-[1rem] leading-snug', b.highlight ? 'font-semibold' : '']”>
|
||||
<ul v-if="p.bullets?.length" class="mt-4 space-y-2">
|
||||
<li v-for="b in p.bullets" :key="b.id" class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-[1rem] text-[var(--text-color-secondary)]"></i>
|
||||
<span :class="['text-[1rem] leading-snug', b.highlight ? 'font-semibold' : '']">
|
||||
{{ b.text }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-else class=”mt-4 text-[1rem] text-[var(--text-color-secondary)]”>
|
||||
<div v-else class="mt-4 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Nenhum benefício configurado.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if=”previewPricePolicy === 'hide'” class=”mt-6 text-[1rem] text-[var(--text-color-secondary)]”>
|
||||
<div v-if="previewPricePolicy === 'hide'" class="mt-6 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Observação: planos sem preço não aparecem no preview (política atual).
|
||||
Para exibir como “Sob consulta”, mude acima.
|
||||
Para exibir como "Sob consulta", mude acima.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -757,90 +757,90 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- Dialog principal (✅ sem drag: removemos draggable) -->
|
||||
<Dialog
|
||||
v-model:visible=”showDlg”
|
||||
v-model:visible="showDlg"
|
||||
modal
|
||||
header=”Editar vitrine”
|
||||
:style=”{ width: '820px' }”
|
||||
:closable=”!saving”
|
||||
:dismissableMask=”!saving”
|
||||
:draggable=”false”
|
||||
header="Editar vitrine"
|
||||
:style="{ width: '820px' }"
|
||||
:closable="!saving"
|
||||
:dismissableMask="!saving"
|
||||
:draggable="false"
|
||||
>
|
||||
<div class=”grid grid-cols-1 md:grid-cols-2 gap-6”>
|
||||
<div class=”flex flex-col gap-4”>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- ✅ Nome público (FloatLabel + Icon) -->
|
||||
<FloatLabel variant=”on”>
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class=”pi pi-tag” />
|
||||
<InputIcon class="pi pi-tag" />
|
||||
<InputText
|
||||
id=”pp-public-name”
|
||||
v-model.trim=”form.public_name”
|
||||
class=”w-full”
|
||||
variant=”filled”
|
||||
:disabled=”saving”
|
||||
autocomplete=”off”
|
||||
id="pp-public-name"
|
||||
v-model.trim="form.public_name"
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
:disabled="saving"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
@keydown.enter.prevent=”save”
|
||||
@keydown.enter.prevent="save"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”pp-public-name”>Nome público *</label>
|
||||
<label for="pp-public-name">Nome público *</label>
|
||||
</FloatLabel>
|
||||
|
||||
<!-- ✅ Descrição pública -->
|
||||
<FloatLabel variant=”on”>
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class=”pi pi-align-left” />
|
||||
<InputIcon class="pi pi-align-left" />
|
||||
<Textarea
|
||||
id=”pp-public-desc”
|
||||
v-model.trim=”form.public_description”
|
||||
class=”w-full”
|
||||
rows=”3”
|
||||
id="pp-public-desc"
|
||||
v-model.trim="form.public_description"
|
||||
class="w-full"
|
||||
rows="3"
|
||||
autoResize
|
||||
:disabled=”saving”
|
||||
:disabled="saving"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”pp-public-desc”>Descrição pública</label>
|
||||
<label for="pp-public-desc">Descrição pública</label>
|
||||
</FloatLabel>
|
||||
|
||||
<!-- ✅ Badge -->
|
||||
<FloatLabel variant=”on”>
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class=”pi pi-bookmark” />
|
||||
<InputIcon class="pi pi-bookmark" />
|
||||
<InputText
|
||||
id=”pp-badge”
|
||||
v-model.trim=”form.badge”
|
||||
class=”w-full”
|
||||
variant=”filled”
|
||||
:disabled=”saving”
|
||||
autocomplete=”off”
|
||||
@keydown.enter.prevent=”save”
|
||||
id="pp-badge"
|
||||
v-model.trim="form.badge"
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
:disabled="saving"
|
||||
autocomplete="off"
|
||||
@keydown.enter.prevent="save"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”pp-badge”>Badge (opcional)</label>
|
||||
<label for="pp-badge">Badge (opcional)</label>
|
||||
</FloatLabel>
|
||||
|
||||
<div class=”grid grid-cols-1 md:grid-cols-2 gap-4”>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- ✅ Ordem -->
|
||||
<FloatLabel variant=”on”>
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class=”pi pi-sort-amount-up-alt” />
|
||||
<InputIcon class="pi pi-sort-amount-up-alt" />
|
||||
<InputNumber
|
||||
id=”pp-sort”
|
||||
v-model=”form.sort_order”
|
||||
class=”w-full”
|
||||
inputClass=”w-full”
|
||||
:disabled=”saving”
|
||||
id="pp-sort"
|
||||
v-model="form.sort_order"
|
||||
class="w-full"
|
||||
inputClass="w-full"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”pp-sort”>Ordem</label>
|
||||
<label for="pp-sort">Ordem</label>
|
||||
</FloatLabel>
|
||||
|
||||
<div class=”flex flex-col gap-3 pt-2”>
|
||||
<div class=”flex items-center gap-2”>
|
||||
<Checkbox v-model=”form.is_visible” :binary=”true” :disabled=”saving” />
|
||||
<div class="flex flex-col gap-3 pt-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="form.is_visible" :binary="true" :disabled="saving" />
|
||||
<label>Visível no público</label>
|
||||
</div>
|
||||
<div class=”flex items-center gap-2”>
|
||||
<Checkbox v-model=”form.is_featured” :binary=”true” :disabled=”saving” />
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="form.is_featured" :binary="true" :disabled="saving" />
|
||||
<label>Destaque</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -849,24 +849,24 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- bullets -->
|
||||
<div>
|
||||
<div class=”flex items-center justify-between mb-3”>
|
||||
<div class=”font-semibold”>Benefícios (bullets)</div>
|
||||
<Button label=”Adicionar” icon=”pi pi-plus” size=”small” :disabled=”saving || bulletSaving” @click=”openBulletCreate” />
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="font-semibold">Benefícios (bullets)</div>
|
||||
<Button label="Adicionar" icon="pi pi-plus" size="small" :disabled="saving || bulletSaving" @click="openBulletCreate" />
|
||||
</div>
|
||||
|
||||
<DataTable :value=”bullets” dataKey=”id” stripedRows responsiveLayout=”scroll”>
|
||||
<Column field=”text” header=”Texto” />
|
||||
<Column field=”sort_order” header=”Ordem” style=”width: 7rem” />
|
||||
<Column header=”Destaque” style=”width: 8rem”>
|
||||
<template #body=”{ data }”>
|
||||
<DataTable :value="bullets" dataKey="id" stripedRows responsiveLayout="scroll">
|
||||
<Column field="text" header="Texto" />
|
||||
<Column field="sort_order" header="Ordem" style="width: 7rem" />
|
||||
<Column header="Destaque" style="width: 8rem">
|
||||
<template #body="{ data }">
|
||||
<span>{{ data.highlight ? 'Sim' : 'Não' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header=”Ações” style=”width: 9rem”>
|
||||
<template #body=”{ data }”>
|
||||
<div class=”flex gap-2”>
|
||||
<Button icon=”pi pi-pencil” severity=”secondary” outlined size=”small” :disabled=”saving || bulletSaving” @click=”openBulletEdit(data)” />
|
||||
<Button icon=”pi pi-trash” severity=”danger” outlined size=”small” :disabled=”saving || bulletSaving” @click=”askDeleteBullet(data)” />
|
||||
<Column header="Ações" style="width: 9rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex gap-2">
|
||||
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" :disabled="saving || bulletSaving" @click="openBulletEdit(data)" />
|
||||
<Button icon="pi pi-trash" severity="danger" outlined size="small" :disabled="saving || bulletSaving" @click="askDeleteBullet(data)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
@@ -875,62 +875,62 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label=”Cancelar” severity=”secondary” outlined :disabled=”saving” @click=”showDlg = false” />
|
||||
<Button label=”Salvar” icon=”pi pi-check” :loading=”saving” @click=”save” />
|
||||
<Button label="Cancelar" severity="secondary" outlined :disabled="saving" @click="showDlg = false" />
|
||||
<Button label="Salvar" icon="pi pi-check" :loading="saving" @click="save" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Dialog bullet (✅ sem drag + inputs padronizados) -->
|
||||
<Dialog
|
||||
v-model:visible=”showBulletDlg”
|
||||
v-model:visible="showBulletDlg"
|
||||
modal
|
||||
:header=”bulletIsEdit ? 'Editar benefício' : 'Novo benefício'”
|
||||
:style=”{ width: '560px' }”
|
||||
:closable=”!bulletSaving”
|
||||
:dismissableMask=”!bulletSaving”
|
||||
:draggable=”false”
|
||||
:header="bulletIsEdit ? 'Editar benefício' : 'Novo benefício'"
|
||||
:style="{ width: '560px' }"
|
||||
:closable="!bulletSaving"
|
||||
:dismissableMask="!bulletSaving"
|
||||
:draggable="false"
|
||||
>
|
||||
<div class=”flex flex-col gap-4”>
|
||||
<FloatLabel variant=”on”>
|
||||
<div class="flex flex-col gap-4">
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class=”pi pi-list” />
|
||||
<InputIcon class="pi pi-list" />
|
||||
<Textarea
|
||||
id=”pp-bullet-text”
|
||||
v-model.trim=”bulletForm.text”
|
||||
class=”w-full”
|
||||
rows=”3”
|
||||
id="pp-bullet-text"
|
||||
v-model.trim="bulletForm.text"
|
||||
class="w-full"
|
||||
rows="3"
|
||||
autoResize
|
||||
:disabled=”bulletSaving”
|
||||
:disabled="bulletSaving"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”pp-bullet-text”>Texto *</label>
|
||||
<label for="pp-bullet-text">Texto *</label>
|
||||
</FloatLabel>
|
||||
|
||||
<div class=”grid grid-cols-1 md:grid-cols-2 gap-4”>
|
||||
<FloatLabel variant=”on”>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class=”pi pi-sort-numeric-up” />
|
||||
<InputIcon class="pi pi-sort-numeric-up" />
|
||||
<InputNumber
|
||||
id=”pp-bullet-order”
|
||||
v-model=”bulletForm.sort_order”
|
||||
class=”w-full”
|
||||
inputClass=”w-full”
|
||||
:disabled=”bulletSaving”
|
||||
id="pp-bullet-order"
|
||||
v-model="bulletForm.sort_order"
|
||||
class="w-full"
|
||||
inputClass="w-full"
|
||||
:disabled="bulletSaving"
|
||||
/>
|
||||
</IconField>
|
||||
<label for=”pp-bullet-order”>Ordem</label>
|
||||
<label for="pp-bullet-order">Ordem</label>
|
||||
</FloatLabel>
|
||||
|
||||
<div class=”flex items-center gap-2 pt-7”>
|
||||
<Checkbox v-model=”bulletForm.highlight” :binary=”true” :disabled=”bulletSaving” />
|
||||
<div class="flex items-center gap-2 pt-7">
|
||||
<Checkbox v-model="bulletForm.highlight" :binary="true" :disabled="bulletSaving" />
|
||||
<label>Destaque</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label=”Cancelar” severity=”secondary” outlined :disabled=”bulletSaving” @click=”showBulletDlg = false” />
|
||||
<Button :label=”bulletIsEdit ? 'Salvar' : 'Criar'” icon=”pi pi-check” :loading=”bulletSaving” @click=”saveBullet” />
|
||||
<Button label="Cancelar" severity="secondary" outlined :disabled="bulletSaving" @click="showBulletDlg = false" />
|
||||
<Button :label="bulletIsEdit ? 'Salvar' : 'Criar'" icon="pi pi-check" :loading="bulletSaving" @click="saveBullet" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user