iachat/app/javascript/dashboard/components/widgets/AutomationActionScheduledMessageInput.vue
Gabriel Jablonski 42b2530a53
fix: update default delay for scheduled messages in automation (#209)
* fix: update default delay for scheduled messages in automation

* fix: set default delay for scheduled messages to 24 hours in automation
2026-02-04 18:23:25 -03:00

315 lines
8.8 KiB
Vue

<script setup>
import { computed, ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import FileUpload from 'vue-upload-component';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import WhatsappTemplates from 'dashboard/components/widgets/conversation/WhatsappTemplates/Modal.vue';
import { DURATION_UNITS } from 'dashboard/components-next/input/constants';
import { DEFAULT_SCHEDULED_MESSAGE_DELAY_MINUTES } from 'dashboard/routes/dashboard/settings/automation/constants.js';
const props = defineProps({
modelValue: {
type: [Object, Array],
default: () => ({}),
},
initialFileName: {
type: String,
default: '',
},
conditions: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:modelValue']);
const MAX_DELAY_MINUTES = 999 * 24 * 60; // 999 days
const { t } = useI18n();
const store = useStore();
const showWhatsAppTemplatesModal = ref(false);
const normalizedParams = computed(() => {
const value = props.modelValue;
if (Array.isArray(value)) {
const first = value[0];
return typeof first === 'object' && first !== null ? first : {};
}
return typeof value === 'object' && value !== null ? value : {};
});
const updateParams = updates => {
const newParams = { ...normalizedParams.value, ...updates };
emit('update:modelValue', [newParams]);
};
const content = computed({
get: () => {
const value = normalizedParams.value.content;
return typeof value === 'string' ? value : '';
},
set: value => updateParams({ content: value }),
});
const delayMinutes = computed({
get: () =>
normalizedParams.value.delay_minutes ??
DEFAULT_SCHEDULED_MESSAGE_DELAY_MINUTES,
set: value => {
const numValue = Math.min(
Math.max(1, Number(value) || 1),
MAX_DELAY_MINUTES
);
updateParams({ delay_minutes: numValue });
},
});
const delayUnit = ref(DURATION_UNITS.MINUTES);
const detectUnit = minutes => {
const m = Number(minutes) || 0;
if (m === 0) return DURATION_UNITS.DAYS;
if (m % (24 * 60) === 0) return DURATION_UNITS.DAYS;
if (m % 60 === 0) return DURATION_UNITS.HOURS;
return DURATION_UNITS.MINUTES;
};
onBeforeMount(() => {
// Normalize delay_minutes for existing automations with invalid/out-of-range values
// For new actions, resetAction in useAutomation.js sets the default
const rawDelay =
normalizedParams.value.delay_minutes ??
DEFAULT_SCHEDULED_MESSAGE_DELAY_MINUTES;
const clampedDelay = Math.min(
Math.max(1, Number(rawDelay) || 1),
MAX_DELAY_MINUTES
);
// Only emit if the value needs normalization to avoid unnecessary updates
if (clampedDelay !== normalizedParams.value.delay_minutes) {
updateParams({ delay_minutes: clampedDelay });
}
delayUnit.value = detectUnit(clampedDelay);
});
// Attachment handling
const attachmentState = ref('idle'); // 'idle' | 'uploading' | 'uploaded' | 'failed'
const attachmentFileName = ref(props.initialFileName || '');
const hasAttachment = computed(() => {
const blobId = normalizedParams.value.blob_id;
return !!blobId;
});
const attachmentLabel = computed(() => {
if (attachmentState.value === 'uploading') {
return t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
}
if (attachmentFileName.value) {
return attachmentFileName.value;
}
return t('AUTOMATION.ATTACHMENT.LABEL_IDLE');
});
const onFileUpload = async file => {
if (!file?.file) return;
attachmentState.value = 'uploading';
try {
const id = await store.dispatch('automations/uploadAttachment', file.file);
updateParams({ blob_id: id });
attachmentState.value = 'uploaded';
attachmentFileName.value = file.file.name;
} catch {
attachmentState.value = 'failed';
useAlert(t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
}
};
const clearAttachment = () => {
updateParams({ blob_id: null });
attachmentState.value = 'idle';
attachmentFileName.value = '';
};
// Template params handling
const templateParams = computed(
() => normalizedParams.value.template_params || null
);
const hasTemplate = computed(
() => templateParams.value && Object.keys(templateParams.value).length > 0
);
const templateName = computed(() => {
return templateParams.value?.name || templateParams.value?.id || null;
});
// Extract inbox IDs from conditions
const inboxIdsFromConditions = computed(() => {
const inboxConditions = props.conditions.filter(
condition => condition.attribute_key === 'inbox_id'
);
const ids = [];
inboxConditions.forEach(condition => {
const values = condition.values;
if (Array.isArray(values)) {
values.forEach(v => {
const id = typeof v === 'object' ? v.id : v;
if (id) ids.push(Number(id));
});
} else if (values) {
const id = typeof values === 'object' ? values.id : values;
if (id) ids.push(Number(id));
}
});
return ids;
});
// Get the first inbox ID that has templates (for the modal)
const inboxIdForTemplates = computed(() => {
const inboxWithTemplates = inboxIdsFromConditions.value.find(inboxId => {
const templates =
store.getters['inboxes/getWhatsAppTemplates'](inboxId) || [];
return templates.length > 0;
});
return inboxWithTemplates ?? null;
});
// Check if any inbox has WhatsApp templates
const showWhatsappTemplates = computed(() => {
return inboxIdForTemplates.value !== null;
});
// Show action buttons only when no attachment, no template, and not uploading
const isUploading = computed(() => attachmentState.value === 'uploading');
const showActionButtons = computed(
() => !hasAttachment.value && !hasTemplate.value && !isUploading.value
);
const openWhatsAppTemplatesModal = () => {
showWhatsAppTemplatesModal.value = true;
};
const hideWhatsAppTemplatesModal = () => {
showWhatsAppTemplatesModal.value = false;
};
const onTemplateSelect = messagePayload => {
updateParams({
template_params: messagePayload.templateParams,
content: messagePayload.message,
});
hideWhatsAppTemplatesModal();
};
const clearTemplate = () => {
updateParams({
template_params: null,
content: '',
});
};
</script>
<template>
<div class="mt-2 flex flex-col gap-1">
<div class="flex flex-col gap-1">
<label class="text-xs text-n-slate-11">
{{ $t('AUTOMATION.ACTION.SCHEDULED_MESSAGE_DELAY_LABEL') }}
</label>
<div class="flex items-center gap-2">
<!-- allow 1 min to 999 days -->
<DurationInput
v-model:model-value="delayMinutes"
v-model:unit="delayUnit"
:min="1"
:max="MAX_DELAY_MINUTES"
/>
</div>
</div>
<WootMessageEditor
v-model="content"
rows="4"
enable-variables
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
class="action-message"
:class="hasTemplate ? 'opacity-60 cursor-not-allowed' : ''"
:disabled="hasTemplate"
/>
<div
v-if="isUploading"
class="flex items-center gap-2 text-xs text-n-slate-11"
>
<NextButton
ghost
xs
icon="i-lucide-paperclip"
:label="t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING')"
is-loading
disabled
/>
</div>
<div v-if="showActionButtons" class="flex items-center gap-2">
<FileUpload
:multiple="false"
:maximum="1"
class="cursor-pointer [&:hover_button]:bg-n-alpha-2 [&:hover_button]:text-n-slate-12"
@input-file="onFileUpload"
>
<NextButton
ghost
xs
icon="i-lucide-paperclip"
:label="t('AUTOMATION.ACTION.ATTACHMENT_ADD')"
class="pointer-events-none"
/>
</FileUpload>
<NextButton
v-if="showWhatsappTemplates"
ghost
xs
icon="i-lucide-zap"
:label="t('AUTOMATION.ACTION.TEMPLATE_SELECT')"
@click="openWhatsAppTemplatesModal"
/>
</div>
<div
v-if="hasAttachment"
class="flex items-center gap-2 text-xs text-n-slate-11"
>
<span>{{ attachmentLabel }}</span>
<NextButton ghost xs slate icon="i-lucide-x" @click="clearAttachment" />
</div>
<div
v-if="hasTemplate"
class="flex items-center gap-2 text-xs text-n-slate-11"
>
<span>
{{ t('AUTOMATION.ACTION.TEMPLATE_SELECTED', { name: templateName }) }}
</span>
<NextButton ghost xs slate icon="i-lucide-x" @click="clearTemplate" />
</div>
<WhatsappTemplates
v-model:show="showWhatsAppTemplatesModal"
:inbox-id="inboxIdForTemplates"
:send-button-label="t('AUTOMATION.ACTION.TEMPLATE_USE')"
@on-send="onTemplateSelect"
@cancel="hideWhatsAppTemplatesModal"
/>
</div>
</template>