feat: log outbound SMS and show history in compose modal
Persist manual, campaign, and automation SMS; expose history APIs for people and leads. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import type { OutboundSmsHistoryResponse, OutboundSmsLogDto } from '@mento/shared';
|
||||
import { MESSAGE_AUTOMATION_FIELDS, MESSAGE_AUTOMATION_SMS_SEGMENT } from '@mento/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatJalaliDay, toFaDigits } from '@/lib/jalali';
|
||||
import { formatJalaliDay, formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||
import { UiButton, UiModal } from '@/components/ui';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
endpoint: string;
|
||||
historyEndpoint?: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
recordTitle?: string;
|
||||
@@ -19,6 +21,7 @@ const props = withDefaults(
|
||||
nest?: boolean;
|
||||
}>(),
|
||||
{
|
||||
historyEndpoint: '',
|
||||
recordTitle: '',
|
||||
company: '',
|
||||
title: 'ارسال پیامک',
|
||||
@@ -38,6 +41,8 @@ const error = ref('');
|
||||
const done = ref('');
|
||||
const fieldsOpen = ref(false);
|
||||
const bodyEl = ref<HTMLTextAreaElement | null>(null);
|
||||
const history = ref<OutboundSmsLogDto[]>([]);
|
||||
const historyLoading = ref(false);
|
||||
|
||||
const preview = computed(() => fill(text.value));
|
||||
const count = computed(() => text.value.length);
|
||||
@@ -52,9 +57,26 @@ watch(
|
||||
done.value = '';
|
||||
fieldsOpen.value = false;
|
||||
busy.value = false;
|
||||
void loadHistory();
|
||||
},
|
||||
);
|
||||
|
||||
async function loadHistory() {
|
||||
history.value = [];
|
||||
if (!props.historyEndpoint) return;
|
||||
historyLoading.value = true;
|
||||
try {
|
||||
const { data } = await api.get<OutboundSmsHistoryResponse>(props.historyEndpoint, {
|
||||
params: { limit: 50 },
|
||||
});
|
||||
history.value = data.items ?? [];
|
||||
} catch {
|
||||
history.value = [];
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fill(source: string) {
|
||||
const map: Record<string, string> = {
|
||||
نام: props.name,
|
||||
@@ -108,12 +130,17 @@ async function send() {
|
||||
await api.post(props.endpoint, { text: text.value });
|
||||
done.value = 'پیامک ارسال شد.';
|
||||
emit('sent');
|
||||
void loadHistory();
|
||||
} catch (e) {
|
||||
error.value = apiMessage(e, 'ارسال پیامک ناموفق بود.');
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function statusLabel(status: OutboundSmsLogDto['status']) {
|
||||
return status === 'failed' ? 'ناموفق' : 'ارسالشده';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -147,6 +174,26 @@ async function send() {
|
||||
<p>{{ preview || 'متن پیام هنوز نوشته نشده است.' }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="historyEndpoint" class="history">
|
||||
<header>
|
||||
<strong>پیامکهای قبلی</strong>
|
||||
<span v-if="historyLoading" class="muted">در حال بارگذاری…</span>
|
||||
</header>
|
||||
<p v-if="!historyLoading && !history.length" class="muted empty">
|
||||
هنوز پیامکی ثبت نشده است.
|
||||
</p>
|
||||
<ul v-else class="history-list">
|
||||
<li v-for="item in history" :key="item.id">
|
||||
<div class="meta">
|
||||
<span class="src">{{ item.sourceLabel }}</span>
|
||||
<span :class="['st', item.status]">{{ statusLabel(item.status) }}</span>
|
||||
<time>{{ formatJalaliNumeric(new Date(item.createdAt)) }}</time>
|
||||
</div>
|
||||
<p>{{ item.body }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
<p v-if="done" class="ok">{{ done }}</p>
|
||||
|
||||
@@ -271,6 +318,80 @@ textarea {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.history {
|
||||
margin-top: 0.9rem;
|
||||
border-top: 1px solid #eef2f7;
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
.history header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.history header strong {
|
||||
font-size: 0.9rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.history-list li {
|
||||
padding: 0.55rem 0.65rem;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 10px;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.history-list .meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem 0.65rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.72rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.history-list .src {
|
||||
font-weight: 700;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.history-list .st.failed {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.history-list .st.sent {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.history-list p {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 0.86rem;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.muted,
|
||||
.empty {
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.err,
|
||||
.ok {
|
||||
margin: 0.55rem 0 0;
|
||||
|
||||
Reference in New Issue
Block a user