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
|
class Api::V1::Accounts::LandingHostsController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_inbox, only: [:index, :create]
|
before_action :fetch_inbox, only: [:index, :create]
|
||||||
before_action :fetch_landing_host, only: [:destroy]
|
before_action :fetch_landing_host, only: [:update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@landing_hosts = LandingHost.where(inbox_id: @inbox.id)
|
@landing_hosts = LandingHost.where(inbox_id: @inbox.id)
|
||||||
@ -17,6 +17,14 @@ class Api::V1::Accounts::LandingHostsController < Api::V1::Accounts::BaseControl
|
|||||||
end
|
end
|
||||||
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
|
def destroy
|
||||||
@landing_host.destroy!
|
@landing_host.destroy!
|
||||||
head :no_content
|
head :no_content
|
||||||
@ -31,7 +39,7 @@ class Api::V1::Accounts::LandingHostsController < Api::V1::Accounts::BaseControl
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fetch_landing_host
|
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)
|
valid_inbox_ids = Current.account.inboxes.pluck(:id)
|
||||||
@landing_host = LandingHost.where(inbox_id: valid_inbox_ids).find(params[:id])
|
@landing_host = LandingHost.where(inbox_id: valid_inbox_ids).find(params[:id])
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
@ -39,6 +47,11 @@ class Api::V1::Accounts::LandingHostsController < Api::V1::Accounts::BaseControl
|
|||||||
end
|
end
|
||||||
|
|
||||||
def landing_host_params
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,23 @@
|
|||||||
class Public::LandingPagesController < PublicController
|
class Public::LandingPagesController < PublicController
|
||||||
layout false
|
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
|
end
|
||||||
|
|||||||
@ -13,6 +13,12 @@ export default {
|
|||||||
{ landing_host: data }
|
{ 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) {
|
deleteHost(accountId, inboxId, id) {
|
||||||
return axios.delete(
|
return axios.delete(
|
||||||
`/api/v1/accounts/${accountId}/inboxes/${inboxId}/landing_hosts/${id}`
|
`/api/v1/accounts/${accountId}/inboxes/${inboxId}/landing_hosts/${id}`
|
||||||
|
|||||||
@ -234,7 +234,8 @@
|
|||||||
"Ações"
|
"Ações"
|
||||||
],
|
],
|
||||||
"ADD_NEW_ITEM": "Adicione fotos na galeria",
|
"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": {
|
"DELETE": {
|
||||||
"CONFIRM": {
|
"CONFIRM": {
|
||||||
@ -274,14 +275,14 @@
|
|||||||
"SPECIFIC_HELP": "Essas fotos serão usadas somente na caixa de entrada {inbox}."
|
"SPECIFIC_HELP": "Essas fotos serão usadas somente na caixa de entrada {inbox}."
|
||||||
},
|
},
|
||||||
"SUITE_CATEGORY": {
|
"SUITE_CATEGORY": {
|
||||||
"LABEL": "Categoria da suíte",
|
"LABEL": "Categoria",
|
||||||
"PLACEHOLDER": "Ex: Hidromassagem",
|
"PLACEHOLDER": "Ex: Hidromassagem",
|
||||||
"ERROR": "A categoria é obrigatória"
|
"ERROR": "A categoria é obrigatória"
|
||||||
},
|
},
|
||||||
"SUITE_NUMBER": {
|
"SUITE_NUMBER": {
|
||||||
"LABEL": "Número/identificador da suíte",
|
"LABEL": "Nome/identificador",
|
||||||
"PLACEHOLDER": "Ex: 101",
|
"PLACEHOLDER": "Ex: 101",
|
||||||
"ERROR": "O identificador da suíte é obrigatório"
|
"ERROR": "O identificador é obrigatório"
|
||||||
},
|
},
|
||||||
"DESCRIPTION": {
|
"DESCRIPTION": {
|
||||||
"LABEL": "Descrição da foto",
|
"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.",
|
"SELECT_INBOX_HINT": "Clique em uma caixa de entrada acima para ver e configurar os templates.",
|
||||||
"EMPTY": {
|
"EMPTY": {
|
||||||
"TITLE": "Nenhum template configurado",
|
"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]') }}
|
{{ t('CAPTAIN_SETTINGS.GALLERY.LIST.TABLE_HEADER[2]') }}
|
||||||
</th>
|
</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
|
<th
|
||||||
class="py-3 text-right text-xs font-medium uppercase tracking-wider text-n-slate-10"
|
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"
|
class="h-16 w-24 rounded object-cover"
|
||||||
/>
|
/>
|
||||||
</td>
|
</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">
|
<td class="py-4 pr-4">
|
||||||
<p class="mb-0 font-medium text-n-slate-12">
|
<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">
|
<p class="mb-0 text-xs text-n-slate-10">
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
</p>
|
</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>
|
||||||
<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">
|
<td class="py-4">
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -22,19 +22,23 @@ export default {
|
|||||||
newAutoLabel: '',
|
newAutoLabel: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
|
isUpdating: false,
|
||||||
|
expandedHostId: null,
|
||||||
|
editingHostData: {},
|
||||||
labels: {
|
labels: {
|
||||||
title: 'Landing Pages (Tracking de Origem)',
|
title: 'Landing Pages (Tracking de Origem)',
|
||||||
subtitle:
|
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...',
|
loading: 'Carregando...',
|
||||||
empty: 'Nenhum domínio cadastrado ainda.',
|
empty: 'Nenhum domínio cadastrado ainda.',
|
||||||
colHostname: 'Hostname',
|
colHostname: 'Hostname',
|
||||||
colCode: 'Código / Unidade',
|
colCode: 'Código / Unidade',
|
||||||
colLabel: 'Etiqueta automática',
|
colLabel: 'Etiqueta',
|
||||||
colPublicLink: 'Página pública',
|
colPublicLink: 'Link',
|
||||||
remove: 'Remover',
|
remove: 'Remover',
|
||||||
|
edit: 'Editar',
|
||||||
open: 'Abrir',
|
open: 'Abrir',
|
||||||
copy: 'Copiar link',
|
copy: 'Copiar',
|
||||||
addTitle: 'Adicionar Domínio',
|
addTitle: 'Adicionar Domínio',
|
||||||
labelHostname: 'Hostname *',
|
labelHostname: 'Hostname *',
|
||||||
placeholderHostname: 'express.seuhotel.com.br',
|
placeholderHostname: 'express.seuhotel.com.br',
|
||||||
@ -53,6 +57,22 @@ export default {
|
|||||||
successDel: 'Domínio removido.',
|
successDel: 'Domínio removido.',
|
||||||
successCopy: 'Link copiado!',
|
successCopy: 'Link copiado!',
|
||||||
errCopy: 'Não foi possível copiar o link.',
|
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() {
|
addLabel() {
|
||||||
return this.isSaving ? this.labels.labelSaving : this.labels.labelAdd;
|
return this.isSaving ? this.labels.labelSaving : this.labels.labelAdd;
|
||||||
},
|
},
|
||||||
|
updateLabel() {
|
||||||
|
return this.isUpdating ? this.labels.labelSaving : this.labels.cfgSave;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchHosts();
|
this.fetchHosts();
|
||||||
@ -84,7 +107,6 @@ export default {
|
|||||||
if (!this.newHostname.trim()) return;
|
if (!this.newHostname.trim()) return;
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
|
|
||||||
// Sanitiza: remove protocolo, www, barras e espaços
|
|
||||||
const cleanHostname = this.newHostname
|
const cleanHostname = this.newHostname
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@ -114,6 +136,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteHost(id) {
|
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 {
|
try {
|
||||||
await landingHostsApi.deleteHost(
|
await landingHostsApi.deleteHost(
|
||||||
this.currentAccountId,
|
this.currentAccountId,
|
||||||
@ -121,11 +151,43 @@ export default {
|
|||||||
id
|
id
|
||||||
);
|
);
|
||||||
this.landingHosts = this.landingHosts.filter(h => h.id !== id);
|
this.landingHosts = this.landingHosts.filter(h => h.id !== id);
|
||||||
|
this.expandedHostId = null;
|
||||||
useAlert(this.labels.successDel);
|
useAlert(this.labels.successDel);
|
||||||
} catch {
|
} catch {
|
||||||
useAlert(this.labels.errDel);
|
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) {
|
landingUrl(hostname) {
|
||||||
return `https://${hostname}/lp`;
|
return `https://${hostname}/lp`;
|
||||||
},
|
},
|
||||||
@ -184,12 +246,8 @@ export default {
|
|||||||
<th class="px-4 py-3 text-right" />
|
<th class="px-4 py-3 text-right" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody v-for="host in landingHosts" :key="host.id">
|
||||||
<tr
|
<tr class="border-t border-n-slate-3 hover:bg-n-slate-1">
|
||||||
v-for="host in landingHosts"
|
|
||||||
:key="host.id"
|
|
||||||
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">
|
<td class="px-4 py-3 font-mono text-n-slate-12 text-xs">
|
||||||
{{ host.hostname }}
|
{{ host.hostname }}
|
||||||
</td>
|
</td>
|
||||||
@ -218,19 +276,199 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
<button
|
<div class="flex items-center justify-end gap-3">
|
||||||
class="text-xs text-ruby-9 hover:text-ruby-11 font-medium transition-colors"
|
<button
|
||||||
@click="deleteHost(host.id)"
|
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"
|
||||||
>
|
>
|
||||||
{{ labels.remove }}
|
<h4 class="text-sm font-semibold text-n-slate-12 mb-4">
|
||||||
</button>
|
{{ 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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<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">
|
<h3 class="text-sm font-semibold text-n-slate-12 mb-3">
|
||||||
{{ labels.addTitle }}
|
{{ labels.addTitle }}
|
||||||
|
|||||||
@ -2,14 +2,25 @@
|
|||||||
#
|
#
|
||||||
# Table name: landing_hosts
|
# Table name: landing_hosts
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# active :boolean
|
# active :boolean
|
||||||
# auto_label :string
|
# auto_label :string
|
||||||
# hostname :string
|
# button_text :string default("Ver disponibilidade agora")
|
||||||
# unit_code :string
|
# custom_config :jsonb
|
||||||
# created_at :datetime not null
|
# default_campanha :string
|
||||||
# updated_at :datetime not null
|
# default_source :string
|
||||||
# inbox_id :integer
|
# 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
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
# user_agent :string
|
# user_agent :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# click_id :string
|
||||||
# contact_id :integer
|
# contact_id :integer
|
||||||
# conversation_id :integer
|
# conversation_id :integer
|
||||||
# inbox_id :integer
|
# inbox_id :integer
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<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>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg-1: #040b18;
|
--bg-1: #040b18;
|
||||||
@ -12,7 +13,7 @@
|
|||||||
--card-border: #1f2c43;
|
--card-border: #1f2c43;
|
||||||
--text-1: #e7ecf6;
|
--text-1: #e7ecf6;
|
||||||
--text-2: #96a2b5;
|
--text-2: #96a2b5;
|
||||||
--btn: #27c15b;
|
--btn: <%= @landing_host&.theme_color.presence || '#27c15b' %>;
|
||||||
--btn-text: #f4fff7;
|
--btn-text: #f4fff7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,8 +57,6 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: rgba(40, 215, 122, 0.16);
|
background: rgba(40, 215, 122, 0.16);
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-wrap img {
|
.logo-wrap img {
|
||||||
@ -107,330 +106,91 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #647086;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="page">
|
<% if @landing_host.nil? %>
|
||||||
<div class="card">
|
<div class="page">
|
||||||
<div id="logoTapArea" class="logo-wrap" title="logo">
|
<div class="card">
|
||||||
<img id="logoImage" alt="logo" />
|
<h1>Página não encontrada</h1>
|
||||||
</div>
|
<p class="subtitle">Verifique a URL digitada e tente novamente.</p>
|
||||||
<h1 id="titleText"></h1>
|
</div>
|
||||||
<p id="subtitleText" class="subtitle"></p>
|
</div>
|
||||||
<button id="whatsButton" class="wa-button" type="button">Falar no WhatsApp</button>
|
<% else %>
|
||||||
<div class="foot">Pagina segura · atendimento humano</div>
|
<div class="page">
|
||||||
</div>
|
<div class="card">
|
||||||
</div>
|
<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 id="adminOverlay" class="admin-overlay">
|
</div>
|
||||||
<div class="admin-modal">
|
<h1><%= @landing_host.page_title %></h1>
|
||||||
<div class="admin-head">
|
<p class="subtitle"><%= @landing_host.page_subtitle&.gsub("\n", "<br>")&.html_safe %></p>
|
||||||
<h2>Painel Admin</h2>
|
<button id="whatsButton" class="wa-button" type="button"><%= @landing_host.button_text.presence || 'Falar no WhatsApp' %></button>
|
||||||
<button id="closeAdmin" class="close-btn" type="button">×</button>
|
<div class="foot">Pagina segura · atendimento humano</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const defaults = {
|
const config = {
|
||||||
unit_name: "Hotel",
|
hostname: "<%= j @landing_host.hostname %>",
|
||||||
title: "Atendimento imediato no WhatsApp",
|
phone: "<%= j @landing_host.whatsapp_number.to_s.gsub(/[^\d]/, '') %>",
|
||||||
subtitle: "Clique e fale direto com a recepcao agora",
|
message: "<%= j @landing_host.initial_message.to_s %>",
|
||||||
phone: "556136131003",
|
defaultSource: "<%= j @landing_host.default_source.to_s %>",
|
||||||
message: "Ola! Tenho interesse.",
|
defaultCampanha: "<%= j @landing_host.default_campanha.to_s %>",
|
||||||
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 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) {
|
|
||||||
localStorage.setItem(clickKey, fromUrl);
|
|
||||||
return fromUrl;
|
|
||||||
}
|
|
||||||
const existing = localStorage.getItem(clickKey);
|
|
||||||
if (existing) return existing;
|
|
||||||
const generated = `lp-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
|
||||||
localStorage.setItem(clickKey, generated);
|
|
||||||
return generated;
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentSource() {
|
|
||||||
return params.get("utm_source") || params.get("source") || config.source || "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] || "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendTrack() {
|
|
||||||
const payload = {
|
|
||||||
hostname: window.location.hostname,
|
|
||||||
lp: window.location.href,
|
|
||||||
click_id: getClickId(),
|
|
||||||
source: currentSource(),
|
|
||||||
campanha: currentCampanha(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const params = new URLSearchParams(window.location.search);
|
||||||
await fetch("/track/click", {
|
const clickKey = "lp_click_id_" + window.location.hostname;
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
keepalive: true,
|
|
||||||
});
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openWhatsapp() {
|
function getClickId() {
|
||||||
await sendTrack();
|
const fromUrl = params.get("click_id") || params.get("clickid") || params.get("utm_id") || params.get("gclid");
|
||||||
const phone = (config.phone || "").replace(/[^\d]/g, "");
|
if (fromUrl) {
|
||||||
const text = encodeURIComponent(whatsappText());
|
localStorage.setItem(clickKey, fromUrl);
|
||||||
window.location.href = `https://wa.me/${phone}?text=${text}`;
|
return fromUrl;
|
||||||
}
|
}
|
||||||
|
const existing = localStorage.getItem(clickKey);
|
||||||
let logoTapCount = 0;
|
if (existing) return existing;
|
||||||
let logoTapTimer = null;
|
const generated = `lp-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
||||||
|
localStorage.setItem(clickKey, generated);
|
||||||
$("logoTapArea").addEventListener("click", () => {
|
return generated;
|
||||||
logoTapCount += 1;
|
|
||||||
clearTimeout(logoTapTimer);
|
|
||||||
logoTapTimer = setTimeout(() => {
|
|
||||||
logoTapCount = 0;
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
if (logoTapCount >= 5) {
|
|
||||||
logoTapCount = 0;
|
|
||||||
$("adminOverlay").classList.add("open");
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$("closeAdmin").addEventListener("click", () => {
|
function currentSource() {
|
||||||
$("adminOverlay").classList.remove("open");
|
return params.get("utm_source") || params.get("source") || config.defaultSource || "direto";
|
||||||
});
|
}
|
||||||
|
|
||||||
$("adminOverlay").addEventListener("click", (e) => {
|
function currentCampanha() {
|
||||||
if (e.target === $("adminOverlay")) $("adminOverlay").classList.remove("open");
|
return params.get("utm_campaign") || params.get("campanha") || config.defaultCampanha || "site";
|
||||||
});
|
}
|
||||||
|
|
||||||
Object.keys(inputs).forEach((key) => {
|
async function sendTrack() {
|
||||||
if (!inputs[key]) return;
|
const payload = {
|
||||||
inputs[key].addEventListener("input", () => {
|
hostname: config.hostname || window.location.hostname,
|
||||||
config[key] = inputs[key].value;
|
lp: window.location.href,
|
||||||
syncView();
|
click_id: getClickId(),
|
||||||
});
|
source: currentSource(),
|
||||||
});
|
campanha: currentCampanha(),
|
||||||
|
};
|
||||||
|
|
||||||
$("f_logo_file").addEventListener("change", (e) => {
|
try {
|
||||||
const file = e.target.files && e.target.files[0];
|
await fetch("/track/click", {
|
||||||
if (!file) return;
|
method: "POST",
|
||||||
const reader = new FileReader();
|
headers: { "Content-Type": "application/json" },
|
||||||
reader.onload = () => {
|
body: JSON.stringify(payload),
|
||||||
config.logo_url = String(reader.result || "");
|
keepalive: true,
|
||||||
syncView();
|
});
|
||||||
};
|
} catch (_) {}
|
||||||
reader.readAsDataURL(file);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$("saveAdmin").addEventListener("click", () => {
|
async function openWhatsapp() {
|
||||||
localStorage.setItem(storageKey, JSON.stringify(config));
|
await sendTrack();
|
||||||
$("adminOverlay").classList.remove("open");
|
const text = encodeURIComponent(config.message);
|
||||||
});
|
window.location.href = `https://wa.me/${config.phone}?text=${text}`;
|
||||||
|
}
|
||||||
|
|
||||||
$("resetAdmin").addEventListener("click", () => {
|
document.getElementById("whatsButton").addEventListener("click", openWhatsapp);
|
||||||
localStorage.removeItem(storageKey);
|
})();
|
||||||
config = { ...defaults };
|
</script>
|
||||||
syncView();
|
<% end %>
|
||||||
});
|
|
||||||
|
|
||||||
$("whatsButton").addEventListener("click", openWhatsapp);
|
|
||||||
syncView();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -237,7 +237,7 @@ Rails.application.routes.draw do
|
|||||||
get :campaigns, on: :member
|
get :campaigns, on: :member
|
||||||
get :agent_bot, on: :member
|
get :agent_bot, on: :member
|
||||||
post :set_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 :setup_channel_provider, on: :member
|
||||||
post :disconnect_channel_provider, on: :member
|
post :disconnect_channel_provider, on: :member
|
||||||
delete :avatar, 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.
|
# 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
|
# These extensions should be enabled to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
enable_extension "pg_trgm"
|
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 "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "auto_label"
|
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
|
t.index ["hostname"], name: "index_landing_hosts_on_hostname", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,26 @@ Garantir que os cliques na landing page sejam rastreados, capturando UTMs (orige
|
|||||||
### Contexto
|
### 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.
|
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
|
### Próximos Passos
|
||||||
1. **Migração**: Adicionar o campo `click_id` na tabela `lead_clicks`.
|
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.
|
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
|
### Como Validar
|
||||||
Simular um clique com UTMs:
|
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