forked from morrning/hesabixCore
progress in ai with chart and ticket system
This commit is contained in:
parent
26121aba26
commit
39a2846ff6
|
@ -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
538
hesabixCore/src/Service/AI/ChartService.php
Normal file
538
hesabixCore/src/Service/AI/ChartService.php
Normal 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}]'";
|
||||
}
|
||||
}
|
|
@ -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
|
|||
- فقط اشخاص بدون تراکنش قابل حذف هستند
|
||||
- تمام عملیات در لاگ ثبت میشود";
|
||||
}
|
||||
|
||||
|
||||
}
|
519
hesabixCore/src/Service/AI/TicketManagementService.php
Normal file
519
hesabixCore/src/Service/AI/TicketManagementService.php
Normal 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 میگیرم'";
|
||||
}
|
||||
}
|
449
webUI/src/components/widgets/AIChart.vue
Normal file
449
webUI/src/components/widgets/AIChart.vue
Normal 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>
|
Loading…
Reference in a new issue