refactor: move notification templates de units para inboxes
- Arquitetura corrigida: templates agora pertencem à inbox (WhatsApp),
não à unidade PIX (que é uma config financeira, não de mensagens)
- Migration: troca FK captain_unit_id -> inbox_id (up/down explícito)
- Model: belongs_to :inbox; scope for_inbox
- Controller: escopo via account.inboxes.find(inbox_id)
- Rotas: move de captain/units/:id → inboxes/:id/notification_templates
- Scanner job: joins(:conversation).where(conversations: {inbox_id:})
- UI: página /captain/notifications com seletor de inbox no topo
(chips clicáveis, templates carregam por watch no selectedInboxId)
- i18n PT/EN: novas keys INBOX_LABEL, SELECT_INBOX_HINT, EMPTY
This commit is contained in:
parent
ce2904e57f
commit
84fff38d94
@ -1,30 +1,29 @@
|
|||||||
/* global axios */
|
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
class CaptainNotificationTemplatesAPI extends ApiClient {
|
class NotificationTemplatesAPI extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('captain/units', { accountScoped: true });
|
super('inboxes', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
getTemplates(unitId) {
|
getAll(inboxId) {
|
||||||
return axios.get(`${this.url}/${unitId}/notification_templates`);
|
return this.get(`${inboxId}/notification_templates`);
|
||||||
}
|
}
|
||||||
|
|
||||||
createTemplate(unitId, data) {
|
create(inboxId, data) {
|
||||||
return axios.post(`${this.url}/${unitId}/notification_templates`, {
|
return this.post(`${inboxId}/notification_templates`, {
|
||||||
notification_template: data,
|
notification_template: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTemplate(unitId, id, data) {
|
update(inboxId, id, data) {
|
||||||
return axios.patch(`${this.url}/${unitId}/notification_templates/${id}`, {
|
return this.patch(`${inboxId}/notification_templates/${id}`, {
|
||||||
notification_template: data,
|
notification_template: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTemplate(unitId, id) {
|
delete(inboxId, id) {
|
||||||
return axios.delete(`${this.url}/${unitId}/notification_templates/${id}`);
|
return this.delete(`${inboxId}/notification_templates/${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new CaptainNotificationTemplatesAPI();
|
export default new NotificationTemplatesAPI();
|
||||||
|
|||||||
@ -476,6 +476,13 @@
|
|||||||
"DELETE": {
|
"DELETE": {
|
||||||
"SUCCESS": "Notification removed.",
|
"SUCCESS": "Notification removed.",
|
||||||
"ERROR": "Error removing notification."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -477,6 +477,13 @@
|
|||||||
"DELETE": {
|
"DELETE": {
|
||||||
"SUCCESS": "Notificação removida.",
|
"SUCCESS": "Notificação removida.",
|
||||||
"ERROR": "Erro ao remover notificação."
|
"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": "Crie templates de mensagem automática para esta caixa de entrada."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'units/:unitId/notifications',
|
path: 'notifications',
|
||||||
name: 'captain_settings_notifications',
|
name: 'captain_settings_notifications',
|
||||||
component: NotificationsIndex,
|
component: NotificationsIndex,
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@ -9,17 +8,24 @@ import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
|
|||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const unitId = computed(() => route.params.unitId);
|
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||||
|
const templates = useMapGetter('captainNotificationTemplates/getRecords');
|
||||||
const templates = useMapGetter('captainNotificationTemplates/getTemplates');
|
|
||||||
const uiFlags = useMapGetter('captainNotificationTemplates/getUIFlags');
|
const uiFlags = useMapGetter('captainNotificationTemplates/getUIFlags');
|
||||||
|
|
||||||
|
const selectedInboxId = ref(null);
|
||||||
const editingId = ref(null);
|
const editingId = ref(null);
|
||||||
const showNewForm = ref(false);
|
const showNewForm = ref(false);
|
||||||
|
|
||||||
|
// ─── Inboxes com Captain assistant ────────────────────────────────────────────
|
||||||
|
const captainInboxes = computed(() =>
|
||||||
|
(inboxes.value || []).filter(i => i.captain_assistant_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasInboxes = computed(() => captainInboxes.value.length > 0);
|
||||||
|
|
||||||
|
// ─── Formulários ──────────────────────────────────────────────────────────────
|
||||||
const emptyForm = () => ({
|
const emptyForm = () => ({
|
||||||
label: '',
|
label: '',
|
||||||
content: '',
|
content: '',
|
||||||
@ -39,12 +45,20 @@ const VARIABLES = [
|
|||||||
'{{unit_name}}',
|
'{{unit_name}}',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ─── Carregamento ─────────────────────────────────────────────────────────────
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (unitId.value) {
|
await store.dispatch('inboxes/get');
|
||||||
await store.dispatch('captainNotificationTemplates/fetch', unitId.value);
|
});
|
||||||
|
|
||||||
|
watch(selectedInboxId, async id => {
|
||||||
|
if (id) {
|
||||||
|
await store.dispatch('captainNotificationTemplates/fetch', id);
|
||||||
|
showNewForm.value = false;
|
||||||
|
editingId.value = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Novo template ────────────────────────────────────────────────────────────
|
||||||
const openNewForm = () => {
|
const openNewForm = () => {
|
||||||
newForm.value = emptyForm();
|
newForm.value = emptyForm();
|
||||||
showNewForm.value = true;
|
showNewForm.value = true;
|
||||||
@ -59,8 +73,8 @@ const saveNew = async () => {
|
|||||||
if (!newForm.value.label || !newForm.value.content) return;
|
if (!newForm.value.label || !newForm.value.content) return;
|
||||||
try {
|
try {
|
||||||
await store.dispatch('captainNotificationTemplates/create', {
|
await store.dispatch('captainNotificationTemplates/create', {
|
||||||
unitId: unitId.value,
|
inboxId: selectedInboxId.value,
|
||||||
...newForm.value,
|
payload: newForm.value,
|
||||||
});
|
});
|
||||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.CREATE.SUCCESS'));
|
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.CREATE.SUCCESS'));
|
||||||
showNewForm.value = false;
|
showNewForm.value = false;
|
||||||
@ -70,6 +84,7 @@ const saveNew = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Edição ───────────────────────────────────────────────────────────────────
|
||||||
const startEdit = template => {
|
const startEdit = template => {
|
||||||
editingId.value = template.id;
|
editingId.value = template.id;
|
||||||
editForm.value = { ...template };
|
editForm.value = { ...template };
|
||||||
@ -83,9 +98,9 @@ const cancelEdit = () => {
|
|||||||
const saveEdit = async () => {
|
const saveEdit = async () => {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('captainNotificationTemplates/update', {
|
await store.dispatch('captainNotificationTemplates/update', {
|
||||||
unitId: unitId.value,
|
inboxId: selectedInboxId.value,
|
||||||
id: editingId.value,
|
id: editingId.value,
|
||||||
...editForm.value,
|
payload: editForm.value,
|
||||||
});
|
});
|
||||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.SUCCESS'));
|
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.SUCCESS'));
|
||||||
editingId.value = null;
|
editingId.value = null;
|
||||||
@ -94,22 +109,24 @@ const saveEdit = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Toggle ativo ─────────────────────────────────────────────────────────────
|
||||||
const toggleActive = async template => {
|
const toggleActive = async template => {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('captainNotificationTemplates/update', {
|
await store.dispatch('captainNotificationTemplates/update', {
|
||||||
unitId: unitId.value,
|
inboxId: selectedInboxId.value,
|
||||||
id: template.id,
|
id: template.id,
|
||||||
active: !template.active,
|
payload: { active: !template.active },
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.ERROR'));
|
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.UPDATE.ERROR'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Exclusão ─────────────────────────────────────────────────────────────────
|
||||||
const deleteTemplate = async template => {
|
const deleteTemplate = async template => {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('captainNotificationTemplates/delete', {
|
await store.dispatch('captainNotificationTemplates/delete', {
|
||||||
unitId: unitId.value,
|
inboxId: selectedInboxId.value,
|
||||||
id: template.id,
|
id: template.id,
|
||||||
});
|
});
|
||||||
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.DELETE.SUCCESS'));
|
useAlert(t('CAPTAIN_SETTINGS.NOTIFICATIONS.DELETE.SUCCESS'));
|
||||||
@ -118,6 +135,7 @@ const deleteTemplate = async template => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Variáveis ────────────────────────────────────────────────────────────────
|
||||||
const insertVariable = (variable, target) => {
|
const insertVariable = (variable, target) => {
|
||||||
if (target === 'new') {
|
if (target === 'new') {
|
||||||
newForm.value.content += variable;
|
newForm.value.content += variable;
|
||||||
@ -144,11 +162,48 @@ const timingDisplay = template =>
|
|||||||
<BaseSettingsHeader
|
<BaseSettingsHeader
|
||||||
:title="t('CAPTAIN_SETTINGS.NOTIFICATIONS.TITLE')"
|
:title="t('CAPTAIN_SETTINGS.NOTIFICATIONS.TITLE')"
|
||||||
:description="t('CAPTAIN_SETTINGS.NOTIFICATIONS.DESCRIPTION')"
|
: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>
|
</template>
|
||||||
|
</BaseSettingsHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col gap-3">
|
<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 -->
|
<!-- Template list -->
|
||||||
<div
|
<div
|
||||||
v-for="template in templates"
|
v-for="template in templates"
|
||||||
@ -161,13 +216,13 @@ const timingDisplay = template =>
|
|||||||
class="flex items-start justify-between gap-3"
|
class="flex items-start justify-between gap-3"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-1 flex-1 min-w-0">
|
<div class="flex flex-col gap-1 flex-1 min-w-0">
|
||||||
<span class="text-sm font-semibold text-n-900">{{
|
<span class="text-sm font-semibold text-n-slate-12">{{
|
||||||
template.label
|
template.label
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-sm text-n-600 whitespace-pre-line">{{
|
<span class="text-sm text-n-slate-11 whitespace-pre-line">{{
|
||||||
template.content
|
template.content
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-xs text-n-500 mt-1">
|
<span class="text-xs text-n-slate-10 mt-1">
|
||||||
{{ timingDisplay(template) }}
|
{{ timingDisplay(template) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -175,7 +230,9 @@ const timingDisplay = template =>
|
|||||||
<button
|
<button
|
||||||
class="text-xs px-2 py-1 rounded"
|
class="text-xs px-2 py-1 rounded"
|
||||||
:class="
|
:class="
|
||||||
template.active ? 'bg-g-100 text-g-700' : 'bg-n-75 text-n-500'
|
template.active
|
||||||
|
? 'bg-n-teal-2 text-n-teal-11'
|
||||||
|
: 'bg-n-slate-3 text-n-slate-11'
|
||||||
"
|
"
|
||||||
@click="toggleActive(template)"
|
@click="toggleActive(template)"
|
||||||
>
|
>
|
||||||
@ -186,13 +243,13 @@ const timingDisplay = template =>
|
|||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="text-n-500 hover:text-n-700"
|
class="text-n-slate-10 hover:text-n-slate-12"
|
||||||
@click="startEdit(template)"
|
@click="startEdit(template)"
|
||||||
>
|
>
|
||||||
<span class="i-lucide-pencil w-4 h-4" />
|
<span class="i-lucide-pencil w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="text-r-500 hover:text-r-700"
|
class="text-n-ruby-9 hover:text-n-ruby-11"
|
||||||
@click="deleteTemplate(template)"
|
@click="deleteTemplate(template)"
|
||||||
>
|
>
|
||||||
<span class="i-lucide-trash-2 w-4 h-4" />
|
<span class="i-lucide-trash-2 w-4 h-4" />
|
||||||
@ -207,7 +264,7 @@ const timingDisplay = template =>
|
|||||||
:placeholder="
|
:placeholder="
|
||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.LABEL_PLACEHOLDER')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.LABEL_PLACEHOLDER')
|
||||||
"
|
"
|
||||||
class="w-full rounded border border-n-200 px-3 py-2 text-sm focus:outline-none focus:border-w-500"
|
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500"
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="editForm.content"
|
v-model="editForm.content"
|
||||||
@ -215,36 +272,36 @@ const timingDisplay = template =>
|
|||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CONTENT_PLACEHOLDER')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CONTENT_PLACEHOLDER')
|
||||||
"
|
"
|
||||||
rows="3"
|
rows="3"
|
||||||
class="w-full rounded border border-n-200 px-3 py-2 text-sm focus:outline-none focus:border-w-500 resize-none"
|
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 -->
|
<!-- Variable chips -->
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<button
|
<button
|
||||||
v-for="v in VARIABLES"
|
v-for="v in VARIABLES"
|
||||||
:key="v"
|
:key="v"
|
||||||
class="text-xs bg-n-75 text-n-700 px-2 py-0.5 rounded hover:bg-n-100"
|
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')"
|
@click="insertVariable(v, 'edit')"
|
||||||
>
|
>
|
||||||
{{ v }}
|
{{ v }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Timing row -->
|
<!-- Timing row -->
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm flex-wrap">
|
||||||
<span class="text-n-600">{{
|
<span class="text-n-slate-11">{{
|
||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SEND')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SEND')
|
||||||
}}</span>
|
}}</span>
|
||||||
<input
|
<input
|
||||||
v-model.number="editForm.timing_minutes"
|
v-model.number="editForm.timing_minutes"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
class="w-16 rounded border border-n-200 px-2 py-1 text-sm text-center focus:outline-none focus:border-w-500"
|
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-600">{{
|
<span class="text-n-slate-11">{{
|
||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')
|
||||||
}}</span>
|
}}</span>
|
||||||
<select
|
<select
|
||||||
v-model="editForm.timing_direction"
|
v-model="editForm.timing_direction"
|
||||||
class="rounded border border-n-200 px-2 py-1 text-sm focus:outline-none focus:border-w-500"
|
class="rounded border border-n-weak px-2 py-1 text-sm focus:outline-none focus:border-w-500"
|
||||||
>
|
>
|
||||||
<option value="before">
|
<option value="before">
|
||||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE') }}
|
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE') }}
|
||||||
@ -253,7 +310,7 @@ const timingDisplay = template =>
|
|||||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER') }}
|
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER') }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="text-n-600">{{
|
<span class="text-n-slate-11">{{
|
||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -265,7 +322,7 @@ const timingDisplay = template =>
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SAVE')"
|
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SAVE')"
|
||||||
:is-loading="uiFlags.isUpdating"
|
:is-loading="uiFlags.isSaving"
|
||||||
@click="saveEdit"
|
@click="saveEdit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -282,7 +339,7 @@ const timingDisplay = template =>
|
|||||||
:placeholder="
|
:placeholder="
|
||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.LABEL_PLACEHOLDER')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.LABEL_PLACEHOLDER')
|
||||||
"
|
"
|
||||||
class="w-full rounded border border-n-200 px-3 py-2 text-sm focus:outline-none focus:border-w-500"
|
class="w-full rounded border border-n-weak px-3 py-2 text-sm focus:outline-none focus:border-w-500"
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="newForm.content"
|
v-model="newForm.content"
|
||||||
@ -290,36 +347,36 @@ const timingDisplay = template =>
|
|||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CONTENT_PLACEHOLDER')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.CONTENT_PLACEHOLDER')
|
||||||
"
|
"
|
||||||
rows="3"
|
rows="3"
|
||||||
class="w-full rounded border border-n-200 px-3 py-2 text-sm focus:outline-none focus:border-w-500 resize-none"
|
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 -->
|
<!-- Variable chips -->
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<button
|
<button
|
||||||
v-for="v in VARIABLES"
|
v-for="v in VARIABLES"
|
||||||
:key="v"
|
:key="v"
|
||||||
class="text-xs bg-n-75 text-n-700 px-2 py-0.5 rounded hover:bg-n-100"
|
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')"
|
@click="insertVariable(v, 'new')"
|
||||||
>
|
>
|
||||||
{{ v }}
|
{{ v }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Timing row -->
|
<!-- Timing row -->
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm flex-wrap">
|
||||||
<span class="text-n-600">{{
|
<span class="text-n-slate-11">{{
|
||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SEND')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SEND')
|
||||||
}}</span>
|
}}</span>
|
||||||
<input
|
<input
|
||||||
v-model.number="newForm.timing_minutes"
|
v-model.number="newForm.timing_minutes"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
class="w-16 rounded border border-n-200 px-2 py-1 text-sm text-center focus:outline-none focus:border-w-500"
|
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-600">{{
|
<span class="text-n-slate-11">{{
|
||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.MINUTES')
|
||||||
}}</span>
|
}}</span>
|
||||||
<select
|
<select
|
||||||
v-model="newForm.timing_direction"
|
v-model="newForm.timing_direction"
|
||||||
class="rounded border border-n-200 px-2 py-1 text-sm focus:outline-none focus:border-w-500"
|
class="rounded border border-n-weak px-2 py-1 text-sm focus:outline-none focus:border-w-500"
|
||||||
>
|
>
|
||||||
<option value="before">
|
<option value="before">
|
||||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE') }}
|
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.BEFORE') }}
|
||||||
@ -328,7 +385,7 @@ const timingDisplay = template =>
|
|||||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER') }}
|
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.AFTER') }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="text-n-600">{{
|
<span class="text-n-slate-11">{{
|
||||||
t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')
|
t('CAPTAIN_SETTINGS.NOTIFICATIONS.DIRECTION.OF_ARRIVAL')
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -340,21 +397,48 @@ const timingDisplay = template =>
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SAVE')"
|
:label="t('CAPTAIN_SETTINGS.NOTIFICATIONS.FORM.SAVE')"
|
||||||
:is-loading="uiFlags.isCreating"
|
:is-loading="uiFlags.isSaving"
|
||||||
@click="saveNew"
|
@click="saveNew"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add button -->
|
<!-- Empty state (sem templates, sem form aberto) -->
|
||||||
<button
|
<div
|
||||||
v-if="!showNewForm"
|
v-if="!templates.length && !showNewForm"
|
||||||
class="flex items-center justify-center gap-2 rounded-lg border-2 border-dashed border-n-200 py-4 text-sm text-n-500 hover:border-w-400 hover:text-w-600 transition-colors"
|
class="flex flex-col items-center justify-center gap-4 py-16 text-center"
|
||||||
@click="openNewForm"
|
|
||||||
>
|
>
|
||||||
<span class="i-lucide-plus w-4 h-4" />
|
<div
|
||||||
{{ t('CAPTAIN_SETTINGS.NOTIFICATIONS.ADD') }}
|
class="size-14 rounded-full bg-n-slate-3 flex items-center justify-center"
|
||||||
</button>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
|
|||||||
@ -1,97 +1,77 @@
|
|||||||
import CaptainNotificationTemplatesAPI from 'dashboard/api/captain/notificationTemplates';
|
import notificationTemplatesAPI from '../../api/captain/notificationTemplates';
|
||||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
templates: [],
|
records: [],
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isCreating: false,
|
isSaving: false,
|
||||||
isUpdating: false,
|
|
||||||
isDeleting: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
getTemplates: $state => $state.templates,
|
getRecords: $state => $state.records,
|
||||||
getUIFlags: $state => $state.uiFlags,
|
getUIFlags: $state => $state.uiFlags,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutations = {
|
|
||||||
SET_TEMPLATES($state, templates) {
|
|
||||||
$state.templates = templates;
|
|
||||||
},
|
|
||||||
ADD_TEMPLATE($state, template) {
|
|
||||||
$state.templates.push(template);
|
|
||||||
},
|
|
||||||
UPDATE_TEMPLATE($state, updated) {
|
|
||||||
const index = $state.templates.findIndex(t => t.id === updated.id);
|
|
||||||
if (index !== -1) $state.templates.splice(index, 1, updated);
|
|
||||||
},
|
|
||||||
DELETE_TEMPLATE($state, id) {
|
|
||||||
$state.templates = $state.templates.filter(t => t.id !== id);
|
|
||||||
},
|
|
||||||
SET_UI_FLAG($state, flags) {
|
|
||||||
$state.uiFlags = { ...$state.uiFlags, ...flags };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
fetch: async ({ commit }, unitId) => {
|
async fetch({ commit }, inboxId) {
|
||||||
commit('SET_UI_FLAG', { isFetching: true });
|
commit('SET_UI_FLAG', { isFetching: true });
|
||||||
try {
|
try {
|
||||||
const { data } =
|
const { data } = await notificationTemplatesAPI.getAll(inboxId);
|
||||||
await CaptainNotificationTemplatesAPI.getTemplates(unitId);
|
commit('SET_RECORDS', data);
|
||||||
commit('SET_TEMPLATES', data);
|
|
||||||
} catch (error) {
|
|
||||||
throwErrorMessage(error);
|
|
||||||
} finally {
|
} finally {
|
||||||
commit('SET_UI_FLAG', { isFetching: false });
|
commit('SET_UI_FLAG', { isFetching: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async ({ commit }, { unitId, ...templateData }) => {
|
async create({ commit }, { inboxId, payload }) {
|
||||||
commit('SET_UI_FLAG', { isCreating: true });
|
commit('SET_UI_FLAG', { isSaving: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await CaptainNotificationTemplatesAPI.createTemplate(
|
const { data } = await notificationTemplatesAPI.create(inboxId, payload);
|
||||||
unitId,
|
commit('ADD_RECORD', data);
|
||||||
templateData
|
|
||||||
);
|
|
||||||
commit('ADD_TEMPLATE', data);
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
|
||||||
return throwErrorMessage(error);
|
|
||||||
} finally {
|
} finally {
|
||||||
commit('SET_UI_FLAG', { isCreating: false });
|
commit('SET_UI_FLAG', { isSaving: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async ({ commit }, { unitId, id, ...templateData }) => {
|
async update({ commit }, { inboxId, id, payload }) {
|
||||||
commit('SET_UI_FLAG', { isUpdating: true });
|
commit('SET_UI_FLAG', { isSaving: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await CaptainNotificationTemplatesAPI.updateTemplate(
|
const { data } = await notificationTemplatesAPI.update(
|
||||||
unitId,
|
inboxId,
|
||||||
id,
|
id,
|
||||||
templateData
|
payload
|
||||||
);
|
);
|
||||||
commit('UPDATE_TEMPLATE', data);
|
commit('UPDATE_RECORD', data);
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
|
||||||
return throwErrorMessage(error);
|
|
||||||
} finally {
|
} finally {
|
||||||
commit('SET_UI_FLAG', { isUpdating: false });
|
commit('SET_UI_FLAG', { isSaving: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async ({ commit }, { unitId, id }) => {
|
async delete({ commit }, { inboxId, id }) {
|
||||||
commit('SET_UI_FLAG', { isDeleting: true });
|
await notificationTemplatesAPI.delete(inboxId, id);
|
||||||
try {
|
commit('DELETE_RECORD', id);
|
||||||
await CaptainNotificationTemplatesAPI.deleteTemplate(unitId, id);
|
},
|
||||||
commit('DELETE_TEMPLATE', id);
|
};
|
||||||
} catch (error) {
|
|
||||||
throwErrorMessage(error);
|
const mutations = {
|
||||||
} finally {
|
SET_RECORDS($state, records) {
|
||||||
commit('SET_UI_FLAG', { isDeleting: false });
|
$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 };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,6 +79,6 @@ export default {
|
|||||||
namespaced: true,
|
namespaced: true,
|
||||||
state,
|
state,
|
||||||
getters,
|
getters,
|
||||||
mutations,
|
|
||||||
actions,
|
actions,
|
||||||
|
mutations,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -90,9 +90,7 @@ Rails.application.routes.draw do
|
|||||||
post :label_suggestion
|
post :label_suggestion
|
||||||
post :follow_up
|
post :follow_up
|
||||||
end
|
end
|
||||||
resources :units do
|
resources :units
|
||||||
resources :notification_templates, only: [:index, :create, :update, :destroy]
|
|
||||||
end
|
|
||||||
namespace :reports do
|
namespace :reports do
|
||||||
resource :operational, only: [:show], controller: 'reports/operational'
|
resource :operational, only: [:show], controller: 'reports/operational'
|
||||||
resources :insights, only: [:index, :show] do
|
resources :insights, only: [:index, :show] do
|
||||||
@ -242,6 +240,8 @@ Rails.application.routes.draw do
|
|||||||
post :sync_templates, on: :member
|
post :sync_templates, on: :member
|
||||||
get :health, on: :member
|
get :health, on: :member
|
||||||
post :on_whatsapp, on: :member
|
post :on_whatsapp, on: :member
|
||||||
|
resources :notification_templates, only: [:index, :create, :update, :destroy],
|
||||||
|
module: 'captain'
|
||||||
if ChatwootApp.enterprise?
|
if ChatwootApp.enterprise?
|
||||||
resource :conference, only: %i[create destroy], controller: 'conference' do
|
resource :conference, only: %i[create destroy], controller: 'conference' do
|
||||||
get :token, on: :member
|
get :token, on: :member
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
class ChangeNotificationTemplatesToInbox < ActiveRecord::Migration[7.1]
|
||||||
|
def up
|
||||||
|
remove_index :captain_notification_templates,
|
||||||
|
column: %i[captain_unit_id active],
|
||||||
|
name: 'idx_notif_templates_unit_active',
|
||||||
|
if_exists: true
|
||||||
|
remove_column :captain_notification_templates, :captain_unit_id, :bigint
|
||||||
|
|
||||||
|
add_column :captain_notification_templates, :inbox_id, :bigint
|
||||||
|
add_foreign_key :captain_notification_templates, :inboxes, column: :inbox_id
|
||||||
|
add_index :captain_notification_templates, %i[inbox_id active],
|
||||||
|
name: 'idx_notif_templates_inbox_active'
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_index :captain_notification_templates,
|
||||||
|
name: 'idx_notif_templates_inbox_active',
|
||||||
|
if_exists: true
|
||||||
|
remove_foreign_key :captain_notification_templates, :inboxes
|
||||||
|
remove_column :captain_notification_templates, :inbox_id, :bigint
|
||||||
|
|
||||||
|
add_column :captain_notification_templates, :captain_unit_id, :bigint
|
||||||
|
add_index :captain_notification_templates, %i[captain_unit_id active],
|
||||||
|
name: 'idx_notif_templates_unit_active'
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -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_01_120000) do
|
ActiveRecord::Schema[7.1].define(version: 2026_03_01_200000) 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"
|
||||||
@ -535,7 +535,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_01_120000) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "captain_notification_templates", force: :cascade do |t|
|
create_table "captain_notification_templates", force: :cascade do |t|
|
||||||
t.bigint "captain_unit_id", null: false
|
|
||||||
t.string "label", null: false
|
t.string "label", null: false
|
||||||
t.text "content", null: false
|
t.text "content", null: false
|
||||||
t.integer "timing_minutes", default: 10, null: false
|
t.integer "timing_minutes", default: 10, null: false
|
||||||
@ -544,8 +543,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_01_120000) do
|
|||||||
t.integer "position", default: 0, null: false
|
t.integer "position", default: 0, null: false
|
||||||
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.index ["captain_unit_id", "active"], name: "idx_notif_templates_unit_active"
|
t.bigint "inbox_id", null: false
|
||||||
t.index ["captain_unit_id"], name: "index_captain_notification_templates_on_captain_unit_id"
|
t.index ["inbox_id", "active"], name: "idx_notif_templates_inbox_active"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "captain_pix_charges", force: :cascade do |t|
|
create_table "captain_pix_charges", force: :cascade do |t|
|
||||||
@ -1978,7 +1977,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_01_120000) do
|
|||||||
add_foreign_key "captain_inbox_reminder_settings", "accounts"
|
add_foreign_key "captain_inbox_reminder_settings", "accounts"
|
||||||
add_foreign_key "captain_inbox_reminder_settings", "inboxes"
|
add_foreign_key "captain_inbox_reminder_settings", "inboxes"
|
||||||
add_foreign_key "captain_inboxes", "captain_units"
|
add_foreign_key "captain_inboxes", "captain_units"
|
||||||
add_foreign_key "captain_notification_templates", "captain_units"
|
add_foreign_key "captain_notification_templates", "inboxes"
|
||||||
add_foreign_key "captain_pix_charges", "captain_reservations", column: "reservation_id"
|
add_foreign_key "captain_pix_charges", "captain_reservations", column: "reservation_id"
|
||||||
add_foreign_key "captain_pix_charges", "captain_units", column: "unit_id"
|
add_foreign_key "captain_pix_charges", "captain_units", column: "unit_id"
|
||||||
add_foreign_key "captain_pricings", "accounts"
|
add_foreign_key "captain_pricings", "accounts"
|
||||||
|
|||||||
@ -1,23 +1,27 @@
|
|||||||
class Api::V1::Accounts::Captain::NotificationTemplatesController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Captain::NotificationTemplatesController < Api::V1::Accounts::BaseController
|
||||||
before_action :current_account
|
before_action :set_inbox
|
||||||
before_action -> { check_authorization(Captain::Assistant) }
|
|
||||||
before_action :set_unit
|
|
||||||
before_action :set_template, only: [:update, :destroy]
|
before_action :set_template, only: [:update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@templates = @unit.notification_templates.ordered
|
templates = @inbox.captain_notification_templates.ordered
|
||||||
render json: @templates
|
render json: templates
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template = @unit.notification_templates.new(template_params)
|
template = @inbox.captain_notification_templates.new(template_params)
|
||||||
@template.save!
|
if template.save
|
||||||
render json: @template, status: :created
|
render json: template, status: :created
|
||||||
|
else
|
||||||
|
render json: { error: template.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@template.update!(template_params)
|
if @template.update(template_params)
|
||||||
render json: @template
|
render json: @template
|
||||||
|
else
|
||||||
|
render json: { error: @template.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@ -27,26 +31,15 @@ class Api::V1::Accounts::Captain::NotificationTemplatesController < Api::V1::Acc
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_unit
|
def set_inbox
|
||||||
@unit = Current.account.captain_units.find(params[:unit_id])
|
@inbox = current_account.inboxes.find(params[:inbox_id])
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
render json: { error: 'Unidade não encontrada' }, status: :not_found
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_template
|
def set_template
|
||||||
@template = @unit.notification_templates.find(params[:id])
|
@template = @inbox.captain_notification_templates.find(params[:id])
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
render json: { error: 'Template não encontrado' }, status: :not_found
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:notification_template).permit(
|
params.require(:notification_template).permit(:label, :content, :timing_minutes, :timing_direction, :active, :position)
|
||||||
:label,
|
|
||||||
:content,
|
|
||||||
:timing_minutes,
|
|
||||||
:timing_direction,
|
|
||||||
:active,
|
|
||||||
:position
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -20,12 +20,13 @@ class Captain::Notifications::NotificationScannerJob < ApplicationJob
|
|||||||
window_end = target_time + WINDOW_MINUTES.minutes
|
window_end = target_time + WINDOW_MINUTES.minutes
|
||||||
|
|
||||||
Captain::Reservation
|
Captain::Reservation
|
||||||
.where(captain_unit_id: template.captain_unit_id)
|
.joins(:conversation)
|
||||||
|
.where(conversations: { inbox_id: template.inbox_id })
|
||||||
.where(status: Captain::Reservation.statuses.slice(:confirmed, :active).values)
|
.where(status: Captain::Reservation.statuses.slice(:confirmed, :active).values)
|
||||||
.where(check_in_at: window_start..window_end)
|
.where(check_in_at: window_start..window_end)
|
||||||
.where.not(conversation_id: nil)
|
.where.not(conversation_id: nil)
|
||||||
.where(
|
.where(
|
||||||
"NOT (metadata->'notified_templates' @> ?::jsonb)",
|
"NOT (captain_reservations.metadata->'notified_templates' @> ?::jsonb)",
|
||||||
"[#{template.id}]"
|
"[#{template.id}]"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -11,21 +11,20 @@
|
|||||||
# timing_minutes :integer default(10), not null
|
# timing_minutes :integer default(10), not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# captain_unit_id :bigint not null
|
# inbox_id :bigint not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# idx_notif_templates_unit_active (captain_unit_id,active)
|
# idx_notif_templates_inbox_active (inbox_id,active)
|
||||||
# index_captain_notification_templates_on_captain_unit_id (captain_unit_id)
|
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
# fk_rails_... (captain_unit_id => captain_units.id)
|
# fk_rails_... (inbox_id => inboxes.id)
|
||||||
#
|
#
|
||||||
class Captain::NotificationTemplate < ApplicationRecord
|
class Captain::NotificationTemplate < ApplicationRecord
|
||||||
self.table_name = 'captain_notification_templates'
|
self.table_name = 'captain_notification_templates'
|
||||||
|
|
||||||
belongs_to :unit, class_name: 'Captain::Unit', foreign_key: 'captain_unit_id', inverse_of: :notification_templates
|
belongs_to :inbox, inverse_of: :captain_notification_templates
|
||||||
|
|
||||||
enum timing_direction: { before: 0, after: 1 }
|
enum timing_direction: { before: 0, after: 1 }
|
||||||
|
|
||||||
@ -33,9 +32,9 @@ class Captain::NotificationTemplate < ApplicationRecord
|
|||||||
validates :content, presence: true
|
validates :content, presence: true
|
||||||
validates :timing_minutes, presence: true, numericality: { greater_than: 0 }
|
validates :timing_minutes, presence: true, numericality: { greater_than: 0 }
|
||||||
validates :timing_direction, presence: true
|
validates :timing_direction, presence: true
|
||||||
validates :captain_unit_id, presence: true
|
validates :inbox_id, presence: true
|
||||||
|
|
||||||
scope :active, -> { where(active: true) }
|
scope :active, -> { where(active: true) }
|
||||||
scope :ordered, -> { order(:position, :id) }
|
scope :ordered, -> { order(:position, :id) }
|
||||||
scope :for_unit, ->(unit_id) { where(captain_unit_id: unit_id) }
|
scope :for_inbox, ->(inbox_id) { where(inbox_id: inbox_id) }
|
||||||
end
|
end
|
||||||
|
|||||||
@ -54,8 +54,6 @@ class Captain::Unit < ApplicationRecord
|
|||||||
has_many :pix_charges, class_name: 'Captain::PixCharge', dependent: :restrict_with_error
|
has_many :pix_charges, class_name: 'Captain::PixCharge', dependent: :restrict_with_error
|
||||||
has_many :gallery_items, class_name: 'Captain::GalleryItem', foreign_key: :captain_unit_id, inverse_of: :captain_unit,
|
has_many :gallery_items, class_name: 'Captain::GalleryItem', foreign_key: :captain_unit_id, inverse_of: :captain_unit,
|
||||||
dependent: :destroy
|
dependent: :destroy
|
||||||
has_many :notification_templates, class_name: 'Captain::NotificationTemplate', foreign_key: :captain_unit_id,
|
|
||||||
inverse_of: :unit, dependent: :destroy
|
|
||||||
|
|
||||||
encrypts :inter_client_secret
|
encrypts :inter_client_secret
|
||||||
encrypts :inter_account_number
|
encrypts :inter_account_number
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user