feat(voip): make Daftaresoma setup inbound-first and filter calls by line
Drop required from_number for webhook logging, persist CalleeExtension as lineExtension, and add direction/status/line filters on the calls list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "lineExtension" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipCallLog_organizationId_lineExtension_idx" ON "VoipCallLog"("organizationId", "lineExtension");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipCallLog_organizationId_status_idx" ON "VoipCallLog"("organizationId", "status");
|
||||||
@@ -2997,6 +2997,8 @@ model VoipCallLog {
|
|||||||
status String @default("completed")
|
status String @default("completed")
|
||||||
fromNumber String?
|
fromNumber String?
|
||||||
toNumber String?
|
toNumber String?
|
||||||
|
/// خط/داخلی سازمان که تماس روی آن بوده (مثلاً CalleeExtension دفتر شما)
|
||||||
|
lineExtension String?
|
||||||
personId String?
|
personId String?
|
||||||
activityId String?
|
activityId String?
|
||||||
durationSec Int?
|
durationSec Int?
|
||||||
@@ -3021,6 +3023,8 @@ model VoipCallLog {
|
|||||||
|
|
||||||
@@unique([connectionId, externalCallId])
|
@@unique([connectionId, externalCallId])
|
||||||
@@index([organizationId, syncedAt])
|
@@index([organizationId, syncedAt])
|
||||||
|
@@index([organizationId, lineExtension])
|
||||||
|
@@index([organizationId, status])
|
||||||
@@index([personId])
|
@@index([personId])
|
||||||
@@index([activityId])
|
@@index([activityId])
|
||||||
@@index([summaryStatus, summaryNextAttemptAt])
|
@@index([summaryStatus, summaryNextAttemptAt])
|
||||||
|
|||||||
@@ -67,15 +67,27 @@ export class VoipCallsService {
|
|||||||
|
|
||||||
async listRecent(
|
async listRecent(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
connectionId?: string,
|
opts?: {
|
||||||
|
connectionId?: string;
|
||||||
|
direction?: string;
|
||||||
|
status?: string;
|
||||||
|
lineExtension?: string;
|
||||||
|
},
|
||||||
): Promise<VoipCallLogDto[]> {
|
): Promise<VoipCallLogDto[]> {
|
||||||
const rows = await this.prisma.voipCallLog.findMany({
|
const rows = await this.prisma.voipCallLog.findMany({
|
||||||
where: {
|
where: {
|
||||||
organizationId,
|
organizationId,
|
||||||
...(connectionId ? { connectionId } : {}),
|
...(opts?.connectionId ? { connectionId: opts.connectionId } : {}),
|
||||||
|
...(opts?.direction === 'inbound' || opts?.direction === 'outbound'
|
||||||
|
? { direction: opts.direction }
|
||||||
|
: {}),
|
||||||
|
...(opts?.status?.trim() ? { status: opts.status.trim() } : {}),
|
||||||
|
...(opts?.lineExtension?.trim()
|
||||||
|
? { lineExtension: opts.lineExtension.trim() }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
orderBy: { syncedAt: 'desc' },
|
orderBy: { syncedAt: 'desc' },
|
||||||
take: 50,
|
take: 100,
|
||||||
});
|
});
|
||||||
const personIds = rows
|
const personIds = rows
|
||||||
.map((r) => r.personId)
|
.map((r) => r.personId)
|
||||||
@@ -97,6 +109,7 @@ export class VoipCallsService {
|
|||||||
status: r.status,
|
status: r.status,
|
||||||
fromNumber: r.fromNumber,
|
fromNumber: r.fromNumber,
|
||||||
toNumber: r.toNumber,
|
toNumber: r.toNumber,
|
||||||
|
lineExtension: r.lineExtension ?? null,
|
||||||
personId: r.personId,
|
personId: r.personId,
|
||||||
personName: r.personId ? (nameById.get(r.personId) ?? null) : null,
|
personName: r.personId ? (nameById.get(r.personId) ?? null) : null,
|
||||||
activityId: r.activityId,
|
activityId: r.activityId,
|
||||||
@@ -133,6 +146,9 @@ export class VoipCallsService {
|
|||||||
const toDigits = digitsOnly(event.to ?? event.callee);
|
const toDigits = digitsOnly(event.to ?? event.callee);
|
||||||
const fromNumber = fromNormalized || fromDigits || null;
|
const fromNumber = fromNormalized || fromDigits || null;
|
||||||
const toNumber = toNormalized || toDigits || null;
|
const toNumber = toNormalized || toDigits || null;
|
||||||
|
const lineRaw = String(event.lineExtension ?? '').trim();
|
||||||
|
const lineExtension =
|
||||||
|
normalizePhone(lineRaw) || digitsOnly(lineRaw) || lineRaw || null;
|
||||||
const matchNumber = direction === 'inbound' ? fromNumber : toNumber;
|
const matchNumber = direction === 'inbound' ? fromNumber : toNumber;
|
||||||
const personId = await this.findPersonId(
|
const personId = await this.findPersonId(
|
||||||
conn.organizationId,
|
conn.organizationId,
|
||||||
@@ -199,6 +215,7 @@ export class VoipCallsService {
|
|||||||
status,
|
status,
|
||||||
fromNumber,
|
fromNumber,
|
||||||
toNumber,
|
toNumber,
|
||||||
|
lineExtension: lineExtension ?? existing.lineExtension,
|
||||||
personId: personId ?? existing.personId,
|
personId: personId ?? existing.personId,
|
||||||
activityId: activityId ?? existing.activityId,
|
activityId: activityId ?? existing.activityId,
|
||||||
durationSec,
|
durationSec,
|
||||||
@@ -219,6 +236,7 @@ export class VoipCallsService {
|
|||||||
status,
|
status,
|
||||||
fromNumber,
|
fromNumber,
|
||||||
toNumber,
|
toNumber,
|
||||||
|
lineExtension,
|
||||||
personId,
|
personId,
|
||||||
activityId,
|
activityId,
|
||||||
durationSec,
|
durationSec,
|
||||||
|
|||||||
@@ -52,14 +52,7 @@ export class VoipConnectionsService {
|
|||||||
if (provider === 'daftareshoma') {
|
if (provider === 'daftareshoma') {
|
||||||
const token = dto.apiToken?.trim();
|
const token = dto.apiToken?.trim();
|
||||||
if (!token) throw new BadRequestException('توکن API دفتر شما الزامی است');
|
if (!token) throw new BadRequestException('توکن API دفتر شما الزامی است');
|
||||||
if (!dto.callerExtension?.trim()) {
|
// fromNumber / callerExtension فقط برای تماس خروجی از CRM لازماند؛ ثبت وبهوک بدون آنها ممکن است
|
||||||
throw new BadRequestException('خط / داخلی سازمان الزامی است');
|
|
||||||
}
|
|
||||||
if (!dto.fromNumber?.trim()) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'شماره مبدأ (from_number) برای تماس خروجی الزامی است',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = dto.webhookSecret?.trim();
|
const secret = dto.webhookSecret?.trim();
|
||||||
|
|||||||
@@ -76,16 +76,17 @@ export function mapDaftaresomaWebhook(
|
|||||||
'From',
|
'From',
|
||||||
'fromNumber',
|
'fromNumber',
|
||||||
);
|
);
|
||||||
const callee = pick(
|
/** خط سازمانی که تماس روی آن آمده/رفته */
|
||||||
|
const lineExtension =
|
||||||
|
pick(obj, 'CalleeExtension', 'calleeExtension', 'Extension', 'extension') ||
|
||||||
|
undefined;
|
||||||
|
/** مقصد انتقال / شماره طرف مقابل روی خط */
|
||||||
|
const transferred = pick(
|
||||||
obj,
|
obj,
|
||||||
'TransferredToNumber',
|
'TransferredToNumber',
|
||||||
'transferredToNumber',
|
'transferredToNumber',
|
||||||
'CalleeExtension',
|
|
||||||
'calleeExtension',
|
|
||||||
'to',
|
|
||||||
'To',
|
|
||||||
'toNumber',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const startedAt =
|
const startedAt =
|
||||||
pick(obj, 'StartDateTime', 'startDateTime', 'startedAt') || undefined;
|
pick(obj, 'StartDateTime', 'startDateTime', 'startedAt') || undefined;
|
||||||
const endedAt =
|
const endedAt =
|
||||||
@@ -128,17 +129,28 @@ export function mapDaftaresomaWebhook(
|
|||||||
'recordId',
|
'recordId',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// inbound: from=مشتری، to=همکار/مقصد انتقال یا خود خط
|
||||||
|
// outbound: from=همکار، to=مشتری (TransferredToNumber)
|
||||||
|
let from = caller || undefined;
|
||||||
|
let to: string | undefined;
|
||||||
|
if (isOutbound) {
|
||||||
|
to = transferred || undefined;
|
||||||
|
} else {
|
||||||
|
to = transferred || lineExtension || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const event: VoipWebhookEvent = {
|
const event: VoipWebhookEvent = {
|
||||||
callId,
|
callId,
|
||||||
direction: isOutbound ? 'outbound' : 'inbound',
|
direction: isOutbound ? 'outbound' : 'inbound',
|
||||||
status,
|
status,
|
||||||
from: caller || undefined,
|
from,
|
||||||
to: callee || undefined,
|
to,
|
||||||
|
lineExtension,
|
||||||
startedAt,
|
startedAt,
|
||||||
endedAt,
|
endedAt,
|
||||||
durationSec,
|
durationSec,
|
||||||
caller: caller || undefined,
|
caller: from,
|
||||||
callee: callee || undefined,
|
callee: to,
|
||||||
};
|
};
|
||||||
if (recordingId) {
|
if (recordingId) {
|
||||||
event.recordingUrl = daftareshomaRecordingRef(recordingId);
|
event.recordingUrl = daftareshomaRecordingRef(recordingId);
|
||||||
|
|||||||
@@ -82,8 +82,16 @@ export class VoipController {
|
|||||||
recent(
|
recent(
|
||||||
@CurrentUser() user: { organizationId: string },
|
@CurrentUser() user: { organizationId: string },
|
||||||
@Query('connectionId') connectionId?: string,
|
@Query('connectionId') connectionId?: string,
|
||||||
|
@Query('direction') direction?: string,
|
||||||
|
@Query('status') status?: string,
|
||||||
|
@Query('lineExtension') lineExtension?: string,
|
||||||
) {
|
) {
|
||||||
return this.calls.listRecent(user.organizationId, connectionId);
|
return this.calls.listRecent(user.organizationId, {
|
||||||
|
connectionId,
|
||||||
|
direction,
|
||||||
|
status,
|
||||||
|
lineExtension,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('click-to-call')
|
@Post('click-to-call')
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ const ok = ref('');
|
|||||||
const items = ref<VoipConnectionDto[]>([]);
|
const items = ref<VoipConnectionDto[]>([]);
|
||||||
const calls = ref<VoipCallLogDto[]>([]);
|
const calls = ref<VoipCallLogDto[]>([]);
|
||||||
const tab = ref<TabKey>('daftareshoma');
|
const tab = ref<TabKey>('daftareshoma');
|
||||||
|
const callFilters = reactive({
|
||||||
|
direction: '',
|
||||||
|
status: '',
|
||||||
|
lineExtension: '',
|
||||||
|
});
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
label: 'سانترال اصلی',
|
label: 'سانترال اصلی',
|
||||||
@@ -54,6 +59,16 @@ const daftaItems = computed(() =>
|
|||||||
|
|
||||||
const activeDafta = computed(() => daftaItems.value[0] ?? null);
|
const activeDafta = computed(() => daftaItems.value[0] ?? null);
|
||||||
|
|
||||||
|
const knownLines = ref<string[]>([]);
|
||||||
|
const lineOptions = computed(() => {
|
||||||
|
const set = new Set(knownLines.value);
|
||||||
|
for (const c of calls.value) {
|
||||||
|
if (c.lineExtension) set.add(c.lineExtension);
|
||||||
|
}
|
||||||
|
if (callFilters.lineExtension) set.add(callFilters.lineExtension);
|
||||||
|
return [...set].sort();
|
||||||
|
});
|
||||||
|
|
||||||
const webhookUrlFor = (id: string | undefined) => {
|
const webhookUrlFor = (id: string | undefined) => {
|
||||||
if (!id) return '';
|
if (!id) return '';
|
||||||
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
||||||
@@ -88,16 +103,31 @@ function noteLink(c: VoipCallLogDto): string | null {
|
|||||||
return '/notes';
|
return '/notes';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCalls() {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (callFilters.direction) params.direction = callFilters.direction;
|
||||||
|
if (callFilters.status) params.status = callFilters.status;
|
||||||
|
if (callFilters.lineExtension) params.lineExtension = callFilters.lineExtension;
|
||||||
|
const callRes = await api.get<VoipCallLogDto[]>('/integrations/voip/calls', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
calls.value = callRes.data;
|
||||||
|
const merged = new Set(knownLines.value);
|
||||||
|
for (const c of callRes.data) {
|
||||||
|
if (c.lineExtension) merged.add(c.lineExtension);
|
||||||
|
}
|
||||||
|
knownLines.value = [...merged].sort();
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
const [connRes, callRes] = await Promise.all([
|
const [connRes] = await Promise.all([
|
||||||
api.get<VoipConnectionDto[]>('/integrations/voip/connections'),
|
api.get<VoipConnectionDto[]>('/integrations/voip/connections'),
|
||||||
api.get<VoipCallLogDto[]>('/integrations/voip/calls'),
|
loadCalls(),
|
||||||
]);
|
]);
|
||||||
items.value = connRes.data;
|
items.value = connRes.data;
|
||||||
calls.value = callRes.data;
|
|
||||||
fillDaftaFromActive();
|
fillDaftaFromActive();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (axios.isAxiosError(e) && e.response?.status === 403) {
|
if (axios.isAxiosError(e) && e.response?.status === 403) {
|
||||||
@@ -110,6 +140,18 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyCallFilters() {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
await loadCalls();
|
||||||
|
} catch {
|
||||||
|
error.value = 'فیلتر تماسها ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createGeneric() {
|
async function createGeneric() {
|
||||||
if (!form.label.trim()) {
|
if (!form.label.trim()) {
|
||||||
error.value = 'نام اتصال را وارد کنید.';
|
error.value = 'نام اتصال را وارد کنید.';
|
||||||
@@ -148,10 +190,6 @@ async function saveDafta() {
|
|||||||
error.value = 'توکن API دفتر شما را وارد کنید.';
|
error.value = 'توکن API دفتر شما را وارد کنید.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!daftaForm.fromNumber.trim() || !daftaForm.callerExtension.trim()) {
|
|
||||||
error.value = 'شماره مبدأ و خط / داخلی را وارد کنید.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
ok.value = '';
|
ok.value = '';
|
||||||
@@ -159,8 +197,8 @@ async function saveDafta() {
|
|||||||
if (activeDafta.value) {
|
if (activeDafta.value) {
|
||||||
await api.patch(`/integrations/voip/connections/${activeDafta.value.id}`, {
|
await api.patch(`/integrations/voip/connections/${activeDafta.value.id}`, {
|
||||||
label: daftaForm.label.trim(),
|
label: daftaForm.label.trim(),
|
||||||
fromNumber: daftaForm.fromNumber.trim(),
|
fromNumber: daftaForm.fromNumber.trim() || null,
|
||||||
callerExtension: daftaForm.callerExtension.trim(),
|
callerExtension: daftaForm.callerExtension.trim() || null,
|
||||||
enabled: daftaForm.enabled,
|
enabled: daftaForm.enabled,
|
||||||
...(daftaForm.apiToken.trim()
|
...(daftaForm.apiToken.trim()
|
||||||
? { apiToken: daftaForm.apiToken.trim() }
|
? { apiToken: daftaForm.apiToken.trim() }
|
||||||
@@ -173,8 +211,8 @@ async function saveDafta() {
|
|||||||
label: daftaForm.label.trim(),
|
label: daftaForm.label.trim(),
|
||||||
provider: 'daftareshoma',
|
provider: 'daftareshoma',
|
||||||
apiToken: daftaForm.apiToken.trim(),
|
apiToken: daftaForm.apiToken.trim(),
|
||||||
fromNumber: daftaForm.fromNumber.trim(),
|
fromNumber: daftaForm.fromNumber.trim() || null,
|
||||||
callerExtension: daftaForm.callerExtension.trim(),
|
callerExtension: daftaForm.callerExtension.trim() || null,
|
||||||
enabled: daftaForm.enabled,
|
enabled: daftaForm.enabled,
|
||||||
};
|
};
|
||||||
await api.post('/integrations/voip/connections', payload);
|
await api.post('/integrations/voip/connections', payload);
|
||||||
@@ -265,11 +303,11 @@ onMounted(async () => {
|
|||||||
<template v-if="tab === 'daftareshoma'">
|
<template v-if="tab === 'daftareshoma'">
|
||||||
<UiCard title="اتصال دفتر شما">
|
<UiCard title="اتصال دفتر شما">
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
توکن API را از پرتال دفتر شما (توسعهدهندگان → API) بگیرید.
|
برای ثبت همه تماسهای ورودی و خروجی فقط توکن و وبهوک لازم است.
|
||||||
پس از ذخیره، آدرس وبهوک زیر را در
|
توکن را از پرتال (توسعهدهندگان → API) بگیرید؛ بعد آدرس وبهوک را در
|
||||||
<strong>تنظیمات → هوک</strong>
|
<strong>تنظیمات → هوک</strong>
|
||||||
پرتال ثبت کنید و رویدادهای شروع/پایان تماس ورودی و پایان تماس خروجی را فعال کنید.
|
با رویدادهای شروع/پایان ورودی و پایان خروجی ثبت کنید.
|
||||||
برای تماس خروجی از CRM، توکن باید دسترسی «تماس خروجی» داشته باشد؛ برای خلاصه خودکار، «گزارش تماس» و «دریافت فایل تماس» هم لازم است.
|
فیلدهای شماره مبدأ و خط فقط برای «تماس از داخل CRM» اختیاریاند و بعداً قابل تکمیلاند.
|
||||||
</p>
|
</p>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<UiField label="نام">
|
<UiField label="نام">
|
||||||
@@ -285,17 +323,17 @@ onMounted(async () => {
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</UiField>
|
</UiField>
|
||||||
<UiField label="شماره مبدأ (from_number)">
|
<UiField label="شماره مبدأ — اختیاری (فقط تماس خروجی از CRM)">
|
||||||
<UiInput
|
<UiInput
|
||||||
v-model="daftaForm.fromNumber"
|
v-model="daftaForm.fromNumber"
|
||||||
placeholder="مثلاً 0912…"
|
placeholder="موبایل همکار برای click-to-call"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
</UiField>
|
</UiField>
|
||||||
<UiField label="خط / داخلی (caller_extension)">
|
<UiField label="خط پیشفرض — اختیاری (فقط تماس خروجی از CRM)">
|
||||||
<UiInput
|
<UiInput
|
||||||
v-model="daftaForm.callerExtension"
|
v-model="daftaForm.callerExtension"
|
||||||
placeholder="مثلاً 02128…"
|
placeholder="مثلاً 02128425990"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
/>
|
/>
|
||||||
</UiField>
|
</UiField>
|
||||||
@@ -427,6 +465,37 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UiCard title="آخرین تماسها">
|
<UiCard title="آخرین تماسها">
|
||||||
|
<div class="call-filters">
|
||||||
|
<UiField label="جهت">
|
||||||
|
<select v-model="callFilters.direction" class="filter-select" @change="applyCallFilters">
|
||||||
|
<option value="">همه</option>
|
||||||
|
<option value="inbound">ورودی</option>
|
||||||
|
<option value="outbound">خروجی</option>
|
||||||
|
</select>
|
||||||
|
</UiField>
|
||||||
|
<UiField label="وضعیت">
|
||||||
|
<select v-model="callFilters.status" class="filter-select" @change="applyCallFilters">
|
||||||
|
<option value="">همه</option>
|
||||||
|
<option value="ringing">زنگ</option>
|
||||||
|
<option value="answered">پاسخ</option>
|
||||||
|
<option value="missed">از دسترفته</option>
|
||||||
|
<option value="completed">تمامشده</option>
|
||||||
|
<option value="failed">ناموفق</option>
|
||||||
|
</select>
|
||||||
|
</UiField>
|
||||||
|
<UiField label="خط">
|
||||||
|
<select
|
||||||
|
v-model="callFilters.lineExtension"
|
||||||
|
class="filter-select"
|
||||||
|
@change="applyCallFilters"
|
||||||
|
>
|
||||||
|
<option value="">همه خطوط</option>
|
||||||
|
<option v-for="line in lineOptions" :key="line" :value="line">
|
||||||
|
{{ line }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</UiField>
|
||||||
|
</div>
|
||||||
<UiEmptyState
|
<UiEmptyState
|
||||||
v-if="!calls.length"
|
v-if="!calls.length"
|
||||||
title="تماسی نیست"
|
title="تماسی نیست"
|
||||||
@@ -441,6 +510,7 @@ onMounted(async () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>جهت</th>
|
<th>جهت</th>
|
||||||
<th>وضعیت</th>
|
<th>وضعیت</th>
|
||||||
|
<th>خط</th>
|
||||||
<th>از / به</th>
|
<th>از / به</th>
|
||||||
<th>مشتری</th>
|
<th>مشتری</th>
|
||||||
<th>مدت</th>
|
<th>مدت</th>
|
||||||
@@ -452,6 +522,7 @@ onMounted(async () => {
|
|||||||
<tr v-for="c in calls" :key="c.id">
|
<tr v-for="c in calls" :key="c.id">
|
||||||
<td>{{ c.direction === 'outbound' ? 'خروجی' : 'ورودی' }}</td>
|
<td>{{ c.direction === 'outbound' ? 'خروجی' : 'ورودی' }}</td>
|
||||||
<td>{{ statusLabel[c.status] ?? c.status }}</td>
|
<td>{{ statusLabel[c.status] ?? c.status }}</td>
|
||||||
|
<td dir="ltr">{{ c.lineExtension || '—' }}</td>
|
||||||
<td>{{ c.fromNumber || '—' }} → {{ c.toNumber || '—' }}</td>
|
<td>{{ c.fromNumber || '—' }} → {{ c.toNumber || '—' }}</td>
|
||||||
<td>{{ c.personName || '—' }}</td>
|
<td>{{ c.personName || '—' }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -536,6 +607,21 @@ onMounted(async () => {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin: 0.75rem 0 1rem;
|
margin: 0.75rem 0 1rem;
|
||||||
}
|
}
|
||||||
|
.call-filters {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.filter-select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
background: #fff;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
.list {
|
.list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -628,7 +714,8 @@ th {
|
|||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.form-grid {
|
.form-grid,
|
||||||
|
.call-filters {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5569,6 +5569,8 @@ export interface VoipCallLogDto {
|
|||||||
status: VoipCallStatus | string;
|
status: VoipCallStatus | string;
|
||||||
fromNumber: string | null;
|
fromNumber: string | null;
|
||||||
toNumber: string | null;
|
toNumber: string | null;
|
||||||
|
/** خط/داخلی سازمانی که تماس روی آن بوده */
|
||||||
|
lineExtension: string | null;
|
||||||
personId: string | null;
|
personId: string | null;
|
||||||
personName: string | null;
|
personName: string | null;
|
||||||
activityId: string | null;
|
activityId: string | null;
|
||||||
@@ -5595,6 +5597,8 @@ export interface VoipWebhookEvent {
|
|||||||
status: VoipCallStatus | string;
|
status: VoipCallStatus | string;
|
||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
|
/** خط/داخلی سازمان */
|
||||||
|
lineExtension?: string;
|
||||||
startedAt?: string;
|
startedAt?: string;
|
||||||
endedAt?: string;
|
endedAt?: string;
|
||||||
durationSec?: number;
|
durationSec?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user