Add automatic VoIP call summaries and sync progress checklist.

When a PBX webhook includes recordingUrl or transcript, queue STT and AI to create a call_result note; mark API, bulk SMS, WooCommerce, and VoIP as done and defer online payments.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-27 15:06:41 +03:30
co-authored by Cursor
parent c06c92b333
commit c5c903d4b4
13 changed files with 615 additions and 22 deletions
+85 -1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
import { RouterLink } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import type {
@@ -46,6 +47,21 @@ const statusLabel: Record<string, string> = {
failed: 'ناموفق',
};
const summaryLabel: Record<string, string> = {
none: '—',
pending: 'در صف',
processing: 'در حال ساخت',
done: 'آماده',
failed: 'ناموفق',
skipped: 'رد شده',
};
function noteLink(c: VoipCallLogDto): string | null {
if (!c.summaryNoteId) return null;
if (c.personId) return `/contacts/people/${c.personId}`;
return '/notes';
}
async function load() {
loading.value = true;
error.value = '';
@@ -123,7 +139,7 @@ onMounted(() => {
<div class="page">
<UiPageHeader
title="تلفن سازمانی"
description="ثبت خودکار تماس‌های سانترال روی پرونده مشتری و اعلان تماس ورودی"
description="ثبت خودکار تماس، خلاصه گفتگو و اعلان تماس ورودی"
>
<template #actions>
<UiButton variant="secondary" :disabled="loading" @click="load">
@@ -145,6 +161,15 @@ onMounted(() => {
<code>status</code>،
<code>from</code>،
<code>to</code>.
برای خلاصه خودکار یکی از
<code>recordingUrl</code>
(لینک فایل ضبط) یا
<code>transcript</code>
(متن آماده) را بفرستید؛ پس از
<code>completed</code>
/
<code>answered</code>
صف خلاصه ساخته می‌شود.
امضا اختیاری با هدر
<code>X-Voip-Signature</code>
(HMAC-SHA256 هگز).
@@ -213,6 +238,7 @@ onMounted(() => {
<th>از / به</th>
<th>مشتری</th>
<th>مدت</th>
<th>خلاصه</th>
<th>زمان</th>
</tr>
</thead>
@@ -229,6 +255,27 @@ onMounted(() => {
: '—'
}}
</td>
<td class="summary-cell">
<span
class="summary-badge"
:data-status="c.summaryStatus || 'none'"
>
{{ summaryLabel[c.summaryStatus] ?? c.summaryStatus ?? '—' }}
</span>
<p v-if="c.summary" class="summary-preview">
{{ c.summary.slice(0, 120) }}{{ c.summary.length > 120 ? '…' : '' }}
</p>
<p v-if="c.summaryError && c.summaryStatus === 'failed'" class="warn">
{{ c.summaryError }}
</p>
<RouterLink
v-if="noteLink(c)"
class="note-link"
:to="noteLink(c)!"
>
مشاهده در پرونده
</RouterLink>
</td>
<td>{{ formatJalaliNumeric(new Date(c.startedAt)) }}</td>
</tr>
</tbody>
@@ -310,11 +357,48 @@ td {
text-align: right;
padding: 0.55rem;
border-bottom: 1px solid #eef2f7;
vertical-align: top;
}
th {
color: #64748b;
font-size: 0.8rem;
}
.summary-cell {
max-width: 16rem;
}
.summary-badge {
display: inline-block;
font-size: 0.78rem;
padding: 0.15rem 0.45rem;
border-radius: 6px;
background: #f1f5f9;
color: #475569;
}
.summary-badge[data-status='done'] {
background: #ecfdf5;
color: #047857;
}
.summary-badge[data-status='pending'],
.summary-badge[data-status='processing'] {
background: #eff6ff;
color: #1d4ed8;
}
.summary-badge[data-status='failed'] {
background: #fef2f2;
color: #b91c1c;
}
.summary-preview {
margin: 0.35rem 0 0;
font-size: 0.82rem;
color: #334155;
line-height: 1.5;
}
.note-link {
display: inline-block;
margin-top: 0.35rem;
font-size: 0.8rem;
color: #0f766e;
}
@media (max-width: 800px) {
.form-grid {
grid-template-columns: 1fr;