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']) {
|
if ($result['success']) {
|
||||||
// بررسی وجود کلید response
|
// بررسی وجود کلید response
|
||||||
|
@ -619,6 +740,44 @@ class wizardController extends AbstractController
|
||||||
case 'search_persons':
|
case 'search_persons':
|
||||||
$result = $this->aiService->getPersonManagementService()->searchPersons($commandParams, $business);
|
$result = $this->aiService->getPersonManagementService()->searchPersons($commandParams, $business);
|
||||||
break;
|
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:
|
default:
|
||||||
return $this->json([
|
return $this->json([
|
||||||
'success' => false,
|
'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);
|
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