feat: remove Notificações Automáticas + ajusta assinatura WhatsApp
1. Captain — remove feature Notificações Automáticas (captain_notification_templates)
Feature órfã: 0 registros em prod, substituída pela Jornada do Cliente
(Lifecycle). Sem dependências fora dela própria. Removido:
- Vue: routes/settings/captain/notifications/ + entry no captain.routes.js
- Sidebar: item "Notifications" do Captain menu
- Store: modules captainNotificationTemplates + import no store/index
- API: api/captain/notificationTemplates.js
- Controller: api/v1/accounts/captain/notification_templates_controller
- Model: Captain::NotificationTemplate
- Job: enterprise/app/jobs/captain/notifications/
- Routes: resources :notification_templates no config/routes.rb
- i18n: chaves CAPTAIN_SETTINGS.NOTIFICATIONS + SIDEBAR.CAPTAIN_NOTIFICATIONS
em pt_BR e en (captain.json + settings.json)
Tabela captain_notification_templates mantida (0 rows, sem consumidores).
Se quiser drop, criar migration separada depois.
2. WhatsApp — tira colchetes do prefixo de assinatura
Era: *[ Jasmine(PrimeAL) ]*\ncontent
Agora: *Jasmine(PrimeAL)*\ncontent
Afeta wuzapi_service (outgoing) + incoming_message_wuzapi_service (echo)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c0b54c6783
commit
34d42dfbbd
@ -1,29 +0,0 @@
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class NotificationTemplatesAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('inboxes', { accountScoped: true });
|
||||
}
|
||||
|
||||
getAll(inboxId) {
|
||||
return this.get(`${inboxId}/notification_templates`);
|
||||
}
|
||||
|
||||
create(inboxId, data) {
|
||||
return this.post(`${inboxId}/notification_templates`, {
|
||||
notification_template: data,
|
||||
});
|
||||
}
|
||||
|
||||
update(inboxId, id, data) {
|
||||
return this.patch(`${inboxId}/notification_templates/${id}`, {
|
||||
notification_template: data,
|
||||
});
|
||||
}
|
||||
|
||||
delete(inboxId, id) {
|
||||
return this.delete(`${inboxId}/notification_templates/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotificationTemplatesAPI();
|
||||
@ -436,12 +436,6 @@ const menuItems = computed(() => {
|
||||
activeOn: ['captain_settings_reports'],
|
||||
to: accountScopedRoute('captain_settings_reports'),
|
||||
},
|
||||
{
|
||||
name: 'Notifications',
|
||||
label: t('SIDEBAR.CAPTAIN_NOTIFICATIONS'),
|
||||
activeOn: ['captain_settings_notifications'],
|
||||
to: accountScopedRoute('captain_settings_notifications'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -433,46 +433,6 @@
|
||||
"LABEL": "Available for agent sending"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS": {
|
||||
"TITLE": "Automatic Notifications",
|
||||
"DESCRIPTION": "Configure messages sent automatically before or after the guest's arrival.",
|
||||
"LOADING": "Loading notifications...",
|
||||
"ADD": "Add notification",
|
||||
"ACTIVE": "Active",
|
||||
"INACTIVE": "Inactive",
|
||||
"DIRECTION": {
|
||||
"BEFORE": "before",
|
||||
"AFTER": "after",
|
||||
"OF_ARRIVAL": "arrival"
|
||||
},
|
||||
"FORM": {
|
||||
"LABEL_PLACEHOLDER": "Template name (e.g. Arrival Instructions)",
|
||||
"CONTENT_PLACEHOLDER": "Message to send... Use {{guest_name}}, {{check_in_time}}, {{suite_name}}",
|
||||
"SEND": "Send",
|
||||
"MINUTES": "min",
|
||||
"CANCEL": "Cancel",
|
||||
"SAVE": "Save"
|
||||
},
|
||||
"CREATE": {
|
||||
"SUCCESS": "Notification created successfully!",
|
||||
"ERROR": "Error creating notification. Please try again."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Notification updated!",
|
||||
"ERROR": "Error updating notification."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Notification removed.",
|
||||
"ERROR": "Error removing notification."
|
||||
},
|
||||
"INBOX_LABEL": "Select inbox",
|
||||
"NO_CAPTAIN_INBOXES": "No inboxes with Captain configured.",
|
||||
"SELECT_INBOX_HINT": "Click an inbox above to view and configure its templates.",
|
||||
"EMPTY": {
|
||||
"TITLE": "No templates configured",
|
||||
"DESC": "Create automatic message templates for this inbox."
|
||||
}
|
||||
}
|
||||
},
|
||||
"CAPTAIN": {
|
||||
|
||||
@ -347,7 +347,6 @@
|
||||
"CAPTAIN_FUNNEL": "Conversion Funnel",
|
||||
"CAPTAIN_LIFECYCLE": "Customer Journey",
|
||||
"CAPTAIN_REPORTS": "AI Reports",
|
||||
"CAPTAIN_NOTIFICATIONS": "Automatic Notifications",
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Agents",
|
||||
"AGENT_BOTS": "Bots",
|
||||
|
||||
@ -434,47 +434,6 @@
|
||||
"LABEL": "Disponível para envio pelos agentes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS": {
|
||||
"TITLE": "Notificações Automáticas",
|
||||
"DESCRIPTION": "Configure mensagens automáticas enviadas antes ou depois da chegada do hóspede.",
|
||||
"LOADING": "Carregando notificações...",
|
||||
"ADD": "Adicionar notificação",
|
||||
"ACTIVE": "Ativo",
|
||||
"INACTIVE": "Inativo",
|
||||
"DIRECTION": {
|
||||
"BEFORE": "antes",
|
||||
"AFTER": "depois",
|
||||
"OF_ARRIVAL": "da chegada"
|
||||
},
|
||||
"TIMING_LABEL": "da chegada",
|
||||
"FORM": {
|
||||
"LABEL_PLACEHOLDER": "Nome do template (ex: Orientações de Chegada)",
|
||||
"CONTENT_PLACEHOLDER": "Mensagem a enviar... Use {{guest_name}}, {{check_in_time}}, {{suite_name}}",
|
||||
"SEND": "Enviar",
|
||||
"MINUTES": "min",
|
||||
"CANCEL": "Cancelar",
|
||||
"SAVE": "Salvar"
|
||||
},
|
||||
"CREATE": {
|
||||
"SUCCESS": "Notificação criada com sucesso!",
|
||||
"ERROR": "Erro ao criar notificação. Tente novamente."
|
||||
},
|
||||
"UPDATE": {
|
||||
"SUCCESS": "Notificação atualizada!",
|
||||
"ERROR": "Erro ao atualizar notificação."
|
||||
},
|
||||
"DELETE": {
|
||||
"SUCCESS": "Notificação removida.",
|
||||
"ERROR": "Erro ao remover notificação."
|
||||
},
|
||||
"INBOX_LABEL": "Selecione a caixa de entrada",
|
||||
"NO_CAPTAIN_INBOXES": "Nenhuma caixa de entrada com Captain configurado.",
|
||||
"SELECT_INBOX_HINT": "Clique em uma caixa de entrada acima para ver e configurar os templates.",
|
||||
"EMPTY": {
|
||||
"TITLE": "Nenhum template configurado",
|
||||
"DESC": "Configure as permissões das informações que o sistema utiliza. Por ex.: Quais imagens enviar durante as aproximações."
|
||||
}
|
||||
}
|
||||
},
|
||||
"CAPTAIN": {
|
||||
|
||||
@ -346,7 +346,6 @@
|
||||
"CAPTAIN_FUNNEL": "Funil de Conversão",
|
||||
"CAPTAIN_LIFECYCLE": "Jornada do Cliente",
|
||||
"CAPTAIN_REPORTS": "Relatórios IA",
|
||||
"CAPTAIN_NOTIFICATIONS": "Notificações Automáticas",
|
||||
"HOME": "Principal",
|
||||
"AGENTS": "Agentes",
|
||||
"AGENT_BOTS": "Robôs",
|
||||
|
||||
@ -8,7 +8,6 @@ import UnitEdit from './units/Edit.vue';
|
||||
import GalleryIndex from './gallery/Index.vue';
|
||||
import GalleryEdit from './gallery/Edit.vue';
|
||||
const ReportsIndex = () => import('./reports/Index.vue');
|
||||
const NotificationsIndex = () => import('./notifications/Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@ -78,14 +77,6 @@ export default {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
name: 'captain_settings_notifications',
|
||||
component: NotificationsIndex,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,443 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import SettingsLayout from '../../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
const templates = useMapGetter('captainNotificationTemplates/getRecords');
|
||||
const uiFlags = useMapGetter('captainNotificationTemplates/getUIFlags');
|
||||
|
||||
const selectedInboxId = ref(null);
|
||||
const editingId = ref(null);
|
||||
const showNewForm = ref(false);
|
||||
|
||||
// Todas as inboxes da conta (o usuário escolhe em qual configurar)
|
||||
const captainInboxes = computed(() => inboxes.value || []);
|
||||
|
||||
const hasInboxes = computed(() => captainInboxes.value.length > 0);
|
||||
|
||||
// ─── Formulários ──────────────────────────────────────────────────────────────
|
||||
const emptyForm = () => ({
|
||||
label: '',
|
||||
content: '',
|
||||
timing_minutes: 10,
|
||||
timing_direction: 'before',
|
||||
active: true,
|
||||
});
|
||||
|
||||
const newForm = ref(emptyForm());
|
||||
const editForm = ref(emptyForm());
|
||||
|
||||
const VARIABLES = [
|
||||
'{{guest_name}}',
|
||||
'{{check_in_time}}',
|
||||
'{{check_out_time}}',
|
||||
'{{suite_name}}',
|
||||
'{{unit_name}}',
|
||||
];
|
||||
|
||||
// ─── Carregamento ─────────────────────────────────────────────────────────────
|
||||
onMounted(async () => {
|
||||
await store.dispatch('inboxes/get');
|
||||
});
|
||||
|
||||
watch(selectedInboxId, async id => {
|
||||
if (id) {
|
||||
await store.dispatch('captainNotificationTemplates/fetch', id);
|
||||
showNewForm.value = false;
|
||||
editingId.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Novo template ────────────────────────────────────────────────────────────
|
||||
const openNewForm = () => {
|
||||
newForm.value = emptyForm();
|
||||
showNewForm.value = true;
|
||||
};
|
||||
|
||||
const cancelNew = () => {
|
||||
showNewForm.value = false;
|
||||
newForm.value = emptyForm();
|
||||
};
|
||||
|
||||
const saveNew = async () => {
|
||||
if (!newForm.value.label || !newForm.value.content) return;
|
||||
try {
|
||||
await store.dispatch('captainNotificationTemplates/create', {
|
||||
inboxId: selectedInboxId.value,
|
||||
payload: newForm.value,
|
||||
});
|
||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.CREATE.SUCCESS'));
|
||||
showNewForm.value = false;
|
||||
newForm.value = emptyForm();
|
||||
} catch {
|
||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.CREATE.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Edição ───────────────────────────────────────────────────────────────────
|
||||
const startEdit = template => {
|
||||
editingId.value = template.id;
|
||||
editForm.value = { ...template };
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
editingId.value = null;
|
||||
editForm.value = emptyForm();
|
||||
};
|
||||
|
||||
const saveEdit = async () => {
|
||||
try {
|
||||
await store.dispatch('captainNotificationTemplates/update', {
|
||||
inboxId: selectedInboxId.value,
|
||||
id: editingId.value,
|
||||
payload: editForm.value,
|
||||
});
|
||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.SUCCESS'));
|
||||
editingId.value = null;
|
||||
} catch {
|
||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Toggle ativo ─────────────────────────────────────────────────────────────
|
||||
const toggleActive = async template => {
|
||||
try {
|
||||
await store.dispatch('captainNotificationTemplates/update', {
|
||||
inboxId: selectedInboxId.value,
|
||||
id: template.id,
|
||||
payload: { active: !template.active },
|
||||
});
|
||||
} catch {
|
||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Exclusão ─────────────────────────────────────────────────────────────────
|
||||
const deleteTemplate = async template => {
|
||||
try {
|
||||
await store.dispatch('captainNotificationTemplates/delete', {
|
||||
inboxId: selectedInboxId.value,
|
||||
id: template.id,
|
||||
});
|
||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.DELETE.SUCCESS'));
|
||||
} catch {
|
||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.DELETE.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Variáveis ────────────────────────────────────────────────────────────────
|
||||
const insertVariable = (variable, target) => {
|
||||
if (target === 'new') {
|
||||
newForm.value.content += variable;
|
||||
} else {
|
||||
editForm.value.content += variable;
|
||||
}
|
||||
};
|
||||
|
||||
const directionLabel = direction =>
|
||||
direction === 'before'
|
||||
? t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE')
|
||||
: t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER');
|
||||
|
||||
const timingDisplay = template =>
|
||||
`${template.timing_minutes} ${t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')} ${directionLabel(template.timing_direction)} ${t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')}`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="t('CAPTAIN_SETTINGS.NOTIFICATIONS.LOADING')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="t('CAPTAIN_SETTINGS.NOTIFICATIONS.TITLE')"
|
||||
:description="t('CAPTAIN_SETTINGS.NOTIFICATIONS.DESCRIPTION')"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
v-if="selectedInboxId && !showNewForm"
|
||||
icon="i-lucide-plus"
|
||||
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.ADD')"
|
||||
@click="openNewForm"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-6 px-6 pb-8">
|
||||
<!-- Seletor de inbox -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.INBOX_LABEL') }}
|
||||
</label>
|
||||
<div v-if="!hasInboxes" class="text-sm text-n-slate-10">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.NO_CAPTAIN_INBOXES') }}
|
||||
</div>
|
||||
<div v-else class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="inbox in captainInboxes"
|
||||
:key="inbox.id"
|
||||
class="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm transition-colors"
|
||||
:class="
|
||||
selectedInboxId === inbox.id
|
||||
? 'border-w-500 bg-w-50 text-w-700 font-medium'
|
||||
: 'border-n-weak text-n-slate-11 hover:border-n-300'
|
||||
"
|
||||
@click="selectedInboxId = inbox.id"
|
||||
>
|
||||
<span class="i-lucide-message-circle w-4 h-4" />
|
||||
{{ inbox.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo: só aparece após selecionar inbox -->
|
||||
<div v-if="selectedInboxId" class="flex flex-col gap-3">
|
||||
<!-- Template list -->
|
||||
<div
|
||||
v-for="template in templates"
|
||||
:key="template.id"
|
||||
class="rounded-lg border border-n-75 bg-white p-4"
|
||||
>
|
||||
<!-- View mode -->
|
||||
<div
|
||||
v-if="editingId !== template.id"
|
||||
class="flex items-start justify-between gap-3"
|
||||
>
|
||||
<div class="flex flex-col gap-1 flex-1 min-w-0">
|
||||
<span class="text-sm font-semibold text-n-slate-12">{{
|
||||
template.label
|
||||
}}</span>
|
||||
<span class="text-sm text-n-slate-11 whitespace-pre-line">{{
|
||||
template.content
|
||||
}}</span>
|
||||
<span class="text-xs text-n-slate-10 mt-1">
|
||||
{{ timingDisplay(template) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded"
|
||||
:class="
|
||||
template.active
|
||||
? 'bg-n-teal-2 text-n-teal-11'
|
||||
: 'bg-n-slate-3 text-n-slate-11'
|
||||
"
|
||||
@click="toggleActive(template)"
|
||||
>
|
||||
{{
|
||||
template.active
|
||||
? t('CAPTAIN_SETTINGS.NOTIFICATIONS.ACTIVE')
|
||||
: t('CAPTAIN_SETTINGS.NOTIFICATIONS.INACTIVE')
|
||||
}}
|
||||
</button>
|
||||
<button
|
||||
class="text-n-slate-10 hover:text-n-slate-12"
|
||||
@click="startEdit(template)"
|
||||
>
|
||||
<span class="i-lucide-pencil w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
class="text-n-ruby-9 hover:text-n-ruby-11"
|
||||
@click="deleteTemplate(template)"
|
||||
>
|
||||
<span class="i-lucide-trash-2 w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit mode -->
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<input
|
||||
v-model="editForm.label"
|
||||
:placeholder="
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.LABEL_PLACEHOLDER')
|
||||
"
|
||||
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500"
|
||||
/>
|
||||
<textarea
|
||||
v-model="editForm.content"
|
||||
:placeholder="
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CONTENT_PLACEHOLDER')
|
||||
"
|
||||
rows="3"
|
||||
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500 resize-none"
|
||||
/>
|
||||
<!-- Variable chips -->
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="v in VARIABLES"
|
||||
:key="v"
|
||||
class="text-xs bg-n-slate-3 text-n-slate-11 px-2 py-0.5 rounded hover:bg-n-slate-4"
|
||||
@click="insertVariable(v, 'edit')"
|
||||
>
|
||||
{{ v }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Timing row -->
|
||||
<div class="flex items-center gap-2 text-sm flex-wrap">
|
||||
<span class="text-n-slate-11">{{
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SEND')
|
||||
}}</span>
|
||||
<input
|
||||
v-model.number="editForm.timing_minutes"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-16 rounded border border-n-weak px-2 py-1 text-sm text-center focus:outline-none focus:border-w-500"
|
||||
/>
|
||||
<span class="text-n-slate-11">{{
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')
|
||||
}}</span>
|
||||
<select
|
||||
v-model="editForm.timing_direction"
|
||||
class="rounded border border-n-weak px-2 py-1 text-sm focus:outline-none focus:border-w-500"
|
||||
>
|
||||
<option value="before">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE') }}
|
||||
</option>
|
||||
<option value="after">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER') }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-n-slate-11">{{
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="clear"
|
||||
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CANCEL')"
|
||||
@click="cancelEdit"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SAVE')"
|
||||
:is-loading="uiFlags.isSaving"
|
||||
@click="saveEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New form -->
|
||||
<div
|
||||
v-if="showNewForm"
|
||||
class="rounded-lg border border-w-300 bg-w-25 p-4 flex flex-col gap-3"
|
||||
>
|
||||
<input
|
||||
v-model="newForm.label"
|
||||
:placeholder="
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.LABEL_PLACEHOLDER')
|
||||
"
|
||||
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500"
|
||||
/>
|
||||
<textarea
|
||||
v-model="newForm.content"
|
||||
:placeholder="
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CONTENT_PLACEHOLDER')
|
||||
"
|
||||
rows="3"
|
||||
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500 resize-none"
|
||||
/>
|
||||
<!-- Variable chips -->
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="v in VARIABLES"
|
||||
:key="v"
|
||||
class="text-xs bg-n-slate-3 text-n-slate-11 px-2 py-0.5 rounded hover:bg-n-slate-4"
|
||||
@click="insertVariable(v, 'new')"
|
||||
>
|
||||
{{ v }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Timing row -->
|
||||
<div class="flex items-center gap-2 text-sm flex-wrap">
|
||||
<span class="text-n-slate-11">{{
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SEND')
|
||||
}}</span>
|
||||
<input
|
||||
v-model.number="newForm.timing_minutes"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-16 rounded border border-n-weak px-2 py-1 text-sm text-center focus:outline-none focus:border-w-500"
|
||||
/>
|
||||
<span class="text-n-slate-11">{{
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')
|
||||
}}</span>
|
||||
<select
|
||||
v-model="newForm.timing_direction"
|
||||
class="rounded border border-n-weak px-2 py-1 text-sm focus:outline-none focus:border-w-500"
|
||||
>
|
||||
<option value="before">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE') }}
|
||||
</option>
|
||||
<option value="after">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER') }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-n-slate-11">{{
|
||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="clear"
|
||||
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CANCEL')"
|
||||
@click="cancelNew"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SAVE')"
|
||||
:is-loading="uiFlags.isSaving"
|
||||
@click="saveNew"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (sem templates, sem form aberto) -->
|
||||
<div
|
||||
v-if="!templates.length && !showNewForm"
|
||||
class="flex flex-col items-center justify-center gap-4 py-16 text-center"
|
||||
>
|
||||
<div
|
||||
class="size-14 rounded-full bg-n-slate-3 flex items-center justify-center"
|
||||
>
|
||||
<span class="i-lucide-bell w-6 h-6 text-n-slate-10" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="mb-0 text-base font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.EMPTY.TITLE') }}
|
||||
</p>
|
||||
<p class="mb-0 max-w-sm text-sm text-n-slate-10">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.EMPTY.DESC') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
icon="i-lucide-plus"
|
||||
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.ADD')"
|
||||
@click="openNewForm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado inicial: nenhuma inbox selecionada -->
|
||||
<div
|
||||
v-else-if="hasInboxes"
|
||||
class="flex flex-col items-center justify-center gap-3 py-16 text-center"
|
||||
>
|
||||
<span class="i-lucide-mouse-pointer-click w-8 h-8 text-n-slate-9" />
|
||||
<p class="mb-0 text-sm text-n-slate-10">
|
||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.SELECT_INBOX_HINT') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@ -1,84 +0,0 @@
|
||||
import notificationTemplatesAPI from '../../api/captain/notificationTemplates';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
isSaving: false,
|
||||
},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getRecords: $state => $state.records,
|
||||
getUIFlags: $state => $state.uiFlags,
|
||||
};
|
||||
|
||||
const actions = {
|
||||
async fetch({ commit }, inboxId) {
|
||||
commit('SET_UI_FLAG', { isFetching: true });
|
||||
try {
|
||||
const { data } = await notificationTemplatesAPI.getAll(inboxId);
|
||||
commit('SET_RECORDS', data);
|
||||
} finally {
|
||||
commit('SET_UI_FLAG', { isFetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
async create({ commit }, { inboxId, payload }) {
|
||||
commit('SET_UI_FLAG', { isSaving: true });
|
||||
try {
|
||||
const { data } = await notificationTemplatesAPI.create(inboxId, payload);
|
||||
commit('ADD_RECORD', data);
|
||||
return data;
|
||||
} finally {
|
||||
commit('SET_UI_FLAG', { isSaving: false });
|
||||
}
|
||||
},
|
||||
|
||||
async update({ commit }, { inboxId, id, payload }) {
|
||||
commit('SET_UI_FLAG', { isSaving: true });
|
||||
try {
|
||||
const { data } = await notificationTemplatesAPI.update(
|
||||
inboxId,
|
||||
id,
|
||||
payload
|
||||
);
|
||||
commit('UPDATE_RECORD', data);
|
||||
return data;
|
||||
} finally {
|
||||
commit('SET_UI_FLAG', { isSaving: false });
|
||||
}
|
||||
},
|
||||
|
||||
async delete({ commit }, { inboxId, id }) {
|
||||
await notificationTemplatesAPI.delete(inboxId, id);
|
||||
commit('DELETE_RECORD', id);
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
SET_RECORDS($state, records) {
|
||||
$state.records = records;
|
||||
},
|
||||
ADD_RECORD($state, record) {
|
||||
$state.records.push(record);
|
||||
},
|
||||
UPDATE_RECORD($state, record) {
|
||||
const idx = $state.records.findIndex(r => r.id === record.id);
|
||||
if (idx !== -1) $state.records.splice(idx, 1, record);
|
||||
},
|
||||
DELETE_RECORD($state, id) {
|
||||
$state.records = $state.records.filter(r => r.id !== id);
|
||||
},
|
||||
SET_UI_FLAG($state, flags) {
|
||||
$state.uiFlags = { ...$state.uiFlags, ...flags };
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@ -66,7 +66,6 @@ import captainLifecycleDeliveries from './captain/lifecycleDeliveries';
|
||||
import captainUnits from './modules/captainUnits';
|
||||
import captainGalleryItems from './modules/captainGalleryItems';
|
||||
import captainReports from './modules/captainReports';
|
||||
import captainNotificationTemplates from './captain/notificationTemplates';
|
||||
|
||||
const plugins = [];
|
||||
|
||||
@ -138,7 +137,6 @@ export default createStore({
|
||||
captainUnits,
|
||||
captainGalleryItems,
|
||||
captainReports,
|
||||
captainNotificationTemplates,
|
||||
},
|
||||
plugins,
|
||||
});
|
||||
|
||||
@ -158,9 +158,9 @@ class Whatsapp::IncomingMessageWuzapiService < Whatsapp::IncomingMessageBaseServ
|
||||
|
||||
# Se a mensagem vier do celular (outgoing) e a assinatura estiver ativa,
|
||||
# e o conteúdo não parecer já ter uma assinatura (evita duplicar em ecos)
|
||||
if is_outgoing && inbox_obj.message_signature_enabled? && content.present? && !content.start_with?('*[') && !content.start_with?('*')
|
||||
if is_outgoing && inbox_obj.message_signature_enabled? && content.present? && !content.start_with?('*')
|
||||
signature_name = inbox_obj.shift_signature_name
|
||||
content = "*[ #{signature_name} ]*\n#{content}" if signature_name.present?
|
||||
content = "*#{signature_name}*\n#{content}" if signature_name.present?
|
||||
end
|
||||
|
||||
msg_params = {
|
||||
|
||||
@ -175,7 +175,7 @@ class Whatsapp::Providers::WuzapiService < Whatsapp::Providers::BaseService
|
||||
return content unless message.inbox.message_signature_enabled?
|
||||
|
||||
name = sender_name_for(message)
|
||||
name.present? ? "*[ #{name} ]*\n#{content}" : content
|
||||
name.present? ? "*#{name}*\n#{content}" : content
|
||||
end
|
||||
|
||||
def reply_params(message)
|
||||
|
||||
@ -275,8 +275,6 @@ Rails.application.routes.draw do
|
||||
post :sync_templates, on: :member
|
||||
get :health, on: :member
|
||||
post :on_whatsapp, on: :member
|
||||
resources :notification_templates, only: [:index, :create, :update, :destroy],
|
||||
module: 'captain'
|
||||
if ChatwootApp.enterprise?
|
||||
resource :conference, only: %i[create destroy], controller: 'conference' do
|
||||
get :token, on: :member
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
class Api::V1::Accounts::Captain::NotificationTemplatesController < Api::V1::Accounts::BaseController
|
||||
before_action :set_inbox
|
||||
before_action :set_template, only: [:update, :destroy]
|
||||
|
||||
def index
|
||||
templates = @inbox.captain_notification_templates.ordered
|
||||
render json: templates
|
||||
end
|
||||
|
||||
def create
|
||||
template = @inbox.captain_notification_templates.new(template_params)
|
||||
if template.save
|
||||
render json: template, status: :created
|
||||
else
|
||||
render json: { error: template.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @template.update(template_params)
|
||||
render json: @template
|
||||
else
|
||||
render json: { error: @template.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@template.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_inbox
|
||||
@inbox = current_account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
|
||||
def set_template
|
||||
@template = @inbox.captain_notification_templates.find(params[:id])
|
||||
end
|
||||
|
||||
def template_params
|
||||
params.require(:notification_template).permit(:label, :content, :timing_minutes, :timing_direction, :active, :position)
|
||||
end
|
||||
end
|
||||
@ -1,41 +0,0 @@
|
||||
class Captain::Notifications::NotificationScannerJob < ApplicationJob
|
||||
queue_as :scheduled_jobs
|
||||
|
||||
# Tolerance window around the target time (job runs every 5 min, so ±5 min ensures coverage)
|
||||
WINDOW_MINUTES = 5
|
||||
|
||||
def perform
|
||||
Captain::NotificationTemplate.active.find_each do |template|
|
||||
eligible_reservations_for(template).find_each do |reservation|
|
||||
Captain::Notifications::SendNotificationService.new(reservation, template).perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def eligible_reservations_for(template)
|
||||
target_time = compute_target_time(template)
|
||||
window_start = target_time - WINDOW_MINUTES.minutes
|
||||
window_end = target_time + WINDOW_MINUTES.minutes
|
||||
|
||||
Captain::Reservation
|
||||
.joins(:conversation)
|
||||
.where(conversations: { inbox_id: template.inbox_id })
|
||||
.where(status: Captain::Reservation.statuses.slice(:confirmed, :active).values)
|
||||
.where(check_in_at: window_start..window_end)
|
||||
.where.not(conversation_id: nil)
|
||||
.where(
|
||||
"NOT (captain_reservations.metadata->'notified_templates' @> ?::jsonb)",
|
||||
"[#{template.id}]"
|
||||
)
|
||||
end
|
||||
|
||||
def compute_target_time(template)
|
||||
if template.before?
|
||||
template.timing_minutes.minutes.from_now
|
||||
else
|
||||
template.timing_minutes.minutes.ago
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1,40 +0,0 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: captain_notification_templates
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# active :boolean default(TRUE), not null
|
||||
# content :text not null
|
||||
# label :string not null
|
||||
# position :integer default(0), not null
|
||||
# timing_direction :integer default("before"), not null
|
||||
# timing_minutes :integer default(10), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# inbox_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# idx_notif_templates_inbox_active (inbox_id,active)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (inbox_id => inboxes.id)
|
||||
#
|
||||
class Captain::NotificationTemplate < ApplicationRecord
|
||||
self.table_name = 'captain_notification_templates'
|
||||
|
||||
belongs_to :inbox, inverse_of: :captain_notification_templates
|
||||
|
||||
enum timing_direction: { before: 0, after: 1 }
|
||||
|
||||
validates :label, presence: true
|
||||
validates :content, presence: true
|
||||
validates :timing_minutes, presence: true, numericality: { greater_than: 0 }
|
||||
validates :timing_direction, presence: true
|
||||
validates :inbox_id, presence: true
|
||||
|
||||
scope :active, -> { where(active: true) }
|
||||
scope :ordered, -> { order(:position, :id) }
|
||||
scope :for_inbox, ->(inbox_id) { where(inbox_id: inbox_id) }
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user