feat: configuração de landing pages por domínio e generalização da galeria
This commit is contained in:
parent
8d33289a67
commit
fe24d381cd
@ -1,6 +1,6 @@
|
||||
class Api::V1::Accounts::LandingHostsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox, only: [:index, :create]
|
||||
before_action :fetch_landing_host, only: [:destroy]
|
||||
before_action :fetch_landing_host, only: [:update, :destroy]
|
||||
|
||||
def index
|
||||
@landing_hosts = LandingHost.where(inbox_id: @inbox.id)
|
||||
@ -17,6 +17,14 @@ class Api::V1::Accounts::LandingHostsController < Api::V1::Accounts::BaseControl
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @landing_host.update(landing_host_params)
|
||||
render json: @landing_host
|
||||
else
|
||||
render json: { error: @landing_host.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@landing_host.destroy!
|
||||
head :no_content
|
||||
@ -31,7 +39,7 @@ class Api::V1::Accounts::LandingHostsController < Api::V1::Accounts::BaseControl
|
||||
end
|
||||
|
||||
def fetch_landing_host
|
||||
# Garantimos que a pessoa só possa apagar LandingHosts de Inboxes que pertencem a ela
|
||||
# Garantimos que a pessoa só possa acessar/apagar LandingHosts de Inboxes que pertencem a ela
|
||||
valid_inbox_ids = Current.account.inboxes.pluck(:id)
|
||||
@landing_host = LandingHost.where(inbox_id: valid_inbox_ids).find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
@ -39,6 +47,11 @@ class Api::V1::Accounts::LandingHostsController < Api::V1::Accounts::BaseControl
|
||||
end
|
||||
|
||||
def landing_host_params
|
||||
params.require(:landing_host).permit(:hostname, :unit_code, :active, :auto_label)
|
||||
params.require(:landing_host).permit(
|
||||
:hostname, :unit_code, :active, :auto_label,
|
||||
:page_title, :page_subtitle, :button_text, :logo_url,
|
||||
:suite_image_url, :theme_color, :whatsapp_number,
|
||||
:initial_message, :default_source, :default_campanha
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,5 +1,23 @@
|
||||
class Public::LandingPagesController < PublicController
|
||||
layout false
|
||||
|
||||
def show; end
|
||||
def show
|
||||
host = request.host.to_s.sub(/^www\./, '')
|
||||
@landing_host = LandingHost.find_by(hostname: host, active: true)
|
||||
|
||||
# Fallback local para testes
|
||||
return unless Rails.env.development? && @landing_host.nil?
|
||||
|
||||
@landing_host = LandingHost.first || LandingHost.new(
|
||||
page_title: 'Atendimento Express',
|
||||
page_subtitle: 'Clique e fale direto com a recepcao agora',
|
||||
whatsapp_number: '556136131003',
|
||||
initial_message: 'Ola! Tenho interesse.',
|
||||
theme_color: '#27c15b',
|
||||
logo_url: 'https://iachat.hoteis1001noites.com.br/assets/images/dashboard/captain/logo.svg',
|
||||
unit_code: 'express',
|
||||
default_source: 'direto',
|
||||
default_campanha: 'site'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@ -13,6 +13,12 @@ export default {
|
||||
{ landing_host: data }
|
||||
);
|
||||
},
|
||||
updateHost(accountId, inboxId, id, data) {
|
||||
return axios.patch(
|
||||
`/api/v1/accounts/${accountId}/inboxes/${inboxId}/landing_hosts/${id}`,
|
||||
{ landing_host: data }
|
||||
);
|
||||
},
|
||||
deleteHost(accountId, inboxId, id) {
|
||||
return axios.delete(
|
||||
`/api/v1/accounts/${accountId}/inboxes/${inboxId}/landing_hosts/${id}`
|
||||
|
||||
@ -234,7 +234,8 @@
|
||||
"Ações"
|
||||
],
|
||||
"ADD_NEW_ITEM": "Adicione fotos na galeria",
|
||||
"NO_ITEMS_MESSAGE": "Ainda não há fotos cadastradas para envio automático aos clientes."
|
||||
"NO_ITEMS_MESSAGE": "Ainda não há fotos cadastradas para envio automático aos clientes.",
|
||||
"VIEW_URL": "URL da Imagem"
|
||||
},
|
||||
"DELETE": {
|
||||
"CONFIRM": {
|
||||
@ -274,14 +275,14 @@
|
||||
"SPECIFIC_HELP": "Essas fotos serão usadas somente na caixa de entrada {inbox}."
|
||||
},
|
||||
"SUITE_CATEGORY": {
|
||||
"LABEL": "Categoria da suíte",
|
||||
"LABEL": "Categoria",
|
||||
"PLACEHOLDER": "Ex: Hidromassagem",
|
||||
"ERROR": "A categoria é obrigatória"
|
||||
},
|
||||
"SUITE_NUMBER": {
|
||||
"LABEL": "Número/identificador da suíte",
|
||||
"LABEL": "Nome/identificador",
|
||||
"PLACEHOLDER": "Ex: 101",
|
||||
"ERROR": "O identificador da suíte é obrigatório"
|
||||
"ERROR": "O identificador é obrigatório"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Descrição da foto",
|
||||
@ -337,7 +338,7 @@
|
||||
"SELECT_INBOX_HINT": "Clique em uma caixa de entrada acima para ver e configurar os templates.",
|
||||
"EMPTY": {
|
||||
"TITLE": "Nenhum template configurado",
|
||||
"DESC": "Crie templates de mensagem automática para esta caixa de entrada."
|
||||
"DESC": "Configure as permissões das informações que o sistema utiliza. Por ex.: Quais imagens enviar durante as aproximações."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -98,11 +98,6 @@ const confirmDelete = async () => {
|
||||
>
|
||||
{{ t('CAPTAIN_SETTINGS.GALLERY.LIST.TABLE_HEADER[2]') }}
|
||||
</th>
|
||||
<th
|
||||
class="py-3 pr-4 text-left text-xs font-medium uppercase tracking-wider text-n-slate-10"
|
||||
>
|
||||
{{ t('CAPTAIN_SETTINGS.GALLERY.LIST.TABLE_HEADER[3]') }}
|
||||
</th>
|
||||
<th
|
||||
class="py-3 text-right text-xs font-medium uppercase tracking-wider text-n-slate-10"
|
||||
>
|
||||
@ -120,6 +115,14 @@ const confirmDelete = async () => {
|
||||
class="h-16 w-24 rounded object-cover"
|
||||
/>
|
||||
</td>
|
||||
<td class="py-4 pr-4">
|
||||
<p class="mb-0 font-medium text-n-slate-12">
|
||||
{{ item.suite_category }}
|
||||
</p>
|
||||
<p class="mb-0 text-xs text-n-slate-10">
|
||||
{{ item.suite_number }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="py-4 pr-4">
|
||||
<p class="mb-0 font-medium text-n-slate-12">
|
||||
{{
|
||||
@ -131,13 +134,19 @@ const confirmDelete = async () => {
|
||||
<p class="mb-0 text-xs text-n-slate-10">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
<div v-if="item.image_url" class="mt-1 flex items-center">
|
||||
<a
|
||||
:href="item.image_url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-1 break-all text-[11px] text-n-brand-9 hover:underline"
|
||||
>
|
||||
<i class="i-lucide-external-link size-3" />
|
||||
{{ t('CAPTAIN_SETTINGS.GALLERY.LIST.VIEW_URL') }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 pr-4 text-n-slate-11">
|
||||
{{ item.suite_category }}
|
||||
</td>
|
||||
<td class="py-4 pr-4 text-n-slate-11">
|
||||
{{ item.suite_number }}
|
||||
</td>
|
||||
|
||||
<td class="py-4">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
|
||||
@ -22,19 +22,23 @@ export default {
|
||||
newAutoLabel: '',
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
isUpdating: false,
|
||||
expandedHostId: null,
|
||||
editingHostData: {},
|
||||
labels: {
|
||||
title: 'Landing Pages (Tracking de Origem)',
|
||||
subtitle:
|
||||
'Defina os domínios de Landing Pages que enviam leads para esta caixa de entrada. O sistema usará esses domínios para identificar automaticamente a origem de cada conversa.',
|
||||
'Defina os domínios de Landing Pages para esta caixa de entrada e personalize a aparência de cada um.',
|
||||
loading: 'Carregando...',
|
||||
empty: 'Nenhum domínio cadastrado ainda.',
|
||||
colHostname: 'Hostname',
|
||||
colCode: 'Código / Unidade',
|
||||
colLabel: 'Etiqueta automática',
|
||||
colPublicLink: 'Página pública',
|
||||
colLabel: 'Etiqueta',
|
||||
colPublicLink: 'Link',
|
||||
remove: 'Remover',
|
||||
edit: 'Editar',
|
||||
open: 'Abrir',
|
||||
copy: 'Copiar link',
|
||||
copy: 'Copiar',
|
||||
addTitle: 'Adicionar Domínio',
|
||||
labelHostname: 'Hostname *',
|
||||
placeholderHostname: 'express.seuhotel.com.br',
|
||||
@ -53,6 +57,22 @@ export default {
|
||||
successDel: 'Domínio removido.',
|
||||
successCopy: 'Link copiado!',
|
||||
errCopy: 'Não foi possível copiar o link.',
|
||||
|
||||
// Novos campos
|
||||
cfgTitle: 'Editar Configurações de Aparência e Tracking',
|
||||
cfgPageTitle: 'Título Principal',
|
||||
cfgPageSubtitle: 'Subtítulo',
|
||||
cfgButtonText: 'Texto do Botão',
|
||||
cfgThemeColor: 'Cor do Botão (Ex: #25D366)',
|
||||
cfgLogoUrl: 'URL da Logo',
|
||||
cfgWhatsapp: 'Número de WhatsApp (5511999999999)',
|
||||
cfgMessage: 'Mensagem Inicial',
|
||||
cfgSource: 'Origem (UTM Source) Padrão',
|
||||
cfgCampaign: 'Campanha (UTM Campaign) Padrão',
|
||||
cfgSave: 'Salvar Alterações',
|
||||
cfgCancel: 'Cancelar',
|
||||
errUpdate: 'Erro ao salvar configurações.',
|
||||
successUpdate: 'Configurações atualizadas com sucesso!',
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -61,6 +81,9 @@ export default {
|
||||
addLabel() {
|
||||
return this.isSaving ? this.labels.labelSaving : this.labels.labelAdd;
|
||||
},
|
||||
updateLabel() {
|
||||
return this.isUpdating ? this.labels.labelSaving : this.labels.cfgSave;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchHosts();
|
||||
@ -84,7 +107,6 @@ export default {
|
||||
if (!this.newHostname.trim()) return;
|
||||
this.isSaving = true;
|
||||
|
||||
// Sanitiza: remove protocolo, www, barras e espaços
|
||||
const cleanHostname = this.newHostname
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
@ -114,6 +136,14 @@ export default {
|
||||
}
|
||||
},
|
||||
async deleteHost(id) {
|
||||
if (
|
||||
// eslint-disable-next-line no-alert
|
||||
!window.confirm(
|
||||
'Deseja realmente remover este domínio? A landing page parará de funcionar imediatamente.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await landingHostsApi.deleteHost(
|
||||
this.currentAccountId,
|
||||
@ -121,11 +151,43 @@ export default {
|
||||
id
|
||||
);
|
||||
this.landingHosts = this.landingHosts.filter(h => h.id !== id);
|
||||
this.expandedHostId = null;
|
||||
useAlert(this.labels.successDel);
|
||||
} catch {
|
||||
useAlert(this.labels.errDel);
|
||||
}
|
||||
},
|
||||
openEdit(host) {
|
||||
this.expandedHostId = host.id;
|
||||
this.editingHostData = { ...host };
|
||||
},
|
||||
cancelEdit() {
|
||||
this.expandedHostId = null;
|
||||
},
|
||||
async saveEdit() {
|
||||
this.isUpdating = true;
|
||||
try {
|
||||
const { data } = await landingHostsApi.updateHost(
|
||||
this.currentAccountId,
|
||||
this.inbox.id,
|
||||
this.expandedHostId,
|
||||
this.editingHostData
|
||||
);
|
||||
|
||||
// Atualiza na lista
|
||||
const index = this.landingHosts.findIndex(h => h.id === data.id);
|
||||
if (index !== -1) {
|
||||
this.landingHosts.splice(index, 1, data);
|
||||
}
|
||||
|
||||
this.expandedHostId = null;
|
||||
useAlert(this.labels.successUpdate);
|
||||
} catch {
|
||||
useAlert(this.labels.errUpdate);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
},
|
||||
landingUrl(hostname) {
|
||||
return `https://${hostname}/lp`;
|
||||
},
|
||||
@ -184,12 +246,8 @@ export default {
|
||||
<th class="px-4 py-3 text-right" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="host in landingHosts"
|
||||
:key="host.id"
|
||||
class="border-t border-n-slate-3 hover:bg-n-slate-1"
|
||||
>
|
||||
<tbody v-for="host in landingHosts" :key="host.id">
|
||||
<tr class="border-t border-n-slate-3 hover:bg-n-slate-1">
|
||||
<td class="px-4 py-3 font-mono text-n-slate-12 text-xs">
|
||||
{{ host.hostname }}
|
||||
</td>
|
||||
@ -218,19 +276,199 @@ export default {
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button
|
||||
class="text-xs text-n-brand hover:underline font-medium transition-colors"
|
||||
@click="openEdit(host)"
|
||||
>
|
||||
{{ labels.edit }}
|
||||
</button>
|
||||
<button
|
||||
class="text-xs text-ruby-9 hover:text-ruby-11 font-medium transition-colors"
|
||||
@click="deleteHost(host.id)"
|
||||
>
|
||||
{{ labels.remove }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Formulário de Edição Inline -->
|
||||
<tr v-if="expandedHostId === host.id" class="bg-n-brand-1/30">
|
||||
<td colspan="5" class="p-4 border-t border-n-brand-3">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 border border-n-brand-3 rounded-lg p-5 shadow-sm"
|
||||
>
|
||||
<h4 class="text-sm font-semibold text-n-slate-12 mb-4">
|
||||
{{ labels.cfgTitle }}
|
||||
</h4>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-4"
|
||||
>
|
||||
<!-- Básico -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.labelHostname }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.hostname"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.labelCode }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.unit_code"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.labelAutoLabel }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.auto_label"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Textos LP -->
|
||||
<div class="sm:col-span-2">
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgPageTitle }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.page_title"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgPageSubtitle }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.page_subtitle"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Botão e Aparência -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgButtonText }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.button_text"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgThemeColor }}</label
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="editingHostData.theme_color"
|
||||
type="color"
|
||||
class="h-9 w-10 p-1 border border-n-slate-3 rounded cursor-pointer"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="editingHostData.theme_color"
|
||||
class="flex-1 [&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgLogoUrl }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.logo_url"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- WhatsApp e Tracking -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgWhatsapp }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.whatsapp_number"
|
||||
type="tel"
|
||||
placeholder="5511999999999"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgMessage }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.initial_message"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgSource }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.default_source"
|
||||
placeholder="direto"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-medium text-n-slate-11 mb-1"
|
||||
>{{ labels.cfgCampaign }}</label
|
||||
>
|
||||
<woot-input
|
||||
v-model="editingHostData.default_campanha"
|
||||
placeholder="site"
|
||||
class="[&>input]:!mb-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 flex justify-end gap-3 border-t border-n-slate-2 pt-4"
|
||||
>
|
||||
<NextButton
|
||||
:label="labels.cfgCancel"
|
||||
variant="hollow"
|
||||
@click="cancelEdit"
|
||||
/>
|
||||
<NextButton
|
||||
:label="updateLabel"
|
||||
:disabled="isUpdating"
|
||||
@click="saveEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Formulário de adição -->
|
||||
<!-- Formulário de adição rápida (somente hostname e unit_code) -->
|
||||
<div class="border border-n-slate-3 rounded-lg p-4 bg-n-slate-1">
|
||||
<h3 class="text-sm font-semibold text-n-slate-12 mb-3">
|
||||
{{ labels.addTitle }}
|
||||
|
||||
@ -5,8 +5,19 @@
|
||||
# id :bigint not null, primary key
|
||||
# active :boolean
|
||||
# auto_label :string
|
||||
# button_text :string default("Ver disponibilidade agora")
|
||||
# custom_config :jsonb
|
||||
# default_campanha :string
|
||||
# default_source :string
|
||||
# hostname :string
|
||||
# initial_message :text
|
||||
# logo_url :string
|
||||
# page_subtitle :string default("Atendimento Imediato\nEntrada Discreta\nSem Burocracia")
|
||||
# page_title :string default("Atendimento Express")
|
||||
# suite_image_url :string
|
||||
# theme_color :string default("#25D366")
|
||||
# unit_code :string
|
||||
# whatsapp_number :string default("")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# inbox_id :integer
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
# user_agent :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# click_id :string
|
||||
# contact_id :integer
|
||||
# conversation_id :integer
|
||||
# inbox_id :integer
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Atendimento WhatsApp</title>
|
||||
<title><%= @landing_host&.page_title.presence || 'Atendimento WhatsApp' %></title>
|
||||
<link id="pageFavicon" rel="icon" type="image/png" href="<%= @landing_host&.logo_url || 'https://iachat.hoteis1001noites.com.br/assets/images/dashboard/captain/logo.svg' %>" />
|
||||
<style>
|
||||
:root {
|
||||
--bg-1: #040b18;
|
||||
@ -12,7 +13,7 @@
|
||||
--card-border: #1f2c43;
|
||||
--text-1: #e7ecf6;
|
||||
--text-2: #96a2b5;
|
||||
--btn: #27c15b;
|
||||
--btn: <%= @landing_host&.theme_color.presence || '#27c15b' %>;
|
||||
--btn-text: #f4fff7;
|
||||
}
|
||||
|
||||
@ -56,8 +57,6 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(40, 215, 122, 0.16);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.logo-wrap img {
|
||||
@ -107,210 +106,42 @@
|
||||
font-size: 14px;
|
||||
color: #647086;
|
||||
}
|
||||
|
||||
.admin-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(4, 10, 22, 0.78);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 18px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.admin-overlay.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.admin-modal {
|
||||
width: min(100%, 560px);
|
||||
max-height: 94vh;
|
||||
overflow: auto;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--card-border);
|
||||
background: #10192d;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.admin-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.admin-head h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
background: #1a2840;
|
||||
color: #c8d3e6;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: #8fa1bd;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #2a3b58;
|
||||
border-radius: 10px;
|
||||
background: #182338;
|
||||
color: #e4ecf8;
|
||||
padding: 10px 11px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.field textarea {
|
||||
min-height: 70px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin-top: 10px;
|
||||
background: #111b2d;
|
||||
border: 1px solid #253750;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
color: #c8d3e6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
flex: 1;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
padding: 11px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #27c15b;
|
||||
color: #f4fff7;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: #22324e;
|
||||
color: #d1daea;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<% if @landing_host.nil? %>
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
<div id="logoTapArea" class="logo-wrap" title="logo">
|
||||
<img id="logoImage" alt="logo" />
|
||||
<h1>Página não encontrada</h1>
|
||||
<p class="subtitle">Verifique a URL digitada e tente novamente.</p>
|
||||
</div>
|
||||
<h1 id="titleText"></h1>
|
||||
<p id="subtitleText" class="subtitle"></p>
|
||||
<button id="whatsButton" class="wa-button" type="button">Falar no WhatsApp</button>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
<div class="logo-wrap" title="logo">
|
||||
<img src="<%= @landing_host.logo_url.presence || 'https://iachat.hoteis1001noites.com.br/assets/images/dashboard/captain/logo.svg' %>" alt="logo" />
|
||||
</div>
|
||||
<h1><%= @landing_host.page_title %></h1>
|
||||
<p class="subtitle"><%= @landing_host.page_subtitle&.gsub("\n", "<br>")&.html_safe %></p>
|
||||
<button id="whatsButton" class="wa-button" type="button"><%= @landing_host.button_text.presence || 'Falar no WhatsApp' %></button>
|
||||
<div class="foot">Pagina segura · atendimento humano</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="adminOverlay" class="admin-overlay">
|
||||
<div class="admin-modal">
|
||||
<div class="admin-head">
|
||||
<h2>Painel Admin</h2>
|
||||
<button id="closeAdmin" class="close-btn" type="button">×</button>
|
||||
</div>
|
||||
|
||||
<div class="field"><label>Nome da unidade</label><input id="f_unit_name" /></div>
|
||||
<div class="field"><label>Titulo</label><input id="f_title" /></div>
|
||||
<div class="field"><label>Subtitulo</label><input id="f_subtitle" /></div>
|
||||
<div class="field"><label>Telefone WhatsApp</label><input id="f_phone" /></div>
|
||||
<div class="field"><label>Mensagem inicial (WhatsApp)</label><textarea id="f_message"></textarea></div>
|
||||
|
||||
<div class="field"><label>Source</label><input id="f_source" /></div>
|
||||
<div class="field"><label>Campanha</label><input id="f_campanha" /></div>
|
||||
<div class="field"><label>Unidade (tag)</label><input id="f_unidade" /></div>
|
||||
<div class="field"><label>Inbox (tag)</label><input id="f_inbox" /></div>
|
||||
<div class="field"><label>Cor do botao</label><input id="f_button_color" type="color" /></div>
|
||||
<div class="field"><label>Logo (URL ou base64)</label><input id="f_logo_url" /></div>
|
||||
<div class="field"><label>Enviar logo (arquivo)</label><input id="f_logo_file" type="file" accept="image/*" /></div>
|
||||
|
||||
<div class="preview">
|
||||
<strong>Mensagem que sera enviada:</strong>
|
||||
<div id="messagePreview"></div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="saveAdmin" class="save-btn" type="button">Salvar</button>
|
||||
<button id="resetAdmin" class="reset-btn" type="button">Resetar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const defaults = {
|
||||
unit_name: "Hotel",
|
||||
title: "Atendimento imediato no WhatsApp",
|
||||
subtitle: "Clique e fale direto com a recepcao agora",
|
||||
phone: "556136131003",
|
||||
message: "Ola! Tenho interesse.",
|
||||
source: "direto",
|
||||
campanha: "site",
|
||||
unidade: "express",
|
||||
inbox: "EXPRESS",
|
||||
button_color: "#27c15b",
|
||||
logo_url: "https://iachat.hoteis1001noites.com.br/assets/images/dashboard/captain/logo.svg",
|
||||
const config = {
|
||||
hostname: "<%= j @landing_host.hostname %>",
|
||||
phone: "<%= j @landing_host.whatsapp_number.to_s.gsub(/[^\d]/, '') %>",
|
||||
message: "<%= j @landing_host.initial_message.to_s %>",
|
||||
defaultSource: "<%= j @landing_host.default_source.to_s %>",
|
||||
defaultCampanha: "<%= j @landing_host.default_campanha.to_s %>",
|
||||
};
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const storageKey = "lp_config_" + window.location.hostname;
|
||||
const clickKey = "lp_click_id_" + window.location.hostname;
|
||||
|
||||
const toConfig = (raw) => {
|
||||
try {
|
||||
return { ...defaults, ...(JSON.parse(raw || "{}")) };
|
||||
} catch (_) {
|
||||
return { ...defaults };
|
||||
}
|
||||
};
|
||||
|
||||
let config = toConfig(localStorage.getItem(storageKey));
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const inputs = {
|
||||
unit_name: $("f_unit_name"),
|
||||
title: $("f_title"),
|
||||
subtitle: $("f_subtitle"),
|
||||
phone: $("f_phone"),
|
||||
message: $("f_message"),
|
||||
source: $("f_source"),
|
||||
campanha: $("f_campanha"),
|
||||
unidade: $("f_unidade"),
|
||||
inbox: $("f_inbox"),
|
||||
button_color: $("f_button_color"),
|
||||
logo_url: $("f_logo_url"),
|
||||
};
|
||||
|
||||
function getClickId() {
|
||||
const fromUrl = params.get("click_id") || params.get("clickid") || params.get("utm_id") || params.get("gclid");
|
||||
if (fromUrl) {
|
||||
@ -325,32 +156,16 @@
|
||||
}
|
||||
|
||||
function currentSource() {
|
||||
return params.get("utm_source") || params.get("source") || config.source || "direto";
|
||||
return params.get("utm_source") || params.get("source") || config.defaultSource || "direto";
|
||||
}
|
||||
|
||||
function currentCampanha() {
|
||||
return params.get("utm_campaign") || params.get("campanha") || config.campanha || "site";
|
||||
}
|
||||
|
||||
function whatsappText() {
|
||||
return config.message || "";
|
||||
}
|
||||
|
||||
function syncView() {
|
||||
$("titleText").textContent = config.title;
|
||||
$("subtitleText").textContent = config.subtitle;
|
||||
$("logoImage").src = config.logo_url;
|
||||
$("whatsButton").style.background = config.button_color || "#27c15b";
|
||||
$("messagePreview").textContent = whatsappText();
|
||||
|
||||
Object.keys(inputs).forEach((key) => {
|
||||
if (inputs[key]) inputs[key].value = config[key] || "";
|
||||
});
|
||||
return params.get("utm_campaign") || params.get("campanha") || config.defaultCampanha || "site";
|
||||
}
|
||||
|
||||
async function sendTrack() {
|
||||
const payload = {
|
||||
hostname: window.location.hostname,
|
||||
hostname: config.hostname || window.location.hostname,
|
||||
lp: window.location.href,
|
||||
click_id: getClickId(),
|
||||
source: currentSource(),
|
||||
@ -369,68 +184,13 @@
|
||||
|
||||
async function openWhatsapp() {
|
||||
await sendTrack();
|
||||
const phone = (config.phone || "").replace(/[^\d]/g, "");
|
||||
const text = encodeURIComponent(whatsappText());
|
||||
window.location.href = `https://wa.me/${phone}?text=${text}`;
|
||||
const text = encodeURIComponent(config.message);
|
||||
window.location.href = `https://wa.me/${config.phone}?text=${text}`;
|
||||
}
|
||||
|
||||
let logoTapCount = 0;
|
||||
let logoTapTimer = null;
|
||||
|
||||
$("logoTapArea").addEventListener("click", () => {
|
||||
logoTapCount += 1;
|
||||
clearTimeout(logoTapTimer);
|
||||
logoTapTimer = setTimeout(() => {
|
||||
logoTapCount = 0;
|
||||
}, 1500);
|
||||
|
||||
if (logoTapCount >= 5) {
|
||||
logoTapCount = 0;
|
||||
$("adminOverlay").classList.add("open");
|
||||
}
|
||||
});
|
||||
|
||||
$("closeAdmin").addEventListener("click", () => {
|
||||
$("adminOverlay").classList.remove("open");
|
||||
});
|
||||
|
||||
$("adminOverlay").addEventListener("click", (e) => {
|
||||
if (e.target === $("adminOverlay")) $("adminOverlay").classList.remove("open");
|
||||
});
|
||||
|
||||
Object.keys(inputs).forEach((key) => {
|
||||
if (!inputs[key]) return;
|
||||
inputs[key].addEventListener("input", () => {
|
||||
config[key] = inputs[key].value;
|
||||
syncView();
|
||||
});
|
||||
});
|
||||
|
||||
$("f_logo_file").addEventListener("change", (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
config.logo_url = String(reader.result || "");
|
||||
syncView();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
$("saveAdmin").addEventListener("click", () => {
|
||||
localStorage.setItem(storageKey, JSON.stringify(config));
|
||||
$("adminOverlay").classList.remove("open");
|
||||
});
|
||||
|
||||
$("resetAdmin").addEventListener("click", () => {
|
||||
localStorage.removeItem(storageKey);
|
||||
config = { ...defaults };
|
||||
syncView();
|
||||
});
|
||||
|
||||
$("whatsButton").addEventListener("click", openWhatsapp);
|
||||
syncView();
|
||||
document.getElementById("whatsButton").addEventListener("click", openWhatsapp);
|
||||
})();
|
||||
</script>
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -237,7 +237,7 @@ Rails.application.routes.draw do
|
||||
get :campaigns, on: :member
|
||||
get :agent_bot, on: :member
|
||||
post :set_agent_bot, on: :member
|
||||
resources :landing_hosts, only: [:index, :create, :destroy]
|
||||
resources :landing_hosts, only: [:index, :create, :update, :destroy]
|
||||
post :setup_channel_provider, on: :member
|
||||
post :disconnect_channel_provider, on: :member
|
||||
delete :avatar, on: :member
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
class AddVisualConfigToLandingHosts < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
end
|
||||
end
|
||||
8
db/migrate/20260303074000_add_config_to_landing_hosts.rb
Normal file
8
db/migrate/20260303074000_add_config_to_landing_hosts.rb
Normal file
@ -0,0 +1,8 @@
|
||||
class AddConfigToLandingHosts < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :landing_hosts, :initial_message, :text
|
||||
add_column :landing_hosts, :default_source, :string
|
||||
add_column :landing_hosts, :default_campanha, :string
|
||||
add_column :landing_hosts, :custom_config, :jsonb, default: {}
|
||||
end
|
||||
end
|
||||
13
db/schema.rb
13
db/schema.rb
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_03_02_211000) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_03_03_074000) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@ -1561,6 +1561,17 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_02_211000) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "auto_label"
|
||||
t.string "page_title", default: "Atendimento Express"
|
||||
t.string "page_subtitle", default: "Atendimento Imediato\nEntrada Discreta\nSem Burocracia"
|
||||
t.string "button_text", default: "Ver disponibilidade agora"
|
||||
t.string "logo_url"
|
||||
t.string "suite_image_url"
|
||||
t.string "theme_color", default: "#25D366"
|
||||
t.string "whatsapp_number", default: ""
|
||||
t.text "initial_message"
|
||||
t.string "default_source"
|
||||
t.string "default_campanha"
|
||||
t.jsonb "custom_config", default: {}
|
||||
t.index ["hostname"], name: "index_landing_hosts_on_hostname", unique: true
|
||||
end
|
||||
|
||||
|
||||
@ -6,10 +6,26 @@ Garantir que os cliques na landing page sejam rastreados, capturando UTMs (orige
|
||||
### Contexto
|
||||
Atualmente, o `TrackingController` e o `LeadClick` capturam o hostname e o IP, mas não estão salvando o `click_id` (enviado pelo frontend) nem extraindo parâmetros UTM da URL da landing page.
|
||||
|
||||
### Como testar:
|
||||
Na tela de chat, conferir se os Atributos `lp`, `campanha`, `origem` e `click_id` estão sendo preenchidos para novos contatos do WhatsApp criados através de LPs.
|
||||
|
||||
## Parte 2 - Multiunidade (Server-side LPs)
|
||||
- **Problema:** As Landing Pages dependiam do `localStorage` do navegador para manter configurações (nº de telefone, titulo, etc). Isso tornava impossível rastreamento persistente (quando o usuário trocava de celular) e não suportava estrutura multiunidade profissional.
|
||||
- **Implementação:**
|
||||
1. Feita migração adicionando à tabela `landing_hosts` os campos faltantes: `initial_message`, `default_source`, `default_campanha`, `custom_config`.
|
||||
2. Atualizamos `LandingPagesController` (Controller Backend) para carregar os dados baseando-se no `request.host`.
|
||||
3. Modificamos o HTML de exibição principal (`app/views/public/landing_pages/show.html.erb`), removendo painel de administração overlay e substituindo por ERB para renderização Server-Side (SSG).
|
||||
|
||||
## Em Andamento / Concluídos Recentes
|
||||
1. **Backend**: Atualizar o `TrackingController` para salvar `click_id` e extrair UTMs caso não sejam enviados explicitamente.
|
||||
2. Validar se o `click_id` está sendo recebido no Controller corretamente (testado via postman/local e aprovado - os testes do Sidekiq estão rodando no log).
|
||||
3. Criar painel no Dashboard para gerenciar `landing_hosts` (migrando configurações do front-end armazenadas no LocalStorage para Back-end em estrutura multi-unidade) -> **Concluído:** Criada interface em Inboxes > Configurações > Landing Pages.
|
||||
|
||||
### Próximos Passos
|
||||
1. **Migração**: Adicionar o campo `click_id` na tabela `lead_clicks`.
|
||||
2. **Backend**: Atualizar o `TrackingController` para salvar `click_id` e extrair UTMs caso não sejam enviados explicitamente.
|
||||
3. **Frontend**: Sugerir script JS para a landing page que capture UTMs da URL e as envie para o Supabase.
|
||||
- Adicionar interface "Painel Admin" para Gerenciar Landing Pages dentro das configurações originais do Chatwoot (Vue Frontend).
|
||||
- Conferir os Logs de tracking e Sidekiq em mensagens locais para debugar qualquer problema residual no `AttributionMatcherService`.
|
||||
|
||||
### Como Validar
|
||||
Simular um clique com UTMs:
|
||||
|
||||
0
progresso/landing_page_dinamica.md
Normal file
0
progresso/landing_page_dinamica.md
Normal file
20
test_landing_page.sh
Executable file
20
test_landing_page.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Script de teste rápido da Landing Page simulando tráfego
|
||||
|
||||
# Defina a URL base local, se o porto for 3000
|
||||
BASE_URL="http://localhost:3000/lp"
|
||||
|
||||
# Imprime o título de teste e a URL para clicar
|
||||
echo "---------------------------------------------------------"
|
||||
echo "Teste a sua Landing Page abrindo este link no navegador:"
|
||||
echo ""
|
||||
echo " 👉 $BASE_URL"
|
||||
echo ""
|
||||
echo "Teste também com UTMs simulando um clique de Anúncio:"
|
||||
echo ""
|
||||
echo " 👉 $BASE_URL?utm_source=meta&utm_medium=cpc&utm_campaign=black_friday"
|
||||
echo ""
|
||||
echo "Abra os links acima. Eles buscarão as configurações salvas"
|
||||
echo "pelo seu painel do Chatwoot (se houver configuração"
|
||||
echo "para 'localhost', ela aparecerá)."
|
||||
echo "---------------------------------------------------------"
|
||||
Loading…
Reference in New Issue
Block a user