progress in ai with chart and ticket system

This commit is contained in:
Hesabix 2025-07-19 16:28:55 +00:00
parent 26121aba26
commit 39a2846ff6
6 changed files with 2653 additions and 11 deletions

View file

@ -136,8 +136,129 @@ class wizardController extends AbstractController
}
}
// ارسال درخواست به سرویس هوش مصنوعی با تاریخچه
$result = $this->aiService->sendRequest($message, $business, $acc['user'], $conversationHistory);
// بررسی درخواست‌های مستقیم برای تیکت
$directTicketCommands = [
'وضعیت تیکت های من',
'وضعیت تیکت‌های من',
'آمار تیکت های من',
'آمار تیکت‌های من',
'تیکت های من',
'تیکت‌های من'
];
$listTicketCommands = [
'لیست تیکت های من',
'لیست تیکت‌های من',
'مشاهده تیکت های من',
'مشاهده تیکت‌های من',
'نمایش تیکت های من',
'نمایش تیکت‌های من'
];
$messageLower = mb_strtolower(trim($message), 'UTF-8');
$isDirectTicketRequest = false;
$isListTicketRequest = false;
foreach ($directTicketCommands as $command) {
if (strpos($messageLower, mb_strtolower($command, 'UTF-8')) !== false) {
$isDirectTicketRequest = true;
break;
}
}
foreach ($listTicketCommands as $command) {
if (strpos($messageLower, mb_strtolower($command, 'UTF-8')) !== false) {
$isListTicketRequest = true;
break;
}
}
if ($isDirectTicketRequest) {
// پردازش مستقیم درخواست تیکت
$ticketResult = $this->aiService->getTicketManagementService()->getTicketStatistics([], $business, $acc['user']);
if ($ticketResult['success']) {
$stats = $ticketResult['statistics'];
$responseContent = "📊 وضعیت تیکت‌های شما:\n\n";
$responseContent .= "📋 کل تیکت‌ها: {$stats['total']}\n";
$responseContent .= "⏳ در حال پیگیری: {$stats['pending']}\n";
$responseContent .= "✅ پاسخ داده شده: {$stats['answered']}\n";
$responseContent .= "🔒 خاتمه یافته: {$stats['closed']}\n\n";
if ($stats['total'] > 0) {
$responseContent .= "💡 برای مشاهده جزئیات تیکت‌ها، بگویید: 'لیست تیکت‌های من'";
} else {
$responseContent .= "💡 برای ایجاد تیکت جدید، بگویید: 'تیکت جدید'";
}
$result = [
'success' => true,
'response' => $responseContent,
'requires_action' => false,
'action_data' => null,
'debug_info' => [
'direct_ticket_request' => true,
'statistics' => $stats
]
];
} else {
$result = [
'success' => false,
'error' => $ticketResult['error'] ?? 'خطا در دریافت آمار تیکت‌ها'
];
}
} elseif ($isListTicketRequest) {
// پردازش مستقیم درخواست لیست تیکت‌ها
$ticketResult = $this->aiService->getTicketManagementService()->listUserTickets([], $business, $acc['user']);
if ($ticketResult['success']) {
$tickets = $ticketResult['tickets'];
$count = count($tickets);
$responseContent = "📋 لیست تیکت‌های شما ({$count} تیکت):\n";
$responseContent .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
if ($count > 0) {
foreach ($tickets as $index => $ticket) {
$statusIcon = $this->getTicketStatusIcon($ticket['state']);
$responseContent .= ($index + 1) . ". {$statusIcon} ";
$responseContent .= $ticket['title'] . "\n";
$responseContent .= " 📅 تاریخ: " . $ticket['date'] . "\n";
$responseContent .= " 🔢 کد: " . $ticket['code'] . "\n";
$responseContent .= " 📄 متن: " . $ticket['body'] . "\n";
if ($ticket['has_file']) {
$responseContent .= " 📎 فایل پیوست دارد\n";
}
$responseContent .= "\n";
}
$responseContent .= "💡 برای مشاهده جزئیات یک تیکت خاص، بگویید: 'مشاهده تیکت [کد تیکت]'";
} else {
$responseContent .= "❌ هیچ تیکتی یافت نشد.\n\n";
$responseContent .= "💡 برای ایجاد تیکت جدید، بگویید: 'تیکت جدید'";
}
$result = [
'success' => true,
'response' => $responseContent,
'requires_action' => false,
'action_data' => null,
'debug_info' => [
'direct_ticket_request' => true,
'tickets_count' => $count,
'tickets' => $tickets
]
];
} else {
$result = [
'success' => false,
'error' => $ticketResult['error'] ?? 'خطا در دریافت لیست تیکت‌ها'
];
}
} else {
// ارسال درخواست به سرویس هوش مصنوعی با تاریخچه
$result = $this->aiService->sendRequest($message, $business, $acc['user'], $conversationHistory);
}
if ($result['success']) {
// بررسی وجود کلید response
@ -619,6 +740,44 @@ class wizardController extends AbstractController
case 'search_persons':
$result = $this->aiService->getPersonManagementService()->searchPersons($commandParams, $business);
break;
case 'advanced_search_persons':
$result = $this->aiService->getPersonManagementService()->advancedSearchPersons($commandParams, $business);
break;
case 'search_by_mobile':
$result = $this->aiService->getPersonManagementService()->searchByMobile($commandParams, $business);
break;
case 'search_debtors':
$result = $this->aiService->getPersonManagementService()->searchDebtors($commandParams, $business);
break;
case 'search_creditors':
$result = $this->aiService->getPersonManagementService()->searchCreditors($commandParams, $business);
break;
// ابزارهای مدیریت تیکت
case 'create_ticket':
$result = $this->aiService->getTicketManagementService()->createTicket($commandParams, $business, $acc['user']);
break;
case 'list_tickets':
$result = $this->aiService->getTicketManagementService()->listUserTickets($commandParams, $business, $acc['user']);
break;
case 'view_ticket':
$result = $this->aiService->getTicketManagementService()->viewTicket($commandParams, $business, $acc['user']);
break;
case 'reply_ticket':
$result = $this->aiService->getTicketManagementService()->replyToTicket($commandParams, $business, $acc['user']);
break;
case 'search_tickets':
$result = $this->aiService->getTicketManagementService()->searchTickets($commandParams, $business, $acc['user']);
break;
case 'get_ticket_statistics':
$result = $this->aiService->getTicketManagementService()->getTicketStatistics($commandParams, $business, $acc['user']);
break;
default:
return $this->json([
'success' => false,
@ -938,4 +1097,17 @@ class wizardController extends AbstractController
]);
}
}
/**
* دریافت آیکون وضعیت تیکت
*/
private function getTicketStatusIcon(string $state): string
{
return match ($state) {
'در حال پیگیری' => '⏳',
'پاسخ داده شده' => '✅',
'خاتمه یافته' => '🔒',
default => '📋'
};
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,538 @@
<?php
namespace App\Service\AI;
use App\Entity\Business;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\Log;
use App\Service\Provider;
use App\Service\Jdate;
/**
* سرویس مدیریت نمودارها و چارت‌ها
*/
class ChartService
{
private EntityManagerInterface $entityManager;
private Log $log;
private Provider $provider;
private Jdate $jdate;
public function __construct(EntityManagerInterface $entityManager, Log $log, Provider $provider, Jdate $jdate)
{
$this->entityManager = $entityManager;
$this->log = $log;
$this->provider = $provider;
$this->jdate = $jdate;
}
/**
* ایجاد نمودار بر اساس نوع و داده‌ها
*/
public function createChart(array $params, Business $business, $user): array
{
$chartType = $params['chart_type'] ?? 'bar';
$title = $params['title'] ?? 'نمودار';
$data = $params['data'] ?? [];
$options = $params['options'] ?? [];
if (empty($data)) {
return [
'success' => false,
'error' => 'داده‌ای برای نمودار ارائه نشده است'
];
}
// اعتبارسنجی نوع نمودار
$validChartTypes = ['bar', 'line', 'pie', 'doughnut', 'area', 'radar', 'scatter', 'bubble'];
if (!in_array($chartType, $validChartTypes)) {
return [
'success' => false,
'error' => "نوع نمودار '{$chartType}' پشتیبانی نمی‌شود. انواع مجاز: " . implode(', ', $validChartTypes)
];
}
// پردازش داده‌ها بر اساس نوع نمودار
$processedData = $this->processChartData($chartType, $data);
if (!$processedData['success']) {
return $processedData;
}
// ایجاد تنظیمات نمودار
$chartOptions = $this->buildChartOptions($chartType, $title, $processedData['data'], $options);
// ثبت لاگ
$this->log->insert(
'مدیریت نمودار',
"ایجاد نمودار {$chartType}: {$title}",
$user,
$business
);
return [
'success' => true,
'chart' => [
'type' => $chartType,
'title' => $title,
'options' => $chartOptions,
'data' => $processedData['data'],
'chart_id' => $this->generateChartId()
],
'message' => "نمودار {$title} با موفقیت ایجاد شد"
];
}
/**
* پردازش داده‌ها بر اساس نوع نمودار
*/
private function processChartData(string $chartType, array $data): array
{
switch ($chartType) {
case 'bar':
case 'line':
case 'area':
return $this->processBarLineData($data);
case 'pie':
case 'doughnut':
return $this->processPieData($data);
case 'radar':
return $this->processRadarData($data);
case 'scatter':
case 'bubble':
return $this->processScatterData($data);
default:
return [
'success' => false,
'error' => 'نوع نمودار پشتیبانی نمی‌شود'
];
}
}
/**
* پردازش داده‌های نمودار ستونی، خطی و ناحیه‌ای
*/
private function processBarLineData(array $data): array
{
// فرمت مورد انتظار: [['category' => 'عنوان', 'value' => عدد], ...]
// یا [['x' => 'عنوان', 'y' => عدد], ...]
$categories = [];
$values = [];
foreach ($data as $item) {
if (isset($item['category']) && isset($item['value'])) {
$categories[] = $item['category'];
$values[] = (float) $item['value'];
} elseif (isset($item['x']) && isset($item['y'])) {
$categories[] = $item['x'];
$values[] = (float) $item['y'];
} elseif (isset($item['label']) && isset($item['data'])) {
$categories[] = $item['label'];
$values[] = (float) $item['data'];
} else {
return [
'success' => false,
'error' => 'فرمت داده نامعتبر. فرمت مورد انتظار: [{"category": "عنوان", "value": عدد}, ...]'
];
}
}
return [
'success' => true,
'data' => [
'categories' => $categories,
'series' => [
[
'name' => 'داده‌ها',
'data' => $values
]
]
]
];
}
/**
* پردازش داده‌های نمودار دایره‌ای
*/
private function processPieData(array $data): array
{
// فرمت مورد انتظار: [['label' => 'عنوان', 'value' => عدد], ...]
// یا [['name' => 'عنوان', 'data' => عدد], ...]
$labels = [];
$values = [];
foreach ($data as $item) {
if (isset($item['label']) && isset($item['value'])) {
$labels[] = $item['label'];
$values[] = (float) $item['value'];
} elseif (isset($item['name']) && isset($item['data'])) {
$labels[] = $item['name'];
$values[] = (float) $item['data'];
} elseif (isset($item['category']) && isset($item['value'])) {
$labels[] = $item['category'];
$values[] = (float) $item['value'];
} else {
return [
'success' => false,
'error' => 'فرمت داده نامعتبر. فرمت مورد انتظار: [{"label": "عنوان", "value": عدد}, ...]'
];
}
}
return [
'success' => true,
'data' => [
'labels' => $labels,
'series' => $values
]
];
}
/**
* پردازش داده‌های نمودار راداری
*/
private function processRadarData(array $data): array
{
// فرمت مورد انتظار: [['category' => 'عنوان', 'value' => عدد], ...]
$categories = [];
$values = [];
foreach ($data as $item) {
if (isset($item['category']) && isset($item['value'])) {
$categories[] = $item['category'];
$values[] = (float) $item['value'];
} elseif (isset($item['label']) && isset($item['data'])) {
$categories[] = $item['label'];
$values[] = (float) $item['data'];
} else {
return [
'success' => false,
'error' => 'فرمت داده نامعتبر. فرمت مورد انتظار: [{"category": "عنوان", "value": عدد}, ...]'
];
}
}
return [
'success' => true,
'data' => [
'categories' => $categories,
'series' => [
[
'name' => 'داده‌ها',
'data' => $values
]
]
]
];
}
/**
* پردازش داده‌های نمودار پراکندگی
*/
private function processScatterData(array $data): array
{
// فرمت مورد انتظار: [['x' => عدد, 'y' => عدد], ...]
$series = [];
foreach ($data as $item) {
if (isset($item['x']) && isset($item['y'])) {
$series[] = [
'x' => (float) $item['x'],
'y' => (float) $item['y']
];
} elseif (isset($item['x_value']) && isset($item['y_value'])) {
$series[] = [
'x' => (float) $item['x_value'],
'y' => (float) $item['y_value']
];
} else {
return [
'success' => false,
'error' => 'فرمت داده نامعتبر. فرمت مورد انتظار: [{"x": عدد, "y": عدد}, ...]'
];
}
}
return [
'success' => true,
'data' => [
'series' => [
[
'name' => 'داده‌ها',
'data' => $series
]
]
]
];
}
/**
* ساخت تنظیمات نمودار
*/
private function buildChartOptions(string $chartType, string $title, array $data, array $customOptions = []): array
{
$baseOptions = [
'chart' => [
'id' => 'ai-chart-' . $this->generateChartId(),
'type' => $chartType,
'fontFamily' => "'Vazirmatn FD', Arial, sans-serif",
'toolbar' => [
'show' => true,
'tools' => [
'download' => true,
'selection' => true,
'zoom' => true,
'zoomin' => true,
'zoomout' => true,
'pan' => true,
'reset' => true
]
]
],
'title' => [
'text' => $title,
'align' => 'center',
'style' => [
'fontSize' => '16px',
'fontWeight' => 'bold',
'fontFamily' => "'Vazirmatn FD', Arial, sans-serif"
]
],
'colors' => ['#2196F3', '#4CAF50', '#FFC107', '#F44336', '#9C27B0', '#00BCD4', '#FF9800', '#795548', '#607D8B', '#E91E63'],
'legend' => [
'position' => 'bottom',
'fontSize' => '14px',
'fontFamily' => "'Vazirmatn FD', Arial, sans-serif"
],
'tooltip' => [
'theme' => 'light',
'style' => [
'fontSize' => '12px',
'fontFamily' => "'Vazirmatn FD', Arial, sans-serif"
]
],
'responsive' => [
[
'breakpoint' => 480,
'options' => [
'chart' => ['width' => '100%'],
'legend' => ['position' => 'bottom']
]
]
]
];
// اضافه کردن تنظیمات خاص بر اساس نوع نمودار
switch ($chartType) {
case 'bar':
case 'line':
case 'area':
if (isset($data['categories'])) {
$baseOptions['xaxis'] = [
'categories' => $data['categories'],
'labels' => [
'style' => [
'fontSize' => '12px',
'fontFamily' => "'Vazirmatn FD', Arial, sans-serif"
]
]
];
}
$baseOptions['yaxis'] = [
'labels' => [
'style' => [
'fontSize' => '12px',
'fontFamily' => "'Vazirmatn FD', Arial, sans-serif"
]
]
];
break;
case 'pie':
case 'doughnut':
if (isset($data['labels'])) {
$baseOptions['labels'] = $data['labels'];
}
break;
case 'radar':
if (isset($data['categories'])) {
$baseOptions['xaxis'] = [
'categories' => $data['categories']
];
}
break;
}
// ادغام تنظیمات سفارشی
return array_merge_recursive($baseOptions, $customOptions);
}
/**
* تولید شناسه منحصر به فرد برای نمودار
*/
private function generateChartId(): string
{
return 'chart_' . time() . '_' . rand(1000, 9999);
}
/**
* ابزار ایجاد نمودار ستونی
*/
public function createBarChart(array $params, Business $business, $user): array
{
$params['chart_type'] = 'bar';
return $this->createChart($params, $business, $user);
}
/**
* ابزار ایجاد نمودار خطی
*/
public function createLineChart(array $params, Business $business, $user): array
{
$params['chart_type'] = 'line';
return $this->createChart($params, $business, $user);
}
/**
* ابزار ایجاد نمودار دایره‌ای
*/
public function createPieChart(array $params, Business $business, $user): array
{
$params['chart_type'] = 'pie';
return $this->createChart($params, $business, $user);
}
/**
* ابزار ایجاد نمودار ناحیه‌ای
*/
public function createAreaChart(array $params, Business $business, $user): array
{
$params['chart_type'] = 'area';
return $this->createChart($params, $business, $user);
}
/**
* ابزار ایجاد نمودار راداری
*/
public function createRadarChart(array $params, Business $business, $user): array
{
$params['chart_type'] = 'radar';
return $this->createChart($params, $business, $user);
}
/**
* ابزار ایجاد نمودار پراکندگی
*/
public function createScatterChart(array $params, Business $business, $user): array
{
$params['chart_type'] = 'scatter';
return $this->createChart($params, $business, $user);
}
/**
* ابزار ایجاد نمودار دونات
*/
public function createDoughnutChart(array $params, Business $business, $user): array
{
$params['chart_type'] = 'doughnut';
return $this->createChart($params, $business, $user);
}
/**
* پردازش درخواست نمودار
*/
public function processRequest(string $message, Business $business, $user): array
{
// استخراج دستور از پیام
$command = $this->extractCommand($message);
if (!$command) {
return [
'success' => false,
'error' => 'دستور نامعتبر است. لطفاً واضح‌تر بیان کنید.',
'guide' => $this->getOperationsGuide()
];
}
// اجرای دستور
return $this->executeCommand($command, $business, $user);
}
/**
* استخراج دستور از پیام کاربر
*/
private function extractCommand(string $message): ?array
{
$message = mb_strtolower(trim($message), 'UTF-8');
// الگوهای دستورات
$patterns = [
'create_chart' => [
'/(?:ایجاد|ساخت|ساز)\s+(?:نمودار|چارت)\s+(?:ستونی|خطی|دایره‌ای|ناحیه‌ای|راداری|پراکندگی|دونات)\s+(?:با\s+عنوان\s+)?([^\n]+)/u',
'/(?:نمودار|چارت)\s+(?:ستونی|خطی|دایره‌ای|ناحیه‌ای|راداری|پراکندگی|دونات)\s+(?:با\s+عنوان\s+)?([^\n]+)\s+(?:ایجاد|ساخت|ساز)/u'
],
'bar_chart' => [
'/(?:نمودار|چارت)\s+ستونی\s+(?:برای|از)\s+([^\n]+)/u',
'/(?:ایجاد|ساخت)\s+نمودار\s+ستونی\s+(?:برای|از)\s+([^\n]+)/u'
],
'line_chart' => [
'/(?:نمودار|چارت)\s+خطی\s+(?:برای|از)\s+([^\n]+)/u',
'/(?:ایجاد|ساخت)\s+نمودار\s+خطی\s+(?:برای|از)\s+([^\n]+)/u'
],
'pie_chart' => [
'/(?:نمودار|چارت)\s+دایره‌ای\s+(?:برای|از)\s+([^\n]+)/u',
'/(?:ایجاد|ساخت)\s+نمودار\s+دایره‌ای\s+(?:برای|از)\s+([^\n]+)/u'
]
];
foreach ($patterns as $commandType => $commandPatterns) {
foreach ($commandPatterns as $pattern) {
if (preg_match($pattern, $message, $matches)) {
return [
'type' => $commandType,
'params' => $matches[1] ?? null
];
}
}
}
return null;
}
/**
* اجرای دستور
*/
private function executeCommand(array $command, Business $business, $user): array
{
// این بخش می‌تواند برای پردازش دستورات پیچیده‌تر توسعه یابد
return [
'success' => false,
'error' => 'دستور نمودار نیاز به داده‌های مشخص دارد'
];
}
/**
* راهنمای عملیات
*/
public function getOperationsGuide(): string
{
return "📊 راهنمای ایجاد نمودار:\n\n" .
"📈 نمودار ستونی: create_bar_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}\n" .
"📉 نمودار خطی: create_line_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}\n" .
"🥧 نمودار دایره‌ای: create_pie_chart{title:عنوان, data:[{label:عنوان, value:عدد}]}\n" .
"🌊 نمودار ناحیه‌ای: create_area_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}\n" .
"🎯 نمودار راداری: create_radar_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}\n" .
"🔵 نمودار پراکندگی: create_scatter_chart{title:عنوان, data:[{x:عدد, y:عدد}]}\n" .
"🍩 نمودار دونات: create_doughnut_chart{title:عنوان, data:[{label:عنوان, value:عدد}]}\n\n" .
"💡 مثال: 'نمودار ستونی فروش ماهانه با داده‌های [{\"category\": \"فروردین\", \"value\": 1000}]'";
}
}

View file

@ -338,6 +338,415 @@ class PersonTools
];
}
/**
* ابزار جستجوی پیشرفته اشخاص
*/
public function advancedSearchPersons(array $params, Business $business): array
{
$search = $params['search'] ?? '';
$limit = $params['limit'] ?? 20;
$searchType = $params['search_type'] ?? 'all'; // all, mobile, name, code, debtor, creditor
$queryBuilder = $this->entityManager->getRepository(Person::class)
->createQueryBuilder('p')
->leftJoin('p.hesabdariRows', 'hr')
->leftJoin('hr.doc', 'd')
->where('p.bid = :bid')
->setParameter('bid', $business);
// جستجو بر اساس نوع
switch ($searchType) {
case 'mobile':
$queryBuilder->andWhere('p.mobile LIKE :search OR p.tel LIKE :search')
->setParameter('search', "%{$search}%");
break;
case 'name':
$queryBuilder->andWhere('p.nikename LIKE :search OR p.name LIKE :search')
->setParameter('search', "%{$search}%");
break;
case 'code':
$queryBuilder->andWhere('p.code LIKE :search')
->setParameter('search', "%{$search}%");
break;
case 'debtor':
// بدهکاران (کسانی که تراز منفی دارند)
$dql = "
SELECT p, SUM(hr.bs - hr.bd) as balance
FROM App\Entity\Person p
LEFT JOIN p.hesabdariRows hr
WHERE p.bid = :bid
GROUP BY p.id
HAVING SUM(hr.bs - hr.bd) < 0
";
if (!empty($search)) {
$dql .= " AND (p.nikename LIKE :search OR p.name LIKE :search)";
}
$dql .= " ORDER BY balance ASC";
$query = $this->entityManager->createQuery($dql);
$query->setParameter('bid', $business);
if (!empty($search)) {
$query->setParameter('search', "%{$search}%");
}
$query->setMaxResults($limit);
$results = $query->getResult();
$result = [];
foreach ($results as $row) {
$person = $row[0];
$balance = $row['balance'];
$result[] = [
'id' => $person->getId(),
'code' => $person->getCode(),
'nikename' => $person->getNikename(),
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'email' => $person->getEmail(),
'address' => $person->getAddress(),
'balance' => $balance,
'balance_formatted' => number_format($balance) . ' ریال',
'status' => 'بدهکار'
];
}
return [
'success' => true,
'persons' => $result,
'count' => count($result),
'search_type' => $searchType
];
case 'creditor':
// بستانکاران (کسانی که تراز مثبت دارند)
$dql = "
SELECT p, SUM(hr.bs - hr.bd) as balance
FROM App\Entity\Person p
LEFT JOIN p.hesabdariRows hr
WHERE p.bid = :bid
GROUP BY p.id
HAVING SUM(hr.bs - hr.bd) > 0
";
if (!empty($search)) {
$dql .= " AND (p.nikename LIKE :search OR p.name LIKE :search)";
}
$dql .= " ORDER BY balance DESC";
$query = $this->entityManager->createQuery($dql);
$query->setParameter('bid', $business);
if (!empty($search)) {
$query->setParameter('search', "%{$search}%");
}
$query->setMaxResults($limit);
$results = $query->getResult();
$result = [];
foreach ($results as $row) {
$person = $row[0];
$balance = $row['balance'];
$result[] = [
'id' => $person->getId(),
'code' => $person->getCode(),
'nikename' => $person->getNikename(),
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'email' => $person->getEmail(),
'address' => $person->getAddress(),
'balance' => $balance,
'balance_formatted' => number_format($balance) . ' ریال',
'status' => 'بستانکار'
];
}
return [
'success' => true,
'persons' => $result,
'count' => count($result),
'search_type' => $searchType
];
case 'balanced':
// تسویه شده‌ها (کسانی که تراز صفر دارند)
$dql = "
SELECT p, SUM(hr.bs - hr.bd) as balance
FROM App\Entity\Person p
LEFT JOIN p.hesabdariRows hr
WHERE p.bid = :bid
GROUP BY p.id
HAVING SUM(hr.bs - hr.bd) = 0
";
if (!empty($search)) {
$dql .= " AND (p.nikename LIKE :search OR p.name LIKE :search)";
}
$dql .= " ORDER BY p.name ASC";
$query = $this->entityManager->createQuery($dql);
$query->setParameter('bid', $business);
if (!empty($search)) {
$query->setParameter('search', "%{$search}%");
}
$query->setMaxResults($limit);
$results = $query->getResult();
$result = [];
foreach ($results as $row) {
$person = $row[0];
$balance = $row['balance'];
$result[] = [
'id' => $person->getId(),
'code' => $person->getCode(),
'nikename' => $person->getNikename(),
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'email' => $person->getEmail(),
'address' => $person->getAddress(),
'balance' => $balance,
'balance_formatted' => number_format($balance) . ' ریال',
'status' => 'تسویه شده'
];
}
return [
'success' => true,
'persons' => $result,
'count' => count($result),
'search_type' => $searchType
];
default:
// جستجوی عمومی
if (!empty($search)) {
$queryBuilder->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search OR p.tel LIKE :search')
->setParameter('search', "%{$search}%");
}
}
$queryBuilder->groupBy('p.id')
->orderBy('p.name', 'ASC')
->setMaxResults($limit);
$persons = $queryBuilder->getQuery()->getResult();
$result = [];
foreach ($persons as $person) {
// محاسبه تراز
$balance = $this->calculatePersonBalance($person, $business);
$result[] = [
'id' => $person->getId(),
'code' => $person->getCode(),
'nikename' => $person->getNikename(),
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'email' => $person->getEmail(),
'address' => $person->getAddress(),
'balance' => $balance,
'balance_formatted' => number_format($balance) . ' ریال',
'status' => $balance > 0 ? 'بستانکار' : ($balance < 0 ? 'بدهکار' : 'تسویه شده')
];
}
return [
'success' => true,
'persons' => $result,
'count' => count($result),
'search_type' => $searchType
];
}
/**
* ابزار جستجو بر اساس شماره موبایل
*/
public function searchByMobile(array $params, Business $business): array
{
$mobile = $params['mobile'] ?? '';
if (empty($mobile)) {
return [
'success' => false,
'error' => 'شماره موبایل الزامی است'
];
}
$person = $this->entityManager->getRepository(Person::class)->findOneBy([
'mobile' => $mobile,
'bid' => $business
]);
if (!$person) {
return [
'success' => false,
'error' => "شخصی با شماره موبایل {$mobile} یافت نشد"
];
}
$balance = $this->calculatePersonBalance($person, $business);
return [
'success' => true,
'person' => [
'id' => $person->getId(),
'code' => $person->getCode(),
'nikename' => $person->getNikename(),
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'tel' => $person->getTel(),
'email' => $person->getEmail(),
'address' => $person->getAddress(),
'balance' => $balance,
'balance_formatted' => number_format($balance) . ' ریال',
'status' => $balance > 0 ? 'بستانکار' : ($balance < 0 ? 'بدهکار' : 'تسویه شده')
]
];
}
/**
* ابزار جستجوی بدهکاران
*/
public function searchDebtors(array $params, Business $business): array
{
$limit = $params['limit'] ?? 20;
$search = $params['search'] ?? '';
// استفاده از DQL برای محاسبه تراز
$dql = "
SELECT p, SUM(hr.bs - hr.bd) as balance
FROM App\Entity\Person p
LEFT JOIN p.hesabdariRows hr
WHERE p.bid = :bid
GROUP BY p.id
HAVING SUM(hr.bs - hr.bd) < 0
";
if (!empty($search)) {
$dql .= " AND (p.nikename LIKE :search OR p.name LIKE :search)";
}
$dql .= " ORDER BY balance ASC";
$query = $this->entityManager->createQuery($dql);
$query->setParameter('bid', $business);
if (!empty($search)) {
$query->setParameter('search', "%{$search}%");
}
$query->setMaxResults($limit);
$results = $query->getResult();
$result = [];
$totalDebt = 0;
foreach ($results as $row) {
$person = $row[0];
$balance = $row['balance'];
$result[] = [
'id' => $person->getId(),
'code' => $person->getCode(),
'nikename' => $person->getNikename(),
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'balance' => $balance,
'balance_formatted' => number_format($balance) . ' ریال',
'debt_amount' => abs($balance)
];
$totalDebt += abs($balance);
}
return [
'success' => true,
'persons' => $result,
'count' => count($result),
'total_debt' => $totalDebt
];
}
/**
* ابزار جستجوی بستانکاران
*/
public function searchCreditors(array $params, Business $business): array
{
$limit = $params['limit'] ?? 20;
$search = $params['search'] ?? '';
// استفاده از DQL برای محاسبه تراز
$dql = "
SELECT p, SUM(hr.bs - hr.bd) as balance
FROM App\Entity\Person p
LEFT JOIN p.hesabdariRows hr
WHERE p.bid = :bid
GROUP BY p.id
HAVING SUM(hr.bs - hr.bd) > 0
";
if (!empty($search)) {
$dql .= " AND (p.nikename LIKE :search OR p.name LIKE :search)";
}
$dql .= " ORDER BY balance DESC";
$query = $this->entityManager->createQuery($dql);
$query->setParameter('bid', $business);
if (!empty($search)) {
$query->setParameter('search', "%{$search}%");
}
$query->setMaxResults($limit);
$results = $query->getResult();
$result = [];
$totalCredit = 0;
foreach ($results as $row) {
$person = $row[0];
$balance = $row['balance'];
$result[] = [
'id' => $person->getId(),
'code' => $person->getCode(),
'nikename' => $person->getNikename(),
'name' => $person->getName(),
'mobile' => $person->getMobile(),
'balance' => $balance,
'balance_formatted' => number_format($balance) . ' ریال',
'credit_amount' => $balance
];
$totalCredit += $balance;
}
return [
'success' => true,
'persons' => $result,
'count' => count($result),
'total_credit' => $totalCredit
];
}
/**
* محاسبه تراز شخص
*/
private function calculatePersonBalance(Person $person, Business $business): float
{
$hesabdariRows = $this->entityManager->getRepository(\App\Entity\HesabdariRow::class)
->findBy(['person' => $person, 'bid' => $business]);
$balance = 0;
foreach ($hesabdariRows as $row) {
$balance += $row->getBs() - $row->getBd();
}
return $balance;
}
/**
* یافتن شخص با نام
*/
@ -424,6 +833,187 @@ class PersonManagementService
return $this->tools->searchPersons($params, $business);
}
/**
* جستجوی پیشرفته اشخاص
*/
public function advancedSearchPersons(array $params, Business $business): array
{
return $this->tools->advancedSearchPersons($params, $business);
}
/**
* جستجو بر اساس شماره موبایل
*/
public function searchByMobile(array $params, Business $business): array
{
return $this->tools->searchByMobile($params, $business);
}
/**
* جستجوی بدهکاران
*/
public function searchDebtors(array $params, Business $business): array
{
return $this->tools->searchDebtors($params, $business);
}
/**
* جستجوی بستانکاران
*/
public function searchCreditors(array $params, Business $business): array
{
return $this->tools->searchCreditors($params, $business);
}
/**
* تحلیل نتایج جستجو
*/
public function analyzeSearchResults(array $params, Business $business): array
{
$analysisType = $params['analysis_type'] ?? 'highest_debtor'; // highest_debtor, highest_creditor, statistics
$searchResults = $params['search_results'] ?? [];
if (empty($searchResults)) {
return [
'success' => false,
'error' => 'نتایج جستجو برای تحلیل موجود نیست'
];
}
switch ($analysisType) {
case 'highest_debtor':
return $this->findHighestDebtor($searchResults);
case 'highest_creditor':
return $this->findHighestCreditor($searchResults);
case 'statistics':
return $this->generateStatistics($searchResults);
default:
return [
'success' => false,
'error' => 'نوع تحلیل نامعتبر است'
];
}
}
/**
* یافتن شخص با بیشترین بدهی
*/
private function findHighestDebtor(array $searchResults): array
{
$highestDebtor = null;
$maxDebt = 0;
foreach ($searchResults as $person) {
$balance = $person['balance'] ?? 0;
if ($balance < 0 && abs($balance) > $maxDebt) {
$maxDebt = abs($balance);
$highestDebtor = $person;
}
}
if ($highestDebtor) {
return [
'success' => true,
'analysis_type' => 'highest_debtor',
'person' => $highestDebtor,
'debt_amount' => $maxDebt,
'debt_formatted' => number_format($maxDebt) . ' ریال',
'message' => "بیشترین بدهکار: {$highestDebtor['nikename']} با بدهی {$maxDebt} ریال"
];
} else {
return [
'success' => false,
'error' => 'هیچ بدهکاری یافت نشد'
];
}
}
/**
* یافتن شخص با بیشترین بستانکاری
*/
private function findHighestCreditor(array $searchResults): array
{
$highestCreditor = null;
$maxCredit = 0;
foreach ($searchResults as $person) {
$balance = $person['balance'] ?? 0;
if ($balance > 0 && $balance > $maxCredit) {
$maxCredit = $balance;
$highestCreditor = $person;
}
}
if ($highestCreditor) {
return [
'success' => true,
'analysis_type' => 'highest_creditor',
'person' => $highestCreditor,
'credit_amount' => $maxCredit,
'credit_formatted' => number_format($maxCredit) . ' ریال',
'message' => "بیشترین بستانکار: {$highestCreditor['nikename']} با بستانکاری {$maxCredit} ریال"
];
} else {
return [
'success' => false,
'error' => 'هیچ بستانکاری یافت نشد'
];
}
}
/**
* تولید آمار از نتایج جستجو
*/
private function generateStatistics(array $searchResults): array
{
$totalCount = count($searchResults);
$totalDebt = 0;
$totalCredit = 0;
$debtors = [];
$creditors = [];
$balanced = [];
foreach ($searchResults as $person) {
$balance = $person['balance'] ?? 0;
if ($balance < 0) {
$totalDebt += abs($balance);
$debtors[] = $person;
} elseif ($balance > 0) {
$totalCredit += $balance;
$creditors[] = $person;
} else {
$balanced[] = $person;
}
}
$avgDebt = count($debtors) > 0 ? $totalDebt / count($debtors) : 0;
$avgCredit = count($creditors) > 0 ? $totalCredit / count($creditors) : 0;
return [
'success' => true,
'analysis_type' => 'statistics',
'statistics' => [
'total_count' => $totalCount,
'debtors_count' => count($debtors),
'creditors_count' => count($creditors),
'balanced_count' => count($balanced),
'total_debt' => $totalDebt,
'total_credit' => $totalCredit,
'avg_debt' => $avgDebt,
'avg_credit' => $avgCredit,
'total_debt_formatted' => number_format($totalDebt) . ' ریال',
'total_credit_formatted' => number_format($totalCredit) . ' ریال',
'avg_debt_formatted' => number_format($avgDebt) . ' ریال',
'avg_credit_formatted' => number_format($avgCredit) . ' ریال'
],
'message' => "آمار کلی: {$totalCount} نفر - {$totalDebt} ریال بدهی کل - {$totalCredit} ریال بستانکاری کل"
];
}
/**
* پردازش درخواست مدیریت اشخاص (برای سازگاری با سیستم قدیمی)
*/
@ -553,4 +1143,6 @@ class PersonManagementService
- فقط اشخاص بدون تراکنش قابل حذف هستند
- تمام عملیات در لاگ ثبت می‌شود";
}
}

View file

@ -0,0 +1,519 @@
<?php
namespace App\Service\AI;
use App\Entity\Support;
use App\Entity\Business;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\Log;
use App\Service\Provider;
use App\Service\Jdate;
/**
* سرویس مدیریت تیکت‌های پشتیبانی
*/
class TicketManagementService
{
private EntityManagerInterface $entityManager;
private Log $log;
private Provider $provider;
private Jdate $jdate;
public function __construct(EntityManagerInterface $entityManager, Log $log, Provider $provider, Jdate $jdate)
{
$this->entityManager = $entityManager;
$this->log = $log;
$this->provider = $provider;
$this->jdate = $jdate;
}
/**
* ابزار ایجاد تیکت جدید
*/
public function createTicket(array $params, Business $business, $user): array
{
$title = $params['title'] ?? '';
$body = $params['body'] ?? '';
$priority = $params['priority'] ?? 'عادی'; // عادی، مهم، فوری
if (empty($title) || empty($body)) {
return [
'success' => false,
'error' => 'عنوان و متن تیکت الزامی است'
];
}
// تولید کد منحصر به فرد
$code = $this->generateTicketCode();
// ایجاد تیکت جدید
$ticket = new Support();
$ticket->setTitle($title)
->setBody($body)
->setDateSubmit(time())
->setSubmitter($user)
->setMain(0)
->setCode($code)
->setState('در حال پیگیری')
->setBid($business);
$this->entityManager->persist($ticket);
$this->entityManager->flush();
// ثبت لاگ
$this->log->insert(
'مدیریت تیکت',
"ایجاد تیکت جدید: {$title} (کد: {$code})",
$user,
$business
);
return [
'success' => true,
'message' => "تیکت با موفقیت ایجاد شد. کد تیکت: {$code}",
'ticket' => [
'id' => $ticket->getId(),
'code' => $ticket->getCode(),
'title' => $ticket->getTitle(),
'state' => $ticket->getState(),
'date' => $this->jdate->jdate('Y/m/d H:i', $ticket->getDateSubmit())
]
];
}
/**
* ابزار مشاهده تیکت‌های کاربر
*/
public function listUserTickets(array $params, Business $business, $user): array
{
$state = $params['state'] ?? null;
$limit = (int) ($params['limit'] ?? 10);
$page = (int) ($params['page'] ?? 1);
$queryBuilder = $this->entityManager->getRepository(Support::class)
->createQueryBuilder('s')
->where('s.submitter = :user')
->andWhere('s.main = 0')
->setParameter('user', $user)
->orderBy('s.dateSubmit', 'DESC');
// فیلتر بر اساس وضعیت
if ($state && in_array($state, ['در حال پیگیری', 'پاسخ داده شده', 'خاتمه یافته'])) {
$queryBuilder->andWhere('s.state = :state')
->setParameter('state', $state);
}
// محاسبه تعداد کل
$totalQuery = clone $queryBuilder;
$total = (int) $totalQuery->select('COUNT(s.id)')->getQuery()->getSingleScalarResult();
// اعمال صفحه‌بندی
$queryBuilder->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
$tickets = $queryBuilder->getQuery()->getResult();
$ticketsArray = array_map(function ($ticket) {
return [
'id' => $ticket->getId(),
'code' => $ticket->getCode(),
'title' => $ticket->getTitle(),
'body' => mb_substr($ticket->getBody(), 0, 100) . '...',
'state' => $ticket->getState(),
'date' => $this->jdate->jdate('Y/m/d H:i', $ticket->getDateSubmit()),
'has_file' => !empty($ticket->getFileName())
];
}, $tickets);
return [
'success' => true,
'tickets' => $ticketsArray,
'total' => $total,
'page' => $page,
'limit' => $limit,
'message' => "تعداد {$total} تیکت یافت شد"
];
}
/**
* ابزار مشاهده جزئیات تیکت
*/
public function viewTicket(array $params, Business $business, $user): array
{
$ticketId = $params['ticket_id'] ?? null;
if (!$ticketId) {
return [
'success' => false,
'error' => 'شناسه تیکت الزامی است'
];
}
$ticket = $this->entityManager->getRepository(Support::class)->find($ticketId);
if (!$ticket) {
return [
'success' => false,
'error' => 'تیکت یافت نشد'
];
}
// بررسی دسترسی کاربر
if ($ticket->getSubmitter() !== $user) {
return [
'success' => false,
'error' => 'شما دسترسی به این تیکت را ندارید'
];
}
// دریافت پاسخ‌ها
$replies = $this->entityManager->getRepository(Support::class)
->findBy(['main' => $ticket->getId()], ['dateSubmit' => 'ASC']);
$repliesArray = array_map(function ($reply) {
return [
'id' => $reply->getId(),
'body' => $reply->getBody(),
'is_admin' => $reply->getSubmitter()->getRoles()[0] === 'ROLE_ADMIN',
'date' => $this->jdate->jdate('Y/m/d H:i', $reply->getDateSubmit()),
'has_file' => !empty($reply->getFileName())
];
}, $replies);
return [
'success' => true,
'ticket' => [
'id' => $ticket->getId(),
'code' => $ticket->getCode(),
'title' => $ticket->getTitle(),
'body' => $ticket->getBody(),
'state' => $ticket->getState(),
'date' => $this->jdate->jdate('Y/m/d H:i', $ticket->getDateSubmit()),
'has_file' => !empty($ticket->getFileName())
],
'replies' => $repliesArray,
'message' => "جزئیات تیکت {$ticket->getCode()}"
];
}
/**
* ابزار پاسخ به تیکت
*/
public function replyToTicket(array $params, Business $business, $user): array
{
$ticketId = $params['ticket_id'] ?? null;
$body = $params['body'] ?? '';
if (!$ticketId || empty($body)) {
return [
'success' => false,
'error' => 'شناسه تیکت و متن پاسخ الزامی است'
];
}
$mainTicket = $this->entityManager->getRepository(Support::class)->find($ticketId);
if (!$mainTicket) {
return [
'success' => false,
'error' => 'تیکت یافت نشد'
];
}
// بررسی دسترسی کاربر
if ($mainTicket->getSubmitter() !== $user) {
return [
'success' => false,
'error' => 'شما دسترسی به این تیکت را ندارید'
];
}
// ایجاد پاسخ جدید
$reply = new Support();
$reply->setMain($mainTicket->getId())
->setBody($body)
->setTitle($mainTicket->getTitle())
->setDateSubmit(time())
->setSubmitter($user)
->setState('در حال پیگیری');
$this->entityManager->persist($reply);
// به‌روزرسانی وضعیت تیکت اصلی
$mainTicket->setState('در حال پیگیری');
$this->entityManager->persist($mainTicket);
$this->entityManager->flush();
// ثبت لاگ
$this->log->insert(
'مدیریت تیکت',
"پاسخ به تیکت: {$mainTicket->getCode()}",
$user,
$business
);
return [
'success' => true,
'message' => 'پاسخ با موفقیت ارسال شد',
'reply' => [
'id' => $reply->getId(),
'body' => $reply->getBody(),
'date' => $this->jdate->jdate('Y/m/d H:i', $reply->getDateSubmit())
]
];
}
/**
* ابزار جستجوی تیکت‌ها
*/
public function searchTickets(array $params, Business $business, $user): array
{
$search = $params['search'] ?? '';
$state = $params['state'] ?? null;
$limit = (int) ($params['limit'] ?? 10);
if (empty($search)) {
return [
'success' => false,
'error' => 'متن جستجو الزامی است'
];
}
$queryBuilder = $this->entityManager->getRepository(Support::class)
->createQueryBuilder('s')
->where('s.submitter = :user')
->andWhere('s.main = 0')
->andWhere(
's.title LIKE :search OR s.body LIKE :search OR s.code LIKE :search'
)
->setParameter('user', $user)
->setParameter('search', '%' . $search . '%')
->orderBy('s.dateSubmit', 'DESC');
// فیلتر بر اساس وضعیت
if ($state && in_array($state, ['در حال پیگیری', 'پاسخ داده شده', 'خاتمه یافته'])) {
$queryBuilder->andWhere('s.state = :state')
->setParameter('state', $state);
}
$queryBuilder->setMaxResults($limit);
$tickets = $queryBuilder->getQuery()->getResult();
$ticketsArray = array_map(function ($ticket) {
return [
'id' => $ticket->getId(),
'code' => $ticket->getCode(),
'title' => $ticket->getTitle(),
'body' => mb_substr($ticket->getBody(), 0, 100) . '...',
'state' => $ticket->getState(),
'date' => $this->jdate->jdate('Y/m/d H:i', $ticket->getDateSubmit())
];
}, $tickets);
return [
'success' => true,
'tickets' => $ticketsArray,
'count' => count($tickets),
'message' => "تعداد " . count($tickets) . " تیکت برای \"{$search}\" یافت شد"
];
}
/**
* ابزار آمار تیکت‌ها
*/
public function getTicketStatistics(array $params, Business $business, $user): array
{
$queryBuilder = $this->entityManager->getRepository(Support::class)
->createQueryBuilder('s')
->where('s.submitter = :user')
->andWhere('s.main = 0')
->setParameter('user', $user);
// تعداد کل تیکت‌ها
$totalQuery = clone $queryBuilder;
$total = (int) $totalQuery->select('COUNT(s.id)')->getQuery()->getSingleScalarResult();
// تعداد تیکت‌های در حال پیگیری
$pendingQuery = clone $queryBuilder;
$pending = (int) $pendingQuery->andWhere('s.state = :state')
->setParameter('state', 'در حال پیگیری')
->select('COUNT(s.id)')
->getQuery()
->getSingleScalarResult();
// تعداد تیکت‌های پاسخ داده شده
$answeredQuery = clone $queryBuilder;
$answered = (int) $answeredQuery->andWhere('s.state = :state')
->setParameter('state', 'پاسخ داده شده')
->select('COUNT(s.id)')
->getQuery()
->getSingleScalarResult();
// تعداد تیکت‌های خاتمه یافته
$closedQuery = clone $queryBuilder;
$closed = (int) $closedQuery->andWhere('s.state = :state')
->setParameter('state', 'خاتمه یافته')
->select('COUNT(s.id)')
->getQuery()
->getSingleScalarResult();
return [
'success' => true,
'statistics' => [
'total' => $total,
'pending' => $pending,
'answered' => $answered,
'closed' => $closed
],
'message' => "آمار تیکت‌های شما: {$total} کل، {$pending} در حال پیگیری، {$answered} پاسخ داده شده، {$closed} خاتمه یافته"
];
}
/**
* تولید کد منحصر به فرد برای تیکت
*/
private function generateTicketCode(): string
{
$characters = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
$code = '';
for ($i = 0; $i < 8; $i++) {
$code .= $characters[rand(0, strlen($characters) - 1)];
}
// بررسی یکتا بودن کد
$existing = $this->entityManager->getRepository(Support::class)->findOneBy(['code' => $code]);
if ($existing) {
return $this->generateTicketCode(); // بازگشت بازگشتی
}
return $code;
}
/**
* پردازش درخواست مدیریت تیکت
*/
public function processRequest(string $message, Business $business, $user): array
{
// استخراج دستور از پیام
$command = $this->extractCommand($message);
if (!$command) {
return [
'success' => false,
'error' => 'دستور نامعتبر است. لطفاً واضح‌تر بیان کنید.',
'guide' => $this->getOperationsGuide()
];
}
// اجرای دستور
return $this->executeCommand($command, $business, $user);
}
/**
* استخراج دستور از پیام کاربر
*/
private function extractCommand(string $message): ?array
{
$message = mb_strtolower(trim($message), 'UTF-8');
// الگوهای دستورات
$patterns = [
'create' => [
'/(?:ایجاد|ساخت|ثبت)\s+(?:کن|کنید|بکن|بکنید)\s+(?:تیکت|درخواست|پشتیبانی)\s+(?:جدید\s+)?(?:با\s+عنوان\s+)?([^\n]+)/u',
'/(?:تیکت|درخواست|پشتیبانی)\s+(?:جدید\s+)?(?:با\s+عنوان\s+)?([^\n]+)\s+(?:ایجاد|ساخت|ثبت)\s+(?:کن|کنید|بکن|بکنید)/u'
],
'list' => [
'/(?:لیست|مشاهده|نشون\s+بده)\s+(?:تیکت|درخواست|پشتیبانی)\s*(?:ها|های\s+من)?/u',
'/(?:تیکت|درخواست|پشتیبانی)\s*(?:ها|های\s+من)?\s+(?:رو\s+)?(?:لیست|مشاهده|نشون\s+بده)/u'
],
'view' => [
'/(?:مشاهده|ببین|جزئیات)\s+(?:تیکت|درخواست)\s+(?:با\s+کد\s+)?([A-Z0-9]+)/u',
'/(?:تیکت|درخواست)\s+([A-Z0-9]+)\s+(?:رو\s+)?(?:مشاهده|ببین)/u'
],
'search' => [
'/(?:جستجو|پیدا\s+کن)\s+(?:تیکت|درخواست)\s+(?:برای\s+)?([^\n]+)/u',
'/(?:تیکت|درخواست)\s+(?:برای\s+)?([^\n]+)\s+(?:رو\s+)?(?:جستجو|پیدا\s+کن)/u'
],
'statistics' => [
'/(?:آمار|وضعیت)\s+(?:تیکت|درخواست|پشتیبانی)\s*(?:ها|های\s+من)?/u',
'/(?:تیکت|درخواست|پشتیبانی)\s*(?:ها|های\s+من)?\s+(?:رو\s+)?(?:آمار|وضعیت)/u'
]
];
foreach ($patterns as $commandType => $commandPatterns) {
foreach ($commandPatterns as $pattern) {
if (preg_match($pattern, $message, $matches)) {
return [
'type' => $commandType,
'params' => $matches[1] ?? null
];
}
}
}
return null;
}
/**
* اجرای دستور
*/
private function executeCommand(array $command, Business $business, $user): array
{
switch ($command['type']) {
case 'create':
// استخراج عنوان و متن از پیام
$content = $command['params'];
$lines = explode("\n", $content);
$title = trim($lines[0]);
$body = implode("\n", array_slice($lines, 1));
return $this->createTicket([
'title' => $title,
'body' => $body
], $business, $user);
case 'list':
return $this->listUserTickets([], $business, $user);
case 'view':
return $this->viewTicket([
'ticket_id' => $command['params']
], $business, $user);
case 'search':
return $this->searchTickets([
'search' => $command['params']
], $business, $user);
case 'statistics':
return $this->getTicketStatistics([], $business, $user);
default:
return [
'success' => false,
'error' => 'دستور نامعتبر است'
];
}
}
/**
* راهنمای عملیات
*/
public function getOperationsGuide(): string
{
return "🔧 راهنمای مدیریت تیکت‌ها:\n\n" .
"📝 ایجاد تیکت: 'تیکت جدید با عنوان [عنوان] [متن]'\n" .
"📋 مشاهده لیست: 'لیست تیکت‌های من'\n" .
"👁️ مشاهده جزئیات: 'مشاهده تیکت [کد]'\n" .
"🔍 جستجو: 'جستجو تیکت [متن]'\n" .
"📊 آمار: 'آمار تیکت‌های من'\n\n" .
"💡 مثال: 'تیکت جدید با عنوان مشکل در ورود به سیستم\n" .
"من نمی‌تونم وارد حسابم بشم و خطای 404 می‌گیرم'";
}
}

View file

@ -0,0 +1,449 @@
<template>
<v-card class="ai-chart-widget" elevation="0" variant="outlined">
<v-card-title class="d-flex align-center justify-space-between pa-4">
<span class="text-h6">{{ chartTitle }}</span>
<div class="d-flex align-center gap-2">
<v-btn
icon="mdi-download"
variant="text"
size="small"
@click="downloadChart"
:title="$t('chart.download')"
></v-btn>
<v-btn
icon="mdi-refresh"
variant="text"
size="small"
@click="refreshChart"
:title="$t('chart.refresh')"
></v-btn>
<v-btn
icon="mdi-fullscreen"
variant="text"
size="small"
@click="toggleFullscreen"
:title="$t('chart.fullscreen')"
></v-btn>
</div>
</v-card-title>
<v-card-text class="pa-4">
<div class="chart-container" :class="{ 'fullscreen': isFullscreen }">
<apexchart
ref="chart"
:type="chartType"
:height="chartHeight"
:options="chartOptions"
:series="chartSeries"
></apexchart>
</div>
<!-- اطلاعات نمودار -->
<div class="chart-info mt-4">
<v-expansion-panels variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>mdi-information</v-icon>
{{ $t('chart.details') }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="chart-details">
<div class="detail-item">
<strong>{{ $t('chart.type') }}:</strong> {{ getChartTypeName(chartType) }}
</div>
<div class="detail-item">
<strong>{{ $t('chart.id') }}:</strong> {{ chartId }}
</div>
<div class="detail-item">
<strong>{{ $t('chart.dataPoints') }}:</strong> {{ dataPointsCount }}
</div>
<div class="detail-item">
<strong>{{ $t('chart.created') }}:</strong> {{ formatDate(createdAt) }}
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</div>
</v-card-text>
</v-card>
</template>
<script>
import VueApexCharts from 'vue3-apexcharts';
export default {
name: 'AIChart',
components: {
apexchart: VueApexCharts,
},
props: {
chartData: {
type: Object,
required: true
},
height: {
type: [String, Number],
default: 400
}
},
data() {
return {
isFullscreen: false,
chartId: '',
createdAt: new Date(),
chartType: 'bar',
chartTitle: 'نمودار',
chartOptions: {},
chartSeries: []
};
},
computed: {
chartHeight() {
if (this.isFullscreen) {
return window.innerHeight - 200;
}
return this.height;
},
dataPointsCount() {
if (this.chartData && this.chartData.data) {
const data = this.chartData.data;
if (data.categories) {
return data.categories.length;
}
if (data.labels) {
return data.labels.length;
}
if (data.series && data.series[0] && data.series[0].data) {
return data.series[0].data.length;
}
}
return 0;
}
},
watch: {
chartData: {
handler(newData) {
this.initializeChart(newData);
},
immediate: true,
deep: true
}
},
mounted() {
this.initializeChart(this.chartData);
},
methods: {
initializeChart(data) {
if (!data) return;
this.chartType = data.type || 'bar';
this.chartTitle = data.title || 'نمودار';
this.chartId = data.chart_id || this.generateChartId();
this.createdAt = new Date();
// تنظیمات پایه نمودار
this.chartOptions = {
chart: {
id: this.chartId,
type: this.chartType,
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
toolbar: {
show: true,
tools: {
download: true,
selection: true,
zoom: true,
zoomin: true,
zoomout: true,
pan: true,
reset: true
}
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
animateGradually: {
enabled: true,
delay: 150
},
dynamicAnimation: {
enabled: true,
speed: 350
}
}
},
title: {
text: this.chartTitle,
align: 'center',
style: {
fontSize: '16px',
fontWeight: 'bold',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
}
},
colors: ['#2196F3', '#4CAF50', '#FFC107', '#F44336', '#9C27B0', '#00BCD4', '#FF9800', '#795548', '#607D8B', '#E91E63'],
legend: {
position: 'bottom',
fontSize: '14px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
markers: {
width: 12,
height: 12,
radius: 6
}
},
tooltip: {
theme: 'light',
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
},
y: {
formatter: function(value) {
return this.$filters ? this.$filters.formatNumber(value) : value;
}
}
},
responsive: [
{
breakpoint: 480,
options: {
chart: { width: '100%' },
legend: { position: 'bottom' }
}
}
]
};
// تنظیمات خاص بر اساس نوع نمودار
this.setupChartSpecificOptions(data.data);
// تنظیم سریهای داده
this.setupChartSeries(data.data);
},
setupChartSpecificOptions(data) {
switch (this.chartType) {
case 'bar':
case 'line':
case 'area':
if (data.categories) {
this.chartOptions.xaxis = {
categories: data.categories,
labels: {
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
}
}
};
}
this.chartOptions.yaxis = {
labels: {
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
},
formatter: function(value) {
return this.$filters ? this.$filters.formatNumber(value) : value;
}
}
};
break;
case 'pie':
case 'doughnut':
if (data.labels) {
this.chartOptions.labels = data.labels;
}
this.chartOptions.plotOptions = {
pie: {
donut: {
labels: {
show: true,
name: {
show: true,
fontSize: '14px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
},
value: {
show: true,
fontSize: '16px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
formatter: function(value) {
return this.$filters ? this.$filters.formatNumber(value) : value;
}
}
}
}
}
};
break;
case 'radar':
if (data.categories) {
this.chartOptions.xaxis = {
categories: data.categories
};
}
break;
case 'scatter':
case 'bubble':
this.chartOptions.xaxis = {
type: 'numeric',
labels: {
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
}
}
};
this.chartOptions.yaxis = {
type: 'numeric',
labels: {
style: {
fontSize: '12px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
}
}
};
break;
}
},
setupChartSeries(data) {
if (this.chartType === 'pie' || this.chartType === 'doughnut') {
this.chartSeries = data.series || [];
} else {
this.chartSeries = data.series || [
{
name: 'داده‌ها',
data: []
}
];
}
},
getChartTypeName(type) {
const typeNames = {
'bar': 'ستونی',
'line': 'خطی',
'pie': 'دایره‌ای',
'doughnut': 'دونات',
'area': 'ناحیه‌ای',
'radar': 'راداری',
'scatter': 'پراکندگی',
'bubble': 'حبابی'
};
return typeNames[type] || type;
},
generateChartId() {
return 'ai_chart_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
},
formatDate(date) {
return new Intl.DateTimeFormat('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
},
downloadChart() {
if (this.$refs.chart) {
this.$refs.chart.chart.downloadCSV();
}
},
refreshChart() {
if (this.$refs.chart) {
this.$refs.chart.chart.refresh();
}
},
toggleFullscreen() {
this.isFullscreen = !this.isFullscreen;
this.$nextTick(() => {
if (this.$refs.chart) {
this.$refs.chart.chart.resize();
}
});
}
}
};
</script>
<style scoped>
.ai-chart-widget {
border-radius: 12px;
overflow: hidden;
}
.chart-container {
position: relative;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.chart-container.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
background: white;
padding: 20px;
}
.chart-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.detail-item {
padding: 8px 12px;
background: #f5f5f5;
border-radius: 6px;
font-size: 14px;
}
.detail-item strong {
color: #1976d2;
}
.gap-2 {
gap: 8px;
}
/* Responsive Design */
@media (max-width: 768px) {
.chart-details {
grid-template-columns: 1fr;
}
.chart-container.fullscreen {
padding: 10px;
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.detail-item {
background: #424242;
color: #ffffff;
}
.detail-item strong {
color: #64b5f6;
}
}
</style>