From 39a2846ff6fcacf6e4817136400c5bc902e08823 Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Sat, 19 Jul 2025 16:28:55 +0000 Subject: [PATCH] progress in ai with chart and ticket system --- .../src/Controller/wizardController.php | 176 +++++- hesabixCore/src/Service/AI/AIService.php | 390 +++++++++++- hesabixCore/src/Service/AI/ChartService.php | 538 ++++++++++++++++ .../Service/AI/PersonManagementService.php | 592 ++++++++++++++++++ .../Service/AI/TicketManagementService.php | 519 +++++++++++++++ webUI/src/components/widgets/AIChart.vue | 449 +++++++++++++ 6 files changed, 2653 insertions(+), 11 deletions(-) create mode 100644 hesabixCore/src/Service/AI/ChartService.php create mode 100644 hesabixCore/src/Service/AI/TicketManagementService.php create mode 100644 webUI/src/components/widgets/AIChart.vue diff --git a/hesabixCore/src/Controller/wizardController.php b/hesabixCore/src/Controller/wizardController.php index c0f8b3cc..1f9c036c 100644 --- a/hesabixCore/src/Controller/wizardController.php +++ b/hesabixCore/src/Controller/wizardController.php @@ -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 => '📋' + }; + } } diff --git a/hesabixCore/src/Service/AI/AIService.php b/hesabixCore/src/Service/AI/AIService.php index c43646be..edcfb359 100644 --- a/hesabixCore/src/Service/AI/AIService.php +++ b/hesabixCore/src/Service/AI/AIService.php @@ -19,6 +19,8 @@ class AIService private Provider $provider; private ?PersonDataService $personDataService = null; private ?PersonManagementService $personManagementService = null; + private ?TicketManagementService $ticketManagementService = null; + private ?ChartService $chartService = null; public function __construct(registryMGR $registryMGR, EntityManagerInterface $entityManager, Log $log, Provider $provider) { @@ -114,9 +116,15 @@ class AIService private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string { $basePrompt = $this->getSystemPrompt(); + $customPrompt = $this->getAIPrompt(); $prompt = $basePrompt; + // اضافه کردن پرامپ سفارشی مدیر سیستم + if (!empty($customPrompt) && $customPrompt !== 'شما یک دستیار هوشمند برای سیستم حسابداری هستید.') { + $prompt .= "\n\n" . $customPrompt; + } + if ($business) { $prompt .= "\n\nاطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}."; } @@ -151,7 +159,7 @@ class AIService */ private function getSystemPrompt(): string { - return "شما یک دستیار هوشمند برای سیستم حسابداری حسابیکس هستید.\n\n🔧 ابزارهای موجود:\n\n1. **مدیریت اشخاص**:\n - افزودن شخص جدید: add_person{name:نام مستعار, full_name:نام کامل}\n - حذف شخص: delete_person{name:نام مستعار}\n - ویرایش شخص: edit_person{name:نام مستعار, mobile:موبایل, address:آدرس, email:ایمیل}\n - نمایش مشخصات: show_person{name:نام مستعار}\n - جستجوی اشخاص: search_persons{search:متن جستجو, limit:تعداد نتایج}\n\n📋 قوانین مهم:\n- حتماً از دستورات بالا استفاده کنید\n- نام شخص = نام مستعار (nikename) - فیلد الزامی\n- نام کامل (full_name) = نام و نام خانوادگی - فیلد اختیاری\n\n🔄 سیستم تعاملی چندمرحله‌ای:\nبرای عملیات پیچیده که نیاز به چند مرحله دارند، از ساختار JSON استفاده کنید:\n\n```json\n{ + return "شما یک دستیار هوشمند برای سیستم حسابداری حسابیکس هستید.\n\n🔧 ابزارهای موجود:\n\n1. **مدیریت اشخاص**:\n - افزودن شخص جدید: add_person{name:نام مستعار, full_name:نام کامل}\n - حذف شخص: delete_person{name:نام مستعار}\n - ویرایش شخص: edit_person{name:نام مستعار, mobile:موبایل, address:آدرس, email:ایمیل}\n - نمایش مشخصات: show_person{name:نام مستعار}\n - جستجوی اشخاص: search_persons{search:متن جستجو, limit:تعداد نتایج}\n\n2. **جستجوی پیشرفته اشخاص**:\n - جستجوی پیشرفته: advanced_search_persons{search:متن جستجو, search_type:نوع جستجو, limit:تعداد نتایج}\n * search_type: all, mobile, name, code, debtor, creditor, balanced\n - جستجو بر اساس موبایل: search_by_mobile{mobile:شماره موبایل}\n - جستجوی بدهکاران: search_debtors{search:متن جستجو, limit:تعداد نتایج}\n - جستجوی بستانکاران: search_creditors{search:متن جستجو, limit:تعداد نتایج}\n\n3. **مدیریت تیکت‌های پشتیبانی**:\n - ایجاد تیکت جدید: create_ticket{title:عنوان تیکت, body:متن تیکت, priority:اولویت}\n - مشاهده لیست تیکت‌ها: list_tickets{state:وضعیت, limit:تعداد نتایج}\n - مشاهده جزئیات تیکت: view_ticket{ticket_id:شناسه تیکت}\n - پاسخ به تیکت: reply_ticket{ticket_id:شناسه تیکت, body:متن پاسخ}\n - جستجوی تیکت‌ها: search_tickets{search:متن جستجو, state:وضعیت}\n - آمار تیکت‌ها: get_ticket_statistics{}\n\n4. **مدیریت نمودارها و چارت‌ها**:\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📋 قوانین مهم:\n- حتماً از دستورات بالا استفاده کنید\n- نام شخص = نام مستعار (nikename) - فیلد الزامی\n- نام کامل (full_name) = نام و نام خانوادگی - فیلد اختیاری\n- برای جستجو بر اساس موبایل، از search_by_mobile استفاده کنید\n- برای جستجوی بدهکاران/بستانکاران، از ابزارهای مخصوص استفاده کنید\n- برای سوالات پشتیبانی، ابتدا راهنمایی ارائه دهید، سپس پیشنهاد تیکت بدهید\n\n🔍 راهنمای جستجوی هوشمند:\n\n**جستجو بر اساس موبایل:**\n- وقتی کاربر شماره موبایل می‌دهد: \"کی موبایلش 09183282405 هست؟\"\n- از ابزار search_by_mobile استفاده کنید\n\n**جستجوی بدهکاران:**\n- وقتی کاربر می‌گوید: \"بدهکاران رو بهم معرفی کن\"\n- از ابزار search_debtors استفاده کنید\n\n**جستجوی بستانکاران:**\n- وقتی کاربر می‌گوید: \"بستانکاران رو نشون بده\"\n- از ابزار search_creditors استفاده کنید\n\n**جستجوی پیشرفته:**\n- برای جستجوهای خاص: advanced_search_persons{search_type:debtor, search:علی}\n- برای همه بدهکاران: advanced_search_persons{search_type:debtor}\n- برای همه بستانکاران: advanced_search_persons{search_type:creditor}\n\n**مدیریت تیکت‌های پشتیبانی:**\n- وقتی کاربر مشکل یا سوالی دارد: ابتدا راهنمایی ارائه دهید، سپس پیشنهاد تیکت بدهید\n- برای ایجاد تیکت: create_ticket{title:عنوان, body:متن مشکل}\n- برای مشاهده تیکت‌ها: list_tickets{state:در حال پیگیری}\n- برای مشاهده جزئیات: view_ticket{ticket_id:شناسه}\n- برای پاسخ: reply_ticket{ticket_id:شناسه, body:متن پاسخ}\n- برای جستجو: search_tickets{search:متن جستجو}\n- برای آمار: get_ticket_statistics{}\n\n**راهنمای پشتیبانی هوشمند:**\n1. **سوالات عمومی**: ابتدا پاسخ دهید، سپس پیشنهاد تیکت بدهید\n2. **مشکلات فنی**: راهنمایی ارائه دهید، سپس تیکت ایجاد کنید\n3. **درخواست‌های پیچیده**: ابتدا توضیح دهید، سپس تیکت پیشنهاد دهید\n4. **مشاهده وضعیت**: از ابزارهای تیکت استفاده کنید\n\n**مثال‌های تیکت:**\n- 'مشکل در ورود به سیستم' → راهنمایی + پیشنهاد تیکت\n- 'لیست تیکت‌های من' → list_tickets\n- 'مشاهده تیکت ABC123' → view_ticket{ticket_id:ABC123}\n- 'پاسخ به تیکت 123' → reply_ticket{ticket_id:123, body:متن پاسخ}\n\n**مدیریت نمودارها و چارت‌ها:**\n- وقتی کاربر می‌خواهد نمودار ببیند: ابتدا داده‌ها را جمع‌آوری کنید، سپس نمودار مناسب ایجاد کنید\n- برای نمودار ستونی: create_bar_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}\n- برای نمودار دایره‌ای: create_pie_chart{title:عنوان, data:[{label:عنوان, value:عدد}]}\n- برای نمودار خطی: create_line_chart{title:عنوان, data:[{category:عنوان, 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**راهنمای نمودار هوشمند:**\n1. **تحلیل داده‌ها**: ابتدا داده‌ها را جمع‌آوری و تحلیل کنید\n2. **انتخاب نوع نمودار**: بر اساس نوع داده، نمودار مناسب انتخاب کنید\n3. **ایجاد نمودار**: از ابزارهای نمودار استفاده کنید\n4. **نمایش تعاملی**: نمودار را با قابلیت‌های تعاملی نمایش دهید\n\n**مثال‌های نمودار:**\n- 'نمودار فروش ماهانه' → جمع‌آوری داده‌ها + create_bar_chart\n- 'نمودار دایره‌ای سهم بازار' → جمع‌آوری داده‌ها + create_pie_chart\n- 'نمودار خطی روند رشد' → جمع‌آوری داده‌ها + create_line_chart\n- 'نمودار پراکندگی سن و درآمد' → جمع‌آوری داده‌ها + create_scatter_chart\n\n🎯 تحلیل و نتیجه‌گیری هوشمند:\n\n**برای سوالات تحلیلی، حتماً نتیجه‌گیری کنید:**\n\n1. **بیشترین بدهکار:**\n - وقتی کاربر می‌گوید: \"بیشترین فرد بدهکار رو بهم مشخصاتشو بده\"\n - ابتدا لیست بدهکاران را بگیرید\n - سپس تحلیل کنید و شخص با بیشترین بدهی را مشخص کنید\n - در نهایت مشخصات کامل آن شخص را نمایش دهید\n\n2. **بیشترین بستانکار:**\n - وقتی کاربر می‌گوید: \"بیشترین بستانکار کیه؟\"\n - ابتدا لیست بستانکاران را بگیرید\n - سپس تحلیل کنید و شخص با بیشترین بستانکاری را مشخص کنید\n\n3. **تحلیل آماری:**\n - وقتی کاربر می‌خواهد تحلیل کند: \"بدهکاران رو تحلیل کن\"\n - لیست را بگیرید و آمار ارائه دهید\n - مثلاً: تعداد کل، میانگین بدهی، بیشترین و کمترین بدهی\n\n4. **مقایسه و رتبه‌بندی:**\n - وقتی کاربر می‌خواهد مقایسه کند: \"بدهکاران رو بر اساس میزان بدهی مرتب کن\"\n - لیست را بگیرید و رتبه‌بندی ارائه دهید\n\n**نکات مهم تحلیل:**\n- همیشه نتایج جستجو را تحلیل کنید\n- اگر کاربر می‌خواهد \"بیشترین\" یا \"کمترین\"، حتماً مشخص کنید\n- اگر کاربر می‌خواهد \"مشخصات\"، از ابزار show_person استفاده کنید\n- برای سوالات تحلیلی، requires_followup را true کنید تا بتوانید تحلیل ارائه دهید\n- در user_message، ابتدا نتایج را نشان دهید، سپس تحلیل کنید\n\n🔄 سیستم تعاملی چندمرحله‌ای:\nبرای عملیات پیچیده که نیاز به چند مرحله دارند، از ساختار JSON استفاده کنید:\n\n```json\n{ \"action\": \"tool_command\", \"tool\": \"نام_ابزار\", \"params\": { @@ -265,7 +273,7 @@ class AIService \"new_mobile\": \"09123456789\" } } -```\n\n💡 مثال‌های استفاده:\n\n**مثال 1 - عملیات ساده:**\n- 'علی رو حذف کن' → دستور مستقیم delete_person\n\n**مثال 2 - عملیات با جستجو:**\n- 'تلفن 09123456789 را برای محسن اضافه کن' → ابتدا جستجو، سپس ویرایش\n\n**مثال 3 - عملیات هوشمند:**\n- 'شخصی با نام محسن پیدا کن و موبایلش رو 09123456789 کن' → جستجو + اگر پیدا شد ویرایش + اگر پیدا نشد بساز\n\n**مثال 4 - عملیات پیچیده:**\n- 'برای احمد موبایل 09123456789 تنظیم کن' → جستجو + اگر پیدا نشد بساز + ویرایش موبایل\n\n**مثال 5 - نمایش نتایج جستجو:**\n- '5 نفر اخیر را نشان بده' → جستجو + نمایش مستقیم نتایج بدون followup\n\n⚠️ نکات مهم:\n- حتماً از ساختار JSON استفاده کنید\n- برای عملیات پیچیده، ابتدا جستجو کنید\n- اگر شخصی پیدا نشد، شخص جدید بسازید\n- پیام‌های کاربر را واضح و مفید بنویسید\n- اطلاعات دیباگ را کامل ارائه دهید\n- برای عملیات‌هایی که فقط نیاز به نمایش نتایج دارند، requires_followup را false کنید\n- برای عملیات پیوسته، operation_complete را false نگه دارید تا به نتیجه برسید\n- وقتی عملیات کامل شد، operation_complete را true کنید و نتیجه نهایی را ارائه دهید\n- برای عملیات هوشمند، از on_success و on_failure استفاده کنید تا مرحله بعدی مشخص شود\n- برای جستجو و نمایش نتایج، مستقیماً نتایج را در user_message قرار دهید و requires_followup را false کنید\n\n🔄 منطق عملیات هوشمند:\n1. ابتدا جستجو کنید\n2. اگر پیدا شد، عملیات مورد نظر را انجام دهید\n3. اگر پیدا نشد، شخص جدید بسازید\n4. سپس عملیات مورد نظر را انجام دهید\n5. در نهایت operation_complete را true کنید\n\n🔄 منطق عملیات پیوسته:\n1. ابتدا جستجو کنید\n2. اگر پیدا شد، عملیات مورد نظر را انجام دهید\n3. اگر پیدا نشد، شخص جدید بسازید\n4. سپس عملیات مورد نظر را انجام دهید\n5. در نهایت operation_complete را true کنید\n\n🔄 منطق نمایش نتایج:\n1. جستجو را انجام دهید\n2. نتایج را مستقیماً در user_message قرار دهید\n3. requires_followup را false کنید\n4. اطلاعات کامل را در debug_info قرار دهید\n\n🔄 منطق پاسخ‌دهی عمومی:\n1. سوال را بررسی کنید\n2. اگر سوال عمومی است، پاسخ آموزشی ارائه دهید\n3. از direct_response استفاده کنید\n4. اطلاعات مفید و کامل ارائه دهید\n\nلطفاً درخواست کاربر را بررسی کرده و پاسخ مناسب را با ساختار JSON ارائه دهید."; +```\n\n💡 مثال‌های استفاده:\n\n**مثال 1 - عملیات ساده:**\n- 'علی رو حذف کن' → دستور مستقیم delete_person\n\n**مثال 2 - عملیات با جستجو:**\n- 'تلفن 09123456789 را برای محسن اضافه کن' → ابتدا جستجو، سپس ویرایش\n\n**مثال 3 - عملیات هوشمند:**\n- 'شخصی با نام محسن پیدا کن و موبایلش رو 09123456789 کن' → جستجو + اگر پیدا شد ویرایش + اگر پیدا نشد بساز\n\n**مثال 4 - عملیات پیچیده:**\n- 'برای احمد موبایل 09123456789 تنظیم کن' → جستجو + اگر پیدا نشد بساز + ویرایش موبایل\n\n**مثال 5 - نمایش نتایج جستجو:**\n- '5 نفر اخیر را نشان بده' → جستجو + نمایش مستقیم نتایج بدون followup\n\n**مثال 6 - جستجو بر اساس موبایل:**\n- 'کی موبایلش 09183282405 هست؟' → search_by_mobile{mobile:09183282405}\n\n**مثال 7 - جستجوی بدهکاران:**\n- 'بدهکاران رو بهم معرفی کن' → search_debtors{limit:20}\n\n**مثال 8 - جستجوی بستانکاران:**\n- 'بستانکاران رو نشون بده' → search_creditors{limit:20}\n\n**مثال 9 - جستجوی پیشرفته:**\n- 'بدهکاران علی رو پیدا کن' → advanced_search_persons{search_type:debtor, search:علی}\n\n**مثال 10 - تحلیل و نتیجه‌گیری:**\n- 'بیشترین فرد بدهکار رو بهم مشخصاتشو بده' → search_debtors + تحلیل + show_person\n- 'بیشترین بستانکار کیه؟' → search_creditors + تحلیل + نمایش نتیجه\n- 'بدهکاران رو تحلیل کن' → search_debtors + تحلیل آماری\n\n⚠️ نکات مهم:\n- حتماً از ساختار JSON استفاده کنید\n- برای عملیات پیچیده، ابتدا جستجو کنید\n- اگر شخصی پیدا نشد، شخص جدید بسازید\n- پیام‌های کاربر را واضح و مفید بنویسید\n- اطلاعات دیباگ را کامل ارائه دهید\n- برای عملیات‌هایی که فقط نیاز به نمایش نتایج دارند، requires_followup را false کنید\n- برای عملیات پیوسته، operation_complete را false نگه دارید تا به نتیجه برسید\n- وقتی عملیات کامل شد، operation_complete را true کنید و نتیجه نهایی را ارائه دهید\n- برای عملیات هوشمند، از on_success و on_failure استفاده کنید تا مرحله بعدی مشخص شود\n- برای جستجو و نمایش نتایج، مستقیماً نتایج را در user_message قرار دهید و requires_followup را false کنید\n- برای جستجو بر اساس موبایل، حتماً از ابزار search_by_mobile استفاده کنید\n- برای جستجوی بدهکاران/بستانکاران، از ابزارهای مخصوص استفاده کنید\n- **برای سوالات تحلیلی، حتماً تحلیل کنید و نتیجه‌گیری کنید**\n- **برای سوالات \"بیشترین\" یا \"کمترین\"، حتماً مشخص کنید**\n- **برای سوالات \"مشخصات\"، از ابزار show_person استفاده کنید**\n\n🔄 منطق عملیات هوشمند:\n1. ابتدا جستجو کنید\n2. اگر پیدا شد، عملیات مورد نظر را انجام دهید\n3. اگر پیدا نشد، شخص جدید بسازید\n4. سپس عملیات مورد نظر را انجام دهید\n5. در نهایت operation_complete را true کنید\n\n🔄 منطق عملیات پیوسته:\n1. ابتدا جستجو کنید\n2. اگر پیدا شد، عملیات مورد نظر را انجام دهید\n3. اگر پیدا نشد، شخص جدید بسازید\n4. سپس عملیات مورد نظر را انجام دهید\n5. در نهایت operation_complete را true کنید\n\n🔄 منطق نمایش نتایج:\n1. جستجو را انجام دهید\n2. نتایج را مستقیماً در user_message قرار دهید\n3. requires_followup را false کنید\n4. اطلاعات کامل را در debug_info قرار دهید\n\n🔄 منطق پاسخ‌دهی عمومی:\n1. سوال را بررسی کنید\n2. اگر سوال عمومی است، پاسخ آموزشی ارائه دهید\n3. از direct_response استفاده کنید\n4. اطلاعات مفید و کامل ارائه دهید\n\n🔄 منطق جستجوی هوشمند:\n1. اگر کاربر شماره موبایل می‌دهد، از search_by_mobile استفاده کنید\n2. اگر کاربر می‌خواهد بدهکاران را ببیند، از search_debtors استفاده کنید\n3. اگر کاربر می‌خواهد بستانکاران را ببیند، از search_creditors استفاده کنید\n4. برای جستجوهای پیشرفته، از advanced_search_persons استفاده کنید\n5. برای جستجوهای ساده، از search_persons استفاده کنید\n\n🔄 منطق تحلیل و نتیجه‌گیری:\n1. اگر کاربر می‌خواهد \"بیشترین\" یا \"کمترین\"، ابتدا لیست را بگیرید\n2. سپس تحلیل کنید و شخص مورد نظر را مشخص کنید\n3. اگر کاربر می‌خواهد \"مشخصات\"، از show_person استفاده کنید\n4. در نهایت نتیجه نهایی را ارائه دهید\n5. برای سوالات تحلیلی، requires_followup را true کنید\n\nلطفاً درخواست کاربر را بررسی کرده و پاسخ مناسب را با ساختار JSON ارائه دهید."; } /** @@ -394,16 +402,53 @@ class AIService $finalResponse = $jsonResponse['user_message'] ?? 'عملیات انجام شد'; // برای جستجو، نتایج را مستقیماً نمایش بده - if ($tool === 'search_persons' && $result['success'] && isset($result['persons'])) { + if (in_array($tool, ['search_persons', 'advanced_search_persons', 'search_debtors', 'search_creditors']) && $result['success'] && isset($result['persons'])) { $persons = $result['persons']; $count = count($persons); if ($count > 0) { $finalResponse .= "\n\n"; - if (isset($params['search']) && !empty($params['search'])) { - $finalResponse .= "نتایج جستجو برای \"{$params['search']}\":\n"; + + // تعیین نوع جستجو + if ($tool === 'search_debtors') { + $finalResponse .= "🔴 لیست بدهکاران"; + if (isset($params['search']) && !empty($params['search'])) { + $finalResponse .= " برای \"{$params['search']}\""; + } + $finalResponse .= ":\n"; + if (isset($result['total_debt'])) { + $finalResponse .= "💰 مجموع بدهی: " . number_format($result['total_debt']) . " ریال\n\n"; + } + } elseif ($tool === 'search_creditors') { + $finalResponse .= "🟢 لیست بستانکاران"; + if (isset($params['search']) && !empty($params['search'])) { + $finalResponse .= " برای \"{$params['search']}\""; + } + $finalResponse .= ":\n"; + if (isset($result['total_credit'])) { + $finalResponse .= "💰 مجموع بستانکاری: " . number_format($result['total_credit']) . " ریال\n\n"; + } + } elseif ($tool === 'advanced_search_persons') { + $searchType = $params['search_type'] ?? 'all'; + $searchTypeNames = [ + 'debtor' => 'بدهکاران', + 'creditor' => 'بستانکاران', + 'balanced' => 'تسویه شده‌ها', + 'mobile' => 'بر اساس موبایل', + 'name' => 'بر اساس نام', + 'code' => 'بر اساس کد' + ]; + $finalResponse .= "🔍 نتایج جستجوی " . ($searchTypeNames[$searchType] ?? 'پیشرفته'); + if (isset($params['search']) && !empty($params['search'])) { + $finalResponse .= " برای \"{$params['search']}\""; + } + $finalResponse .= ":\n"; } else { - $finalResponse .= "لیست {$count} نفر اخیر:\n"; + if (isset($params['search']) && !empty($params['search'])) { + $finalResponse .= "🔍 نتایج جستجو برای \"{$params['search']}\":\n"; + } else { + $finalResponse .= "📋 لیست {$count} نفر اخیر:\n"; + } } foreach ($persons as $index => $person) { @@ -420,13 +465,152 @@ class AIService $finalResponse .= " - موبایل: " . $person['mobile']; } + // نمایش تراز برای جستجوهای مالی + if (isset($person['balance_formatted'])) { + $status = $person['status'] ?? ''; + $finalResponse .= " - تراز: " . $person['balance_formatted'] . " ({$status})"; + } + $finalResponse .= " (کد: " . $person['code'] . ")"; } } else { - $finalResponse .= "\n\nهیچ شخصی یافت نشد."; + $finalResponse .= "\n\n❌ هیچ شخصی یافت نشد."; } - // برای جستجو، followup را غیرفعال کن + // برای جستجو، followup را غیرفعال کن مگر اینکه سوال تحلیلی باشد + $requiresFollowup = false; + + // بررسی اینکه آیا سوال تحلیلی است + $userMessage = $jsonResponse['user_message'] ?? ''; + $isAnalyticalQuestion = false; + + // کلمات کلیدی برای سوالات تحلیلی + $analyticalKeywords = [ + 'بیشترین', 'کمترین', 'بزرگترین', 'کوچکترین', + 'مشخصات', 'تحلیل', 'رتبه', 'مرتب', 'مقایسه', + 'اول', 'آخر', 'برتر', 'بدترین', 'بهترین' + ]; + + foreach ($analyticalKeywords as $keyword) { + if (strpos($userMessage, $keyword) !== false) { + $isAnalyticalQuestion = true; + break; + } + } + + // اگر سوال تحلیلی است، followup را فعال کن + if ($isAnalyticalQuestion) { + $requiresFollowup = true; + } + } elseif ($tool === 'search_by_mobile' && $result['success'] && isset($result['person'])) { + $person = $result['person']; + $finalResponse .= "\n\n📱 اطلاعات شخص با موبایل {$params['mobile']}:\n\n"; + $finalResponse .= "👤 نام: " . ($person['nikename'] ?: $person['name']) . "\n"; + $finalResponse .= "📞 موبایل: " . $person['mobile'] . "\n"; + $finalResponse .= "🏢 کد: " . $person['code'] . "\n"; + if (isset($person['balance_formatted'])) { + $finalResponse .= "💰 تراز: " . $person['balance_formatted'] . " ({$person['status']})\n"; + } + if (!empty($person['address'])) { + $finalResponse .= "📍 آدرس: " . $person['address'] . "\n"; + } + if (!empty($person['email'])) { + $finalResponse .= "📧 ایمیل: " . $person['email'] . "\n"; + } + + $requiresFollowup = false; + } elseif (in_array($tool, ['list_tickets', 'search_tickets', 'get_ticket_statistics']) && $result['success']) { + // برای ابزارهای تیکت، نتایج را مستقیماً نمایش بده + if ($tool === 'list_tickets' && isset($result['tickets'])) { + $tickets = $result['tickets']; + $count = count($tickets); + + $finalResponse .= "\n\n📋 لیست تیکت‌های شما ({$count} تیکت):\n"; + $finalResponse .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; + + foreach ($tickets as $index => $ticket) { + $statusIcon = $this->getTicketStatusIcon($ticket['state']); + $finalResponse .= "\n" . ($index + 1) . ". {$statusIcon} "; + $finalResponse .= $ticket['title'] . "\n"; + $finalResponse .= " 📅 تاریخ: " . $ticket['date'] . "\n"; + $finalResponse .= " 🔢 کد: " . $ticket['code'] . "\n"; + $finalResponse .= " 📄 متن: " . $ticket['body'] . "\n"; + if ($ticket['has_file']) { + $finalResponse .= " 📎 فایل پیوست دارد\n"; + } + } + } elseif ($tool === 'search_tickets' && isset($result['tickets'])) { + $tickets = $result['tickets']; + $count = count($tickets); + + $finalResponse .= "\n\n🔍 نتایج جستجوی تیکت‌ها ({$count} تیکت):\n"; + $finalResponse .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; + + foreach ($tickets as $index => $ticket) { + $statusIcon = $this->getTicketStatusIcon($ticket['state']); + $finalResponse .= "\n" . ($index + 1) . ". {$statusIcon} "; + $finalResponse .= $ticket['title'] . "\n"; + $finalResponse .= " 📅 تاریخ: " . $ticket['date'] . "\n"; + $finalResponse .= " 🔢 کد: " . $ticket['code'] . "\n"; + } + } elseif ($tool === 'get_ticket_statistics' && isset($result['statistics'])) { + $stats = $result['statistics']; + $finalResponse .= "\n\n📊 آمار تیکت‌های شما:\n"; + $finalResponse .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; + $finalResponse .= "📋 کل تیکت‌ها: {$stats['total']}\n"; + $finalResponse .= "⏳ در حال پیگیری: {$stats['pending']}\n"; + $finalResponse .= "✅ پاسخ داده شده: {$stats['answered']}\n"; + $finalResponse .= "🔒 خاتمه یافته: {$stats['closed']}\n"; + } + + $requiresFollowup = false; + } elseif ($tool === 'view_ticket' && $result['success'] && isset($result['ticket'])) { + // نمایش جزئیات تیکت + $ticket = $result['ticket']; + $replies = $result['replies'] ?? []; + + $finalResponse .= "\n\n📋 جزئیات تیکت:\n"; + $finalResponse .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; + $finalResponse .= "🔢 کد: " . $ticket['code'] . "\n"; + $finalResponse .= "📝 عنوان: " . $ticket['title'] . "\n"; + $finalResponse .= "📅 تاریخ: " . $ticket['date'] . "\n"; + $finalResponse .= "📄 متن: " . $ticket['body'] . "\n"; + $finalResponse .= "📊 وضعیت: " . $ticket['state'] . "\n"; + if ($ticket['has_file']) { + $finalResponse .= "📎 فایل پیوست دارد\n"; + } + + if (!empty($replies)) { + $finalResponse .= "\n💬 پاسخ‌ها (" . count($replies) . " پاسخ):\n"; + $finalResponse .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; + + foreach ($replies as $index => $reply) { + $role = $reply['is_admin'] ? '👨‍💼 پشتیبان' : '👤 شما'; + $finalResponse .= "\n" . ($index + 1) . ". {$role}\n"; + $finalResponse .= " 📅 تاریخ: " . $reply['date'] . "\n"; + $finalResponse .= " 📄 متن: " . $reply['body'] . "\n"; + if ($reply['has_file']) { + $finalResponse .= " 📎 فایل پیوست دارد\n"; + } + } + } + + $requiresFollowup = false; + } elseif ($tool === 'create_ticket' && $result['success']) { + // نمایش نتیجه ایجاد تیکت + $finalResponse .= "\n\n✅ تیکت با موفقیت ایجاد شد!\n"; + $finalResponse .= "🔢 کد تیکت: " . $result['ticket']['code'] . "\n"; + $finalResponse .= "📝 عنوان: " . $result['ticket']['title'] . "\n"; + $finalResponse .= "📅 تاریخ: " . $result['ticket']['date'] . "\n"; + $finalResponse .= "📊 وضعیت: " . $result['ticket']['state'] . "\n"; + + $requiresFollowup = false; + } elseif ($tool === 'reply_ticket' && $result['success']) { + // نمایش نتیجه پاسخ به تیکت + $finalResponse .= "\n\n✅ پاسخ با موفقیت ارسال شد!\n"; + $finalResponse .= "📅 تاریخ: " . $result['reply']['date'] . "\n"; + $finalResponse .= "📄 متن: " . $result['reply']['body'] . "\n"; + $requiresFollowup = false; } elseif ($tool === 'show_person' && $result['success'] && isset($result['person'])) { // برای نمایش اطلاعات شخص، نتایج را مستقیماً نمایش بده @@ -952,7 +1136,17 @@ class AIService 'delete_person' => '/delete_person\{name:([^}]+)\}/u', 'edit_person' => '/edit_person\{([^}]+)\}/u', 'show_person' => '/show_person\{name:([^}]+)\}/u', - 'search_persons' => '/search_persons\{search:([^}]+)\}/u' + 'search_persons' => '/search_persons\{search:([^}]+)\}/u', + 'advanced_search_persons' => '/advanced_search_persons\{([^}]+)\}/u', + 'search_by_mobile' => '/search_by_mobile\{mobile:([^}]+)\}/u', + 'search_debtors' => '/search_debtors\{([^}]+)\}/u', + 'search_creditors' => '/search_creditors\{([^}]+)\}/u', + 'create_ticket' => '/create_ticket\{([^}]+)\}/u', + 'list_tickets' => '/list_tickets\{([^}]+)\}/u', + 'view_ticket' => '/view_ticket\{ticket_id:([^}]+)\}/u', + 'reply_ticket' => '/reply_ticket\{([^}]+)\}/u', + 'search_tickets' => '/search_tickets\{([^}]+)\}/u', + 'get_ticket_statistics' => '/get_ticket_statistics\{([^}]*)\}/u' ]; foreach ($patterns as $tool => $pattern) { @@ -1089,6 +1283,62 @@ class AIService case 'search_persons': return $this->getPersonManagementService()->searchPersons($params, $business); + case 'advanced_search_persons': + return $this->getPersonManagementService()->advancedSearchPersons($params, $business); + + case 'search_by_mobile': + return $this->getPersonManagementService()->searchByMobile($params, $business); + + case 'search_debtors': + return $this->getPersonManagementService()->searchDebtors($params, $business); + + case 'search_creditors': + return $this->getPersonManagementService()->searchCreditors($params, $business); + + // ابزارهای مدیریت تیکت + case 'create_ticket': + return $this->getTicketManagementService()->createTicket($params, $business, $user); + + case 'list_tickets': + return $this->getTicketManagementService()->listUserTickets($params, $business, $user); + + case 'view_ticket': + return $this->getTicketManagementService()->viewTicket($params, $business, $user); + + case 'reply_ticket': + return $this->getTicketManagementService()->replyToTicket($params, $business, $user); + + case 'search_tickets': + return $this->getTicketManagementService()->searchTickets($params, $business, $user); + + case 'get_ticket_statistics': + return $this->getTicketManagementService()->getTicketStatistics($params, $business, $user); + + // ابزارهای مدیریت نمودار + case 'create_bar_chart': + return $this->getChartService()->createBarChart($params, $business, $user); + + case 'create_line_chart': + return $this->getChartService()->createLineChart($params, $business, $user); + + case 'create_pie_chart': + return $this->getChartService()->createPieChart($params, $business, $user); + + case 'create_area_chart': + return $this->getChartService()->createAreaChart($params, $business, $user); + + case 'create_radar_chart': + return $this->getChartService()->createRadarChart($params, $business, $user); + + case 'create_scatter_chart': + return $this->getChartService()->createScatterChart($params, $business, $user); + + case 'create_doughnut_chart': + return $this->getChartService()->createDoughnutChart($params, $business, $user); + + case 'create_chart': + return $this->getChartService()->createChart($params, $business, $user); + default: return [ 'success' => false, @@ -1120,6 +1370,30 @@ class AIService $personMessage .= "• {$key}: {$value}\n"; } $toolMessages[] = $personMessage; + } + // اگر نتیجه شامل نمودار است، آن را به صورت ساختاریافته نمایش ده + elseif (isset($result['chart']) && is_array($result['chart'])) { + $chartInfo = $result['chart']; + $chartMessage = "\n📊 نمودار ایجاد شد:\n"; + $chartMessage .= "• نوع: {$chartInfo['type']}\n"; + $chartMessage .= "• عنوان: {$chartInfo['title']}\n"; + $chartMessage .= "• شناسه: {$chartInfo['chart_id']}\n"; + + // اضافه کردن اطلاعات داده‌ها + if (isset($chartInfo['data'])) { + $data = $chartInfo['data']; + if (isset($data['categories'])) { + $chartMessage .= "• دسته‌بندی‌ها: " . implode(', ', $data['categories']) . "\n"; + } + if (isset($data['labels'])) { + $chartMessage .= "• برچسب‌ها: " . implode(', ', $data['labels']) . "\n"; + } + if (isset($data['series'])) { + $chartMessage .= "• تعداد سری‌ها: " . count($data['series']) . "\n"; + } + } + + $toolMessages[] = $chartMessage; } else { $toolMessages[] = $result['message'] ?? 'عملیات با موفقیت انجام شد'; } @@ -1401,6 +1675,32 @@ class AIService return $this->personManagementService; } + public function getTicketManagementService(): TicketManagementService + { + if ($this->ticketManagementService === null) { + $this->ticketManagementService = new TicketManagementService( + $this->entityManager, + $this->log, + $this->provider, + new \App\Service\Jdate() + ); + } + return $this->ticketManagementService; + } + + public function getChartService(): ChartService + { + if ($this->chartService === null) { + $this->chartService = new ChartService( + $this->entityManager, + $this->log, + $this->provider, + new \App\Service\Jdate() + ); + } + return $this->chartService; + } + /** * بررسی وضعیت سرویس هوش مصنوعی */ @@ -1561,6 +1861,19 @@ class AIService ]; } + /** + * دریافت آیکون وضعیت تیکت + */ + private function getTicketStatusIcon(string $state): string + { + return match ($state) { + 'در حال پیگیری' => '⏳', + 'پاسخ داده شده' => '✅', + 'خاتمه یافته' => '🔒', + default => '📋' + }; + } + /** * دریافت لیست ابزارهای موجود */ @@ -1582,6 +1895,65 @@ class AIService 'شخص جدید با نام احمد اضافه کن', 'مشخصات محسن رو نشون بده' ] + ], + 'advanced_search' => [ + 'name' => 'جستجوی پیشرفته اشخاص', + 'description' => 'جستجوی هوشمند اشخاص بر اساس معیارهای مختلف', + 'commands' => [ + 'advanced_search_persons{search:متن جستجو, search_type:نوع جستجو, limit:تعداد نتایج}', + 'search_by_mobile{mobile:شماره موبایل}', + 'search_debtors{search:متن جستجو, limit:تعداد نتایج}', + 'search_creditors{search:متن جستجو, limit:تعداد نتایج}' + ], + 'examples' => [ + 'کی موبایلش 09183282405 هست؟', + 'بدهکاران رو بهم معرفی کن', + 'بستانکاران رو نشون بده', + 'بدهکاران علی رو پیدا کن' + ] + ], + 'ticket_management' => [ + 'name' => 'مدیریت تیکت‌های پشتیبانی', + 'description' => 'ایجاد، مشاهده و مدیریت تیکت‌های پشتیبانی', + 'commands' => [ + 'create_ticket{title:عنوان تیکت, body:متن تیکت, priority:اولویت}', + 'list_tickets{state:وضعیت, limit:تعداد نتایج}', + 'view_ticket{ticket_id:شناسه تیکت}', + 'reply_ticket{ticket_id:شناسه تیکت, body:متن پاسخ}', + 'search_tickets{search:متن جستجو, state:وضعیت}', + 'get_ticket_statistics{}' + ], + 'examples' => [ + 'تیکت جدید با عنوان مشکل در ورود به سیستم', + 'لیست تیکت‌های من', + 'مشاهده تیکت ABC123', + 'پاسخ به تیکت 123', + 'جستجو تیکت مشکل', + 'آمار تیکت‌های من' + ] + ], + 'chart_management' => [ + 'name' => 'مدیریت نمودارها و چارت‌ها', + 'description' => 'ایجاد نمودارهای مختلف با داده‌های پویا', + 'commands' => [ + 'create_bar_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}', + 'create_line_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}', + 'create_pie_chart{title:عنوان, data:[{label:عنوان, value:عدد}]}', + 'create_area_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}', + 'create_radar_chart{title:عنوان, data:[{category:عنوان, value:عدد}]}', + 'create_scatter_chart{title:عنوان, data:[{x:عدد, y:عدد}]}', + 'create_doughnut_chart{title:عنوان, data:[{label:عنوان, value:عدد}]}', + 'create_chart{chart_type:نوع, title:عنوان, data:داده‌ها}' + ], + 'examples' => [ + 'نمودار ستونی فروش ماهانه', + 'نمودار دایره‌ای سهم بازار', + 'نمودار خطی روند رشد', + 'نمودار ناحیه‌ای درآمد سالانه', + 'نمودار راداری عملکرد بخش‌ها', + 'نمودار پراکندگی سن و درآمد', + 'نمودار دونات توزیع محصولات' + ] ] ]; } diff --git a/hesabixCore/src/Service/AI/ChartService.php b/hesabixCore/src/Service/AI/ChartService.php new file mode 100644 index 00000000..f6aff8f2 --- /dev/null +++ b/hesabixCore/src/Service/AI/ChartService.php @@ -0,0 +1,538 @@ +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}]'"; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AI/PersonManagementService.php b/hesabixCore/src/Service/AI/PersonManagementService.php index 63dba871..041b7a85 100644 --- a/hesabixCore/src/Service/AI/PersonManagementService.php +++ b/hesabixCore/src/Service/AI/PersonManagementService.php @@ -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 - فقط اشخاص بدون تراکنش قابل حذف هستند - تمام عملیات در لاگ ثبت می‌شود"; } + + } \ No newline at end of file diff --git a/hesabixCore/src/Service/AI/TicketManagementService.php b/hesabixCore/src/Service/AI/TicketManagementService.php new file mode 100644 index 00000000..f2d65441 --- /dev/null +++ b/hesabixCore/src/Service/AI/TicketManagementService.php @@ -0,0 +1,519 @@ +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 می‌گیرم'"; + } +} \ No newline at end of file diff --git a/webUI/src/components/widgets/AIChart.vue b/webUI/src/components/widgets/AIChart.vue new file mode 100644 index 00000000..d6db0b28 --- /dev/null +++ b/webUI/src/components/widgets/AIChart.vue @@ -0,0 +1,449 @@ + + + + + \ No newline at end of file