Compare commits

...

23 commits

Author SHA1 Message Date
root e4d59c1c19 resolve merge conflicts 2025-07-22 21:28:25 +00:00
Hesabix 9b5e7947cd progress in ai 2025-07-22 08:55:13 +00:00
Hesabix 8a3ebc64cb redisign ai agent from over 2025-07-21 00:02:19 +00:00
Hesabix 39a2846ff6 progress in ai with chart and ticket system 2025-07-19 16:28:55 +00:00
Hesabix 26121aba26 redesign wizard home 2025-07-19 13:04:23 +00:00
Hesabix 3b617f9971 progress in ai in person module 2025-07-19 10:19:33 +00:00
Hesabix fc6f286e0e progress in ai 2025-07-18 16:29:35 +00:00
Hesabix a87d479e82 bug fix in registry 2025-07-18 04:06:15 +00:00
Hesabix 96f8229490 bug fix in wizard activator 2025-07-18 03:45:33 +00:00
Hesabix ecba39c6c9 bug fix in accounting document edit 2025-07-18 03:40:13 +00:00
Hesabix fc2aa36b0e almost finish wizard 2025-07-18 02:59:39 +00:00
Hesabix 186229c848 progress in wizard 2025-07-17 23:00:04 +00:00
Hesabix 574bdd1a5b change snackbar location in some files 2025-07-17 09:13:17 +00:00
Hesabix 7090ff44dc remove header from inquiry panel 2025-07-17 09:09:09 +00:00
Hesabix 2a8ea8cb4a almost finish inquiry panel 2025-07-17 09:07:31 +00:00
Hesabix 227767b0d6 progress in inquery panel 2025-07-16 15:11:53 +00:00
Hesabix 2cb7b8945c almost finish postalcode to address 2025-07-14 15:31:05 +00:00
Hesabix 7762613814 progress in inquery plugin 2025-07-13 23:52:24 +00:00
Hesabix 90f5a5b338 start working on inquiry plugin 2025-07-13 11:11:54 +00:00
Hesabix b1f8af83f4 support for change repositories 2025-07-12 11:19:24 +00:00
Hesabix 0d18762f1e change source of installation from github to own repo 2025-07-11 10:30:33 +00:00
Hesabix 82740193cb bug fix in wallet deposit 2025-07-11 10:25:25 +00:00
Hesabix ddbbf1f102 merge upper in commodity cat with pos app 2025-07-11 06:56:42 +00:00
69 changed files with 9698 additions and 493 deletions

127
AI_PERSON_INTEGRATION.md Normal file
View file

@ -0,0 +1,127 @@
# هوش مصنوعی حسابیکس - یکپارچه‌سازی با اطلاعات اشخاص
## خلاصه
این پروژه قابلیت‌های جدیدی به سیستم هوش مصنوعی حسابیکس اضافه کرده است که به کاربران امکان دسترسی پویا به اطلاعات اشخاص را می‌دهد. هوش مصنوعی حالا می‌تواند به سوالات مربوط به اشخاص، موجودی‌ها و تراکنش‌های مالی پاسخ دهد.
## ویژگی‌های جدید
### 1. دسترسی به اطلاعات اشخاص
- نمایش اطلاعات کامل اشخاص شامل نام، کد، آدرس، تلفن و غیره
- محاسبه و نمایش موجودی مالی اشخاص
- نمایش تراکنش‌های اخیر هر شخص
- نمایش کارت‌های بانکی و اطلاعات مالی
### 2. جستجوی هوشمند
- جستجو بر اساس نام، کد یا شماره تلفن
- پیشنهادات جستجو
- فیلتر بر اساس نوع اشخاص (مشتری، تامین‌کننده، کارمند)
### 3. امنیت و حریم خصوصی
- هر کاربر فقط به اطلاعات اشخاص کسب و کار خود دسترسی دارد
- بررسی دسترسی‌ها قبل از نمایش اطلاعات
- محافظت از اطلاعات حساس
## ساختار فایل‌ها
### Backend (PHP/Symfony)
#### سرویس‌های جدید:
- `PersonDataService.php`: مدیریت داده‌های اشخاص
- `AIService.php`: به‌روزرسانی شده برای پشتیبانی از اطلاعات اشخاص
#### کنترلرهای جدید:
- `wizardController.php`: اضافه شدن endpoint های جدید برای اشخاص
#### API Endpoints جدید:
- `POST /api/wizard/persons/search`: جستجوی اشخاص
- `GET /api/wizard/persons/{personId}`: دریافت اطلاعات شخص
- `GET /api/wizard/persons/{personId}/transactions`: دریافت تراکنش‌های شخص
### Frontend (Vue.js)
#### کامپوننت‌های جدید:
- `PersonInfo.vue`: نمایش اطلاعات کامل شخص
- `home.vue`: به‌روزرسانی شده برای پشتیبانی از قابلیت‌های جدید
## نحوه استفاده
### 1. سوالات مربوط به اشخاص
کاربران می‌توانند سوالاتی مانند موارد زیر بپرسند:
- "اطلاعات شخص احمد محمدی"
- "موجودی مشتری علی رضایی"
- "تراکنش‌های تامین‌کننده شرکت ABC"
- "لیست کارمندان"
### 2. جستجوی مستقیم
- استفاده از پیشنهادات موجود در رابط کاربری
- تایپ نام یا کد شخص در چت
### 3. نمایش اطلاعات
- اطلاعات شخص در دیالوگ جداگانه نمایش داده می‌شود
- شامل موجودی مالی، تراکنش‌ها و اطلاعات تماس
- امکان مشاهده جزئیات کامل
## امنیت
### بررسی دسترسی‌ها:
- هر درخواست ابتدا بررسی می‌شود که کاربر دسترسی لازم را داشته باشد
- اطلاعات فقط برای کسب و کار مربوطه نمایش داده می‌شود
- API endpoints محافظت شده با سیستم احراز هویت
### محافظت از داده‌ها:
- شماره کارت‌های بانکی ماسک می‌شوند
- اطلاعات حساس فیلتر می‌شوند
- لاگ تمام درخواست‌ها ثبت می‌شود
## تنظیمات
### پرامپ هوش مصنوعی:
سیستم به طور خودکار اطلاعات اشخاص را به پرامپ اضافه می‌کند تا هوش مصنوعی بتواند به سوالات مربوطه پاسخ دهد.
### محدودیت‌ها:
- حداکثر 20 نتیجه در جستجو
- حداکثر 10 تراکنش در نمایش
- محدودیت دسترسی بر اساس کسب و کار
## نمونه استفاده
```javascript
// جستجوی شخص
const persons = await this.searchPersons('احمد محمدی');
// دریافت اطلاعات شخص
const personDetails = await this.getPersonDetails(personId);
// دریافت تراکنش‌ها
const transactions = await this.getPersonTransactions(personId, 10);
```
## آینده‌نگری
### قابلیت‌های پیشنهادی:
1. گزارش‌گیری پیشرفته از اشخاص
2. تحلیل روند تراکنش‌ها
3. پیش‌بینی موجودی بر اساس الگوهای گذشته
4. یکپارچه‌سازی با سیستم اعلان‌ها
5. پشتیبانی از تصاویر پروفایل اشخاص
### بهبودهای فنی:
1. کش کردن اطلاعات پرکاربرد
2. بهینه‌سازی کوئری‌های دیتابیس
3. پشتیبانی از pagination برای لیست‌های بزرگ
4. اضافه کردن فیلترهای پیشرفته
## عیب‌یابی
### مشکلات رایج:
1. **خطای دسترسی**: بررسی کنید که کاربر دسترسی AI داشته باشد
2. **عدم یافتن شخص**: نام یا کد را بررسی کنید
3. **خطای شبکه**: اتصال اینترنت را بررسی کنید
### لاگ‌ها:
تمام خطاها در console مرورگر و لاگ‌های سرور ثبت می‌شوند.
## پشتیبانی
برای گزارش مشکلات یا درخواست ویژگی‌های جدید، لطفاً با تیم توسعه تماس بگیرید.

View file

@ -95,3 +95,8 @@ services:
App\Twig\NumberFormatExtension:
tags: ['twig.extension']
App\Cog\PersonService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$access: '@App\Service\Access'

View file

@ -0,0 +1,261 @@
# سیستم هوشمند هوش مصنوعی حسابیکس - نسخه 2.0
## مقدمه
سیستم جدید هوشمند هوش مصنوعی حسابیکس با رویکردی کاملاً متفاوت طراحی شده است. در این نسخه، به جای تشخیص دستی دستورات، به هوش مصنوعی گفته می‌شود که چه ابزارهایی دارد و اجازه داده می‌شود خودش تصمیم بگیرد که از کدام ابزار استفاده کند.
## ویژگی‌های کلیدی
### 🔧 تشخیص خودکار ابزارها
- هوش مصنوعی خودش تشخیص می‌دهد که چه ابزاری مناسب است
- نیازی به تشخیص دستی دستورات نیست
- انعطاف‌پذیری بالا در درک درخواست‌های کاربر
### 📝 پرامپ‌های سیستمی هوشمند
- پرامپ‌های جامع که تمام ابزارها را معرفی می‌کنند
- مثال‌های کاربردی برای هر ابزار
- قوانین و محدودیت‌های استفاده
### 🎯 تعامل چندمرحله‌ای
- امکان انجام عملیات پیچیده در چند مرحله
- جمع‌آوری اطلاعات تدریجی
- تجربه کاربری بهتر
## معماری سیستم
### 1. AIService (سرویس اصلی)
```php
class AIService {
// پرامپ سیستمی هوشمند
private function getSystemPrompt(): string
// پردازش پاسخ هوش مصنوعی
private function processAIResponse(string $aiResponse, ?Business $business, $user): array
// استخراج دستورات ابزار
private function extractToolCommands(string $aiResponse): array
// اجرای دستورات ابزار
private function executeToolCommand(array $command, ?Business $business, $user): array
}
```
### 2. PersonManagementService (مدیریت اشخاص)
```php
class PersonManagementService {
// ابزارهای مدیریت اشخاص
public function addPerson(array $params, Business $business, $user): array
public function deletePerson(array $params, Business $business, $user): array
public function editPerson(array $params, Business $business, $user): array
public function showPerson(array $params, Business $business, $user): array
public function searchPersons(array $params, Business $business): array
}
```
## ابزارهای موجود
### مدیریت اشخاص (person_management)
#### 1. افزودن شخص جدید
```bash
add_person{name:نام شخص}
```
**مثال‌ها:**
- `add_person{name:علی}`
- `add_person{name:احمد محمدی}`
#### 2. حذف شخص
```bash
delete_person{name:نام شخص}
```
**مثال‌ها:**
- `delete_person{name:علی}`
- `delete_person{name:محسن محمودی}`
#### 3. ویرایش شخص
```bash
edit_person{name:نام شخص, phone:موبایل, address:آدرس, email:ایمیل}
```
**مثال‌ها:**
- `edit_person{name:علی, phone:09123456789}`
- `edit_person{name:احمد, address:تهران، خیابان ولیعصر}`
#### 4. نمایش مشخصات
```bash
show_person{name:نام شخص}
```
**مثال‌ها:**
- `show_person{name:علی}`
- `show_person{name:محسن محمودی}`
#### 5. جستجوی اشخاص
```bash
search_persons{search:متن جستجو, limit:تعداد نتایج}
```
**مثال‌ها:**
- `search_persons{search:علی}`
- `search_persons{search:محمد, limit:5}`
## پرامپ سیستمی
پرامپ سیستمی شامل موارد زیر است:
### معرفی ابزارها
```
شما یک دستیار هوشمند برای سیستم حسابداری حسابیکس هستید. شما دسترسی به ابزارهای زیر دارید:
🔧 ابزارهای موجود:
1. **مدیریت اشخاص** (person_management):
- افزودن شخص جدید: add_person{name:نام شخص}
- حذف شخص: delete_person{name:نام شخص}
- ویرایش شخص: edit_person{name:نام شخص, phone:موبایل, address:آدرس, email:ایمیل}
- نمایش مشخصات: show_person{name:نام شخص}
- جستجوی اشخاص: search_persons{search:متن جستجو, limit:تعداد نتایج}
```
### قوانین استفاده
```
📋 قوانین استفاده:
- اگر کاربر درخواست عملیات مدیریت اشخاص دارد، از دستورات بالا استفاده کنید
- نام شخص می‌تواند نام مستعار یا نام کامل باشد
- برای عملیات پیچیده، ابتدا اطلاعات را جمع‌آوری کنید
- همیشه پاسخ فارسی و واضح ارائه دهید
```
### مثال‌های کاربردی
```
💡 مثال‌های استفاده:
- 'علی رو حذف کن' → delete_person{name:علی}
- 'شخص جدید با نام احمد اضافه کن' → add_person{name:احمد}
- 'مشخصات محسن رو نشون بده' → show_person{name:محسن}
```
## جریان کار
### 1. دریافت درخواست کاربر
```
کاربر: "شخص علی را حذف کن"
```
### 2. ساخت پرامپ هوشمند
```
پرامپ = پرامپ سیستمی + اطلاعات کسب و کار + سوال کاربر
```
### 3. ارسال به هوش مصنوعی
```
هوش مصنوعی پرامپ را دریافت کرده و تصمیم می‌گیرد که از ابزار مناسب استفاده کند
```
### 4. تشخیص دستورات ابزار
```
پاسخ هوش مصنوعی: "برای حذف شخص علی، از دستور delete_person{name:علی} استفاده می‌کنم."
```
### 5. استخراج و اجرای دستورات
```
دستور استخراج شده: delete_person{name:علی}
نتیجه اجرا: "شخص علی با موفقیت حذف شد."
```
### 6. ساخت پاسخ نهایی
```
پاسخ نهایی = پاسخ هوش مصنوعی + نتایج ابزارها
```
## مزایای سیستم جدید
### 🚀 هوشمندی بیشتر
- هوش مصنوعی خودش تصمیم می‌گیرد
- نیازی به تشخیص دستی دستورات نیست
- انعطاف‌پذیری بالا در درک درخواست‌ها
### 🔧 قابلیت توسعه
- افزودن ابزارهای جدید آسان است
- پرامپ‌ها قابل به‌روزرسانی هستند
- معماری مقیاس‌پذیر
### 🎯 تجربه کاربری بهتر
- تعامل طبیعی‌تر
- پاسخ‌های هوشمندانه‌تر
- پشتیبانی از عملیات پیچیده
### 🛡️ امنیت و کنترل
- تمام عملیات در لاگ ثبت می‌شود
- بررسی دسترسی کاربران
- کنترل خطاها
## توسعه آینده
### ابزارهای پیشنهادی
1. **مدیریت محصولات**
- افزودن، ویرایش، حذف محصولات
- مدیریت موجودی
- قیمت‌گذاری
2. **مدیریت تراکنش‌ها**
- ثبت تراکنش‌های مالی
- گزارش‌گیری
- تحلیل داده‌ها
3. **گزارش‌گیری هوشمند**
- گزارش‌های مالی
- تحلیل‌های آماری
- پیش‌بینی‌ها
4. **مدیریت حساب‌ها**
- مدیریت حساب‌های بانکی
- صندوق‌ها
- حقوق‌ها
### بهبودهای پیشنهادی
1. **یادگیری ماشین**
- بهبود تشخیص دستورات
- شخصی‌سازی پاسخ‌ها
- پیش‌بینی نیازهای کاربر
2. **پشتیبانی چندزبانه**
- پشتیبانی از زبان‌های مختلف
- تشخیص خودکار زبان
- ترجمه خودکار
3. **یکپارچه‌سازی پیشرفته**
- اتصال به سرویس‌های خارجی
- API های پیشرفته
- وب‌هوک‌ها
## نکات فنی
### مدیریت خطاها
- بررسی وجود کلیدهای مورد نیاز
- مدیریت خطاهای شبکه
- لاگ‌گیری کامل
### بهینه‌سازی عملکرد
- کش‌گذاری پاسخ‌ها
- کاهش درخواست‌های تکراری
- بهینه‌سازی پرامپ‌ها
### امنیت
- بررسی دسترسی کاربران
- اعتبارسنجی ورودی‌ها
- محافظت از داده‌های حساس
## نتیجه‌گیری
سیستم جدید هوشمند هوش مصنوعی حسابیکس با رویکردی نوآورانه و انعطاف‌پذیر طراحی شده است. این سیستم قابلیت توسعه بالایی دارد و می‌تواند به راحتی با نیازهای آینده سازگار شود.
مزایای اصلی این سیستم عبارتند از:
- هوشمندی بیشتر در تشخیص دستورات
- انعطاف‌پذیری بالا
- قابلیت توسعه آسان
- تجربه کاربری بهتر
- امنیت و کنترل بیشتر
این سیستم پایه‌ای محکم برای توسعه‌های آینده فراهم می‌کند و می‌تواند به عنوان یک دستیار هوشمند واقعی برای کاربران حسابیکس عمل کند.

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241201000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create postal_code_inquiry table';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE postal_code_inquiry (
id INT AUTO_INCREMENT NOT NULL,
postal_code VARCHAR(10) NOT NULL,
address_data JSON NOT NULL,
created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
updated_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
UNIQUE INDEX UNIQ_POSTAL_CODE (postal_code),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE postal_code_inquiry');
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace App\Cog;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Person;
use App\Entity\PersonType;
use App\Entity\HesabdariRow;
use App\Service\Explore;
use App\Service\Access;
/**
* سرویس مدیریت اشخاص
*
* این سرویس برای مدیریت عملیات مربوط به اشخاص استفاده می‌شود
*/
class PersonService
{
private EntityManagerInterface $entityManager;
private Access $access;
/**
* سازنده سرویس
*/
public function __construct(EntityManagerInterface $entityManager, Access $access)
{
$this->entityManager = $entityManager;
$this->access = $access;
}
/**
* دریافت اطلاعات شخص
*
* @param string $code کد شخص
* @param array $acc اطلاعات دسترسی
* @return array اطلاعات شخص
* @throws \ReflectionException
*/
public function getPersonInfo(string $code, array $acc): array
{
$person = $this->entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
$types = $this->entityManager->getRepository(PersonType::class)->findAll();
$response = Explore::ExplorePerson($person, $types);
$rows = $this->entityManager->getRepository(HesabdariRow::class)->findBy([
'person' => $person,
'bid' => $acc['bid']
]);
$bs = 0;
$bd = 0;
foreach ($rows as $row) {
try {
$doc = $row->getDoc();
if ($doc && $doc->getMoney() && $doc->getYear() &&
$doc->getMoney()->getId() == $acc['money']->getId() &&
$doc->getYear()->getId() == $acc['year']->getId()) {
$bs += (float) $row->getBs(); // بستانکار
$bd += (float) $row->getBd(); // بدهکار
}
} catch (\Exception $e) {
// در صورت بروز خطا، این ردیف را نادیده می‌گیریم و به ردیف بعدی می‌رویم
continue;
}
}
$response['bs'] = $bs;
$response['bd'] = $bd;
$response['balance'] = $bs - $bd;
return $response;
}
}

View file

@ -0,0 +1,245 @@
<?php
namespace App\Controller;
use App\Entity\AIConversation;
use App\Entity\AIMessage;
use App\Service\Access;
use App\Service\Extractor;
use App\Service\Jdate;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
class AIConversationController extends AbstractController
{
#[Route('/api/ai/conversations/list', name: 'api_ai_conversations_list', methods: ['POST'])]
public function listConversations(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = $request->getPayload()->all();
$search = $params['search'] ?? '';
$category = $params['category'] ?? '';
$conversationRepo = $entityManager->getRepository(AIConversation::class);
if (!empty($search)) {
$conversations = $conversationRepo->searchByTitle($acc['user'], $acc['bid'], $search);
} elseif (!empty($category)) {
$conversations = $conversationRepo->findByCategory($acc['user'], $acc['bid'], $category);
} else {
$conversations = $conversationRepo->findActiveConversations($acc['user'], $acc['bid']);
}
$result = [];
foreach ($conversations as $conversation) {
$messageRepo = $entityManager->getRepository(AIMessage::class);
$lastMessage = $messageRepo->findLastMessageByConversation($conversation);
$stats = $messageRepo->getConversationStats($conversation);
$result[] = [
'id' => $conversation->getId(),
'title' => $conversation->getTitle(),
'category' => $conversation->getCategory(),
'createdAt' => $jdate->jdate('Y/m/d H:i', $conversation->getCreatedAt()),
'updatedAt' => $jdate->jdate('Y/m/d H:i', $conversation->getUpdatedAt()),
'messageCount' => count($conversation->getMessages()),
'lastMessage' => $lastMessage ? $lastMessage->getContent() : '',
'stats' => $stats
];
}
return $this->json($result);
}
#[Route('/api/ai/conversations/create', name: 'api_ai_conversations_create', methods: ['POST'])]
public function createConversation(
Request $request,
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = $request->getPayload()->all();
$title = $params['title'] ?? 'گفتگوی جدید';
$category = $params['category'] ?? 'عمومی';
$conversation = new AIConversation();
$conversation->setUser($acc['user']);
$conversation->setBusiness($acc['bid']);
$conversation->setTitle($title);
$conversation->setCategory($category);
$entityManager->persist($conversation);
$entityManager->flush();
return $this->json([
'id' => $conversation->getId(),
'title' => $conversation->getTitle(),
'category' => $conversation->getCategory(),
'createdAt' => $conversation->getCreatedAt()
]);
}
#[Route('/api/ai/conversations/{id}/messages', name: 'api_ai_conversations_messages', methods: ['POST'])]
public function getConversationMessages(
int $id,
Access $access,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$conversation = $entityManager->getRepository(AIConversation::class)->find($id);
if (!$conversation) {
throw $this->createNotFoundException('گفتگو یافت نشد');
}
// بررسی دسترسی کاربر به این گفتگو
if ($conversation->getUser()->getId() !== $acc['user']->getId() ||
$conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
throw $this->createAccessDeniedException();
}
$messageRepo = $entityManager->getRepository(AIMessage::class);
$messages = $messageRepo->findByConversation($conversation);
$result = [];
foreach ($messages as $message) {
$result[] = [
'id' => $message->getId(),
'role' => $message->getRole(),
'content' => $message->getContent(),
'createdAt' => $jdate->jdate('Y/m/d H:i', $message->getCreatedAt()),
'inputTokens' => $message->getInputTokens(),
'outputTokens' => $message->getOutputTokens(),
'inputCost' => $message->getInputCost(),
'outputCost' => $message->getOutputCost(),
'totalCost' => $message->getTotalCost(),
'model' => $message->getModel(),
'agentSource' => $message->getAgentSource()
];
}
return $this->json($result);
}
#[Route('/api/ai/conversations/{id}/update', name: 'api_ai_conversations_update', methods: ['POST'])]
public function updateConversation(
int $id,
Request $request,
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$conversation = $entityManager->getRepository(AIConversation::class)->find($id);
if (!$conversation) {
throw $this->createNotFoundException('گفتگو یافت نشد');
}
// بررسی دسترسی کاربر به این گفتگو
if ($conversation->getUser()->getId() !== $acc['user']->getId() ||
$conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
throw $this->createAccessDeniedException();
}
$params = $request->getPayload()->all();
if (isset($params['title'])) {
$conversation->setTitle($params['title']);
}
if (isset($params['category'])) {
$conversation->setCategory($params['category']);
}
$conversation->setUpdatedAt(time());
$entityManager->persist($conversation);
$entityManager->flush();
return $this->json(['success' => true]);
}
#[Route('/api/ai/conversations/{id}/delete', name: 'api_ai_conversations_delete', methods: ['POST'])]
public function deleteConversation(
int $id,
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$conversation = $entityManager->getRepository(AIConversation::class)->find($id);
if (!$conversation) {
throw $this->createNotFoundException('گفتگو یافت نشد');
}
// بررسی دسترسی کاربر به این گفتگو
if ($conversation->getUser()->getId() !== $acc['user']->getId() ||
$conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
throw $this->createAccessDeniedException();
}
// بررسی اینکه آیا گفتگو قبلاً حذف نشده است
if ($conversation->isDeleted()) {
return $this->json([
'success' => false,
'error' => 'این گفتگو قبلاً حذف شده است'
]);
}
// حذف نرم گفتگو (تنها علامت‌گذاری به عنوان حذف شده)
$conversationRepo = $entityManager->getRepository(AIConversation::class);
$conversationRepo->softDelete($conversation, true);
return $this->json(['success' => true]);
}
#[Route('/api/ai/conversations/categories', name: 'api_ai_conversations_categories', methods: ['POST'])]
public function getCategories(
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$conversations = $entityManager->getRepository(AIConversation::class)
->findActiveConversations($acc['user'], $acc['bid']);
$categories = [];
foreach ($conversations as $conversation) {
$category = $conversation->getCategory();
if ($category && !in_array($category, $categories)) {
$categories[] = $category;
}
}
return $this->json($categories);
}
}

View file

@ -430,7 +430,7 @@ class AdminController extends AbstractController
if (array_key_exists('rejectChequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproRejectChequeInput', $params['plugAccpro']['rejectChequeInput']);
}
return $this->json(JsonResp::success());
}
@ -450,6 +450,26 @@ class AdminController extends AbstractController
$resp['parsianGatewayAPI'] = $registryMGR->get('system', key: 'parsianGatewayAPI');
$resp['paypingKey'] = $registryMGR->get('system', key: 'paypingKey');
$resp['bitpayKey'] = $registryMGR->get('system', key: 'bitpayKey');
$resp['inquiryPanel'] = $registryMGR->get('system', key: 'inquiryPanel');
$resp['inquiryZohalAPIKey'] = $registryMGR->get('system', key: 'inquiryZohalAPIKey');
$resp['enablePostalCodeToAddress'] = $registryMGR->get('system', key: 'enablePostalCodeToAddress');
$resp['inquiryPanelEnable'] = $registryMGR->get('system', key: 'inquiryPanelEnable');
$resp['postalCodeToAddressFee'] = $registryMGR->get('system', key: 'postalCodeToAddressFee');
$resp['enableCardToSheba'] = $registryMGR->get('system', key: 'enableCardToSheba');
$resp['cardToShebaFee'] = $registryMGR->get('system', key: 'cardToShebaFee');
$resp['enableAccountToSheba'] = $registryMGR->get('system', key: 'enableAccountToSheba');
$resp['accountToShebaFee'] = $registryMGR->get('system', key: 'accountToShebaFee');
// تنظیمات جادوگر هوش مصنوعی
$resp['aiEnabled'] = $registryMGR->get('system', key: 'aiEnabled');
$resp['aiAgentSource'] = $registryMGR->get('system', key: 'aiAgentSource');
$resp['aiModel'] = $registryMGR->get('system', key: 'aiModel');
$resp['aiApiKey'] = $registryMGR->get('system', key: 'aiApiKey');
$resp['localModelAddress'] = $registryMGR->get('system', key: 'localModelAddress');
$resp['inputTokenPrice'] = $registryMGR->get('system', key: 'inputTokenPrice');
$resp['outputTokenPrice'] = $registryMGR->get('system', key: 'outputTokenPrice');
$resp['aiPrompt'] = $registryMGR->get('system', key: 'aiPrompt');
return $this->json($resp);
}
@ -474,6 +494,34 @@ class AdminController extends AbstractController
$registryMGR->update('system', 'parsianGatewayAPI', $params['parsianGatewayAPI']);
$registryMGR->update('system', 'paypingKey', $params['paypingKey']);
$registryMGR->update('system', 'bitpayKey', $params['bitpayKey']);
$registryMGR->update('system', 'inquiryPanel', $params['inquiryPanel']);
$registryMGR->update('system', 'inquiryZohalAPIKey', $params['inquiryZohalAPIKey']);
$registryMGR->update('system', 'enablePostalCodeToAddress', $params['enablePostalCodeToAddress']);
$registryMGR->update('system', 'inquiryPanelEnable', $params['inquiryPanelEnable']);
$registryMGR->update('system', 'postalCodeToAddressFee', $params['postalCodeToAddressFee']);
$registryMGR->update('system', 'enableCardToSheba', $params['enableCardToSheba']);
$registryMGR->update('system', 'cardToShebaFee', $params['cardToShebaFee']);
$registryMGR->update('system', 'enableAccountToSheba', $params['enableAccountToSheba']);
$registryMGR->update('system', 'accountToShebaFee', $params['accountToShebaFee']);
// ذخیره تنظیمات جادوگر هوش مصنوعی
if (array_key_exists('aiEnabled', $params))
$registryMGR->update('system', 'aiEnabled', $params['aiEnabled']);
if (array_key_exists('aiAgentSource', $params))
$registryMGR->update('system', 'aiAgentSource', $params['aiAgentSource']);
if (array_key_exists('aiModel', $params))
$registryMGR->update('system', 'aiModel', $params['aiModel']);
if (array_key_exists('aiApiKey', $params))
$registryMGR->update('system', 'aiApiKey', $params['aiApiKey']);
if (array_key_exists('localModelAddress', $params))
$registryMGR->update('system', 'localModelAddress', $params['localModelAddress']);
if (array_key_exists('inputTokenPrice', $params))
$registryMGR->update('system', 'inputTokenPrice', $params['inputTokenPrice']);
if (array_key_exists('outputTokenPrice', $params))
$registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice']);
if (array_key_exists('aiPrompt', $params))
$registryMGR->update('system', 'aiPrompt', $params['aiPrompt']);
$entityManager->persist($item);
$entityManager->flush();
return $this->json(['result' => 1]);
@ -592,6 +640,7 @@ class AdminController extends AbstractController
$temp['cardPan'] = $item->getCardPan();
$temp['refID'] = $item->getRefID();
$temp['shaba'] = $item->getShaba();
$temp['amount'] = $item->getAmount();
$temp['dateSubmit'] = $jdate->jdate('Y/n/d H:i', $item->getDateSubmit());
$temp['gatePay'] = $item->getGatePay();
$resp[] = $temp;

View file

@ -545,6 +545,8 @@ class BusinessController extends AbstractController
'plugHrmDocs' => true,
'plugGhestaManager' => true,
'plugTaxSettings' => true,
'inquiry' => true,
'ai' => true,
];
} elseif ($perm) {
$result = [
@ -589,6 +591,8 @@ class BusinessController extends AbstractController
'plugHrmDocs' => $perm->isPlugHrmDocs(),
'plugGhestaManager' => $perm->isPlugGhestaManager(),
'plugTaxSettings' => $perm->isPlugTaxSettings(),
'inquiry' => $perm->isInquiry(),
'ai' => $perm->isAi(),
];
}
return $this->json($result);
@ -658,6 +662,9 @@ class BusinessController extends AbstractController
$perm->setPlugRepservice($params['plugRepservice']);
$perm->setPlugHrmDocs($params['plugHrmDocs']);
$perm->setPlugGhestaManager($params['plugGhestaManager']);
$perm->setPlugTaxSettings($params['plugTaxSettings']);
$perm->setInquiry($params['inquiry']);
$perm->setAi($params['ai']);
$entityManager->persist($perm);
$entityManager->flush();
$log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);

View file

@ -143,7 +143,8 @@ class CommodityController extends AbstractController
$count += $row->getCommdityCount();
} else {
$count -= $row->getCommdityCount();
} }
}
}
$temp['count'] = $count;
}
return $temp;
@ -1104,17 +1105,41 @@ class CommodityController extends AbstractController
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('upper', $params) || !array_key_exists('text', $params))
if (!array_key_exists('text', $params))
return $this->json(['result' => -1]);
if ($this->isDefaultCategoryName($params['text'])) {
return $this->json([
'result' => 4,
'result' => 4,
'message' => 'این نام برای دسته‌بندی مجاز نیست',
'errorCode' => 'DEFAULT_CATEGORY_NAME'
]);
}
if (!array_key_exists('upper', $params)) {
$upper = $entityManager->getRepository(CommodityCat::class)->findOneBy([
'upper' => null,
'bid' => $acc['bid']
]);
if (!$upper) {
$upper = new CommodityCat();
$upper->setBid($acc['bid']);
$upper->setUpper(null);
$upper->setName('دسته بندی ها');
$upper->setRoot(true);
$entityManager->persist($upper);
$entityManager->flush();
}
$cat = new CommodityCat();
$cat->setBid($acc['bid']);
$cat->setRoot(false);
$cat->setName($params['text']);
$cat->setUpper($upper->getId());
$entityManager->persist($cat);
$entityManager->flush();
return $this->json(['result' => 1, 'id' => $cat->getId()]);
}
$upper = $entityManager->getRepository(CommodityCat::class)->find($params['upper']);
if ($upper) {
if ($upper->getBid() == $acc['bid']) {
@ -1142,22 +1167,22 @@ class CommodityController extends AbstractController
}
if (!array_key_exists('id', $params) || !array_key_exists('text', $params))
return $this->json(['result' => -1]);
if ($this->isDefaultCategoryName($params['text'])) {
return $this->json([
'result' => 4,
'result' => 4,
'message' => 'این نام برای دسته‌بندی مجاز نیست',
'errorCode' => 'DEFAULT_CATEGORY_NAME'
]);
}
$node = $entityManager->getRepository(CommodityCat::class)->find($params['id']);
if ($node) {
if ($node->getBid() == $acc['bid']) {
// بررسی دسته‌بندی پیش‌فرض
if ($this->isDefaultCategoryName($node->getName())) {
return $this->json([
'result' => 4,
'result' => 4,
'message' => 'ویرایش دسته‌بندی پیش‌فرض مجاز نیست',
'errorCode' => 'DEFAULT_CATEGORY_EDIT'
]);
@ -1627,7 +1652,7 @@ class CommodityController extends AbstractController
// بررسی دسته‌بندی پیش‌فرض
if ($this->isDefaultCategoryName($category->getName())) {
return $this->json([
'Success' => false,
'Success' => false,
'message' => 'حذف دسته‌بندی پیش‌فرض مجاز نیست',
'errorCode' => 'DEFAULT_CATEGORY_DELETE'
], 400);

View file

@ -18,6 +18,7 @@ use App\Entity\Storeroom;
use App\Entity\StoreroomItem;
use App\Entity\StoreroomTicket;
use App\Service\Explore;
use App\Cog\PersonService;
use Doctrine\ORM\EntityManagerInterface;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
@ -61,40 +62,13 @@ class PersonsController extends AbstractController
* @throws \ReflectionException
*/
#[Route('/api/person/info/{code}', name: 'app_persons_info')]
public function app_persons_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
public function app_persons_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PersonService $personService): JsonResponse
{
$acc = $access->hasRole('person');
if (!$acc)
throw $this->createAccessDeniedException();
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
$types = $entityManager->getRepository(PersonType::class)->findAll();
$response = Explore::ExplorePerson($person, $types);
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'person' => $person,
'bid' => $acc['bid']
]);
$bs = 0;
$bd = 0;
foreach ($rows as $row) {
try {
$doc = $row->getDoc();
if ($doc && $doc->getMoney() && $doc->getYear() &&
$doc->getMoney()->getId() == $acc['money']->getId() &&
$doc->getYear()->getId() == $acc['year']->getId()) {
$bs += (float) $row->getBs(); // بستانکار
$bd += (float) $row->getBd(); // بدهکار
}
} catch (\Exception $e) {
// در صورت بروز خطا، این ردیف را نادیده می‌گیریم و به ردیف بعدی می‌رویم
continue;
}
}
$response['bs'] = $bs;
$response['bd'] = $bd;
$response['balance'] = $bs - $bd;
$response = $personService->getPersonInfo($code, $acc);
return $this->json($response);
}

View file

@ -0,0 +1,327 @@
<?php
namespace App\Controller\Plugins\inquiry;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\PlugGhestaDoc;
use App\Entity\PlugGhestaItem;
use App\Entity\HesabdariDoc;
use App\Entity\Person;
use App\Service\Access;
use App\Service\Provider;
use App\Service\Printers;
use App\Entity\PrintOptions;
use App\Service\Log;
use App\Entity\Business;
use App\Service\registryMGR;
use App\Service\Inquiry;
class PlugInquiryMainController extends AbstractController
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
#[Route('/api/plugins/inquiry/settings/get', name: 'plugin_inquiry_settings_get', methods: ['GET'])]
public function plugin_inquiry_settings_get(registryMGR $registryMGR): JsonResponse
{
$resp['inquiryPanel'] = $registryMGR->get('system', key: 'inquiryPanel');
$resp['enablePostalCodeToAddress'] = $registryMGR->get('system', key: 'enablePostalCodeToAddress');
$resp['inquiryPanelEnable'] = $registryMGR->get('system', key: 'inquiryPanelEnable');
$resp['postalCodeToAddressFee'] = $registryMGR->get('system', key: 'postalCodeToAddressFee');
$resp['enableCardToSheba'] = $registryMGR->get('system', key: 'enableCardToSheba');
$resp['cardToShebaFee'] = $registryMGR->get('system', key: 'cardToShebaFee');
$resp['enableAccountToSheba'] = $registryMGR->get('system', key: 'enableAccountToSheba');
$resp['accountToShebaFee'] = $registryMGR->get('system', key: 'accountToShebaFee');
return $this->json($resp);
}
#[Route('/api/plugins/inquiry/postalcode-to-address', name: 'plugin_inquiry_postalcode_to_address', methods: ['POST'])]
public function plugin_inquiry_postalcode_to_address(Inquiry $inquiry, Access $access, Request $request, registryMGR $registryMGR, Log $log): JsonResponse
{
$acc = $access->hasRole('inquiry');
if (!$acc) {
return $this->json([
'success' => false,
'message' => 'شما دسترسی به این سرویس را ندارید'
]);
}
// دریافت کد پستی از درخواست
$data = json_decode($request->getContent(), true);
$postalCode = $data['postal_code'] ?? null;
if (!$postalCode) {
return $this->json([
'success' => false,
'message' => 'کد پستی ارسال نشده است'
]);
}
// فراخوانی سرویس استعلام کد پستی
$result = $inquiry->postalCodeToAddress($postalCode);
// بررسی نتیجه و بازگرداندن پاسخ مناسب
if (isset($result['result']) && $result['result'] == 1) {
$isFromCache = isset($result['response_body']['message']) &&
strpos($result['response_body']['message'], 'از کش') !== false;
// ثبت لاگ بر اساس منبع داده
$logMessage = $isFromCache
? "استعلام کد پستی {$postalCode} از کش (بدون کسر کارمزد)"
: "استعلام کد پستی {$postalCode} از API (کسر کارمزد: " . $registryMGR->get('system', key: 'postalCodeToAddressFee') . " ریال)";
$log->insert(
'استعلام',
$logMessage,
$acc['user'],
$acc['bid']
);
// فقط در صورت عدم وجود در کش، کارمزد کسر شود
if (!$isFromCache) {
if ($acc['bid']->getSmsCharge() < $registryMGR->get('system', key: 'postalCodeToAddressFee')) {
// ثبت لاگ عدم موجودی کافی
$log->insert(
'استعلام',
"عدم موجودی کافی برای استعلام کد پستی {$postalCode}",
$acc['user'],
$acc['bid']
);
return $this->json([
'success' => false,
'message' => 'موجودی شما برای این سرویس کافی نیست'
]);
}
$business = $acc['bid'];
$business->setSmsCharge($business->getSmsCharge() - $registryMGR->get('system', key: 'postalCodeToAddressFee'));
$this->entityManager->persist($business);
$this->entityManager->flush();
}
return $this->json([
'success' => true,
'data' => $result['response_body']['data']['address'] ?? null,
'message' => $result['response_body']['message'] ?? 'موفق',
'from_cache' => $isFromCache
]);
} else {
// ثبت لاگ خطا
$errorMessage = $result['message'] ?? 'خطا در استعلام کد پستی';
$log->insert(
'استعلام',
"خطا در استعلام کد پستی {$postalCode}: {$errorMessage}",
$acc['user'],
$acc['bid']
);
return $this->json([
'success' => false,
'message' => $errorMessage,
'error_code' => $result['error_code'] ?? null
]);
}
}
#[Route('/api/plugins/inquiry/card-to-sheba', name: 'plugin_inquiry_card_to_sheba', methods: ['POST'])]
public function plugin_inquiry_card_to_sheba(Inquiry $inquiry, Access $access, Request $request, registryMGR $registryMGR, Log $log): JsonResponse
{
$acc = $access->hasRole('inquiry');
if (!$acc) {
return $this->json([
'success' => false,
'message' => 'شما دسترسی به این سرویس را ندارید'
]);
}
// بررسی فعال بودن سرویس
if (!$registryMGR->get('system', key: 'enableCardToSheba')) {
return $this->json([
'success' => false,
'message' => 'این سرویس در حال حاضر غیرفعال است'
]);
}
// دریافت شماره کارت از درخواست
$data = json_decode($request->getContent(), true);
$cardNumber = $data['card_number'] ?? null;
if (!$cardNumber) {
return $this->json([
'success' => false,
'message' => 'شماره کارت ارسال نشده است'
]);
}
// فراخوانی سرویس استعلام کارت به شبا
$result = $inquiry->cardToSheba($cardNumber);
// بررسی نتیجه و بازگرداندن پاسخ مناسب
if (isset($result['result']) && $result['result'] == 1) {
$isFromCache = isset($result['response_body']['message']) &&
strpos($result['response_body']['message'], 'از حافظه موقت') !== false;
// ثبت لاگ بر اساس منبع داده
$logMessage = $isFromCache
? "استعلام کارت به شبا {$cardNumber} از حافظه موقت (بدون کسر کارمزد)"
: "استعلام کارت به شبا {$cardNumber} از API (کسر کارمزد: " . $registryMGR->get('system', key: 'cardToShebaFee') . " ریال)";
$log->insert(
'استعلام',
$logMessage,
$acc['user'],
$acc['bid']
);
// فقط در صورت عدم وجود در کش، کارمزد کسر شود
if (!$isFromCache) {
if ($acc['bid']->getSmsCharge() < $registryMGR->get('system', key: 'cardToShebaFee')) {
// ثبت لاگ عدم موجودی کافی
$log->insert(
'استعلام',
"عدم موجودی کافی برای استعلام کارت به شبا {$cardNumber}",
$acc['user'],
$acc['bid']
);
return $this->json([
'success' => false,
'message' => 'موجودی شما برای این سرویس کافی نیست'
]);
}
$business = $acc['bid'];
$business->setSmsCharge($business->getSmsCharge() - $registryMGR->get('system', key: 'cardToShebaFee'));
$this->entityManager->persist($business);
$this->entityManager->flush();
}
return $this->json([
'success' => true,
'data' => $result['response_body']['data'] ?? null,
'message' => $result['response_body']['message'] ?? 'موفق',
'from_cache' => $isFromCache
]);
} else {
// ثبت لاگ خطا
$errorMessage = $result['message'] ?? 'خطا در استعلام کارت به شبا';
$log->insert(
'استعلام',
"خطا در استعلام کارت به شبا {$cardNumber}: {$errorMessage}",
$acc['user'],
$acc['bid']
);
return $this->json([
'success' => false,
'message' => $errorMessage,
'error_code' => $result['error_code'] ?? null
]);
}
}
#[Route('/api/plugins/inquiry/account-to-sheba', name: 'plugin_inquiry_account_to_sheba', methods: ['POST'])]
public function plugin_inquiry_account_to_sheba(Inquiry $inquiry, Access $access, Request $request, registryMGR $registryMGR, Log $log): JsonResponse
{
$acc = $access->hasRole('inquiry');
if (!$acc) {
return $this->json([
'success' => false,
'message' => 'شما دسترسی به این سرویس را ندارید'
]);
}
// بررسی فعال بودن سرویس
if (!$registryMGR->get('system', key: 'enableAccountToSheba')) {
return $this->json([
'success' => false,
'message' => 'این سرویس در حال حاضر غیرفعال است'
]);
}
// دریافت داده‌ها از درخواست
$data = json_decode($request->getContent(), true);
$bankCode = $data['bank_code'] ?? null;
$accountNumber = $data['account_number'] ?? null;
if (!$bankCode || !$accountNumber) {
return $this->json([
'success' => false,
'message' => 'کد بانک و شماره حساب الزامی است'
]);
}
// فراخوانی سرویس استعلام حساب به شبا
$result = $inquiry->accountToSheba($bankCode, $accountNumber);
// بررسی نتیجه و بازگرداندن پاسخ مناسب
if (isset($result['result']) && $result['result'] == 1) {
$isFromCache = isset($result['response_body']['message']) &&
strpos($result['response_body']['message'], 'از حافظه موقت') !== false;
// ثبت لاگ بر اساس منبع داده
$logMessage = $isFromCache
? "استعلام حساب به شبا {$bankCode}/{$accountNumber} از حافظه موقت (بدون کسر کارمزد)"
: "استعلام حساب به شبا {$bankCode}/{$accountNumber} از API (کسر کارمزد: " . $registryMGR->get('system', key: 'accountToShebaFee') . " ریال)";
$log->insert(
'استعلام',
$logMessage,
$acc['user'],
$acc['bid']
);
// فقط در صورت عدم وجود در کش، کارمزد کسر شود
if (!$isFromCache) {
if ($acc['bid']->getSmsCharge() < $registryMGR->get('system', key: 'accountToShebaFee')) {
// ثبت لاگ عدم موجودی کافی
$log->insert(
'استعلام',
"عدم موجودی کافی برای استعلام حساب به شبا {$bankCode}/{$accountNumber}",
$acc['user'],
$acc['bid']
);
return $this->json([
'success' => false,
'message' => 'موجودی شما برای این سرویس کافی نیست'
]);
}
$business = $acc['bid'];
$business->setSmsCharge($business->getSmsCharge() - $registryMGR->get('system', key: 'accountToShebaFee'));
$this->entityManager->persist($business);
$this->entityManager->flush();
}
return $this->json([
'success' => true,
'data' => $result['response_body']['data'] ?? null,
'message' => $result['response_body']['message'] ?? 'موفق',
'from_cache' => $isFromCache
]);
} else {
// ثبت لاگ خطا
$errorMessage = $result['message'] ?? 'خطا در استعلام حساب به شبا';
$log->insert(
'استعلام',
"خطا در استعلام حساب به شبا {$bankCode}/{$accountNumber}: {$errorMessage}",
$acc['user'],
$acc['bid']
);
return $this->json([
'success' => false,
'message' => $errorMessage,
'error_code' => $result['error_code'] ?? null
]);
}
}
}

View file

@ -23,8 +23,9 @@ final class UpdateCoreController extends AbstractController
public function api_admin_updatecore_run(): JsonResponse
{
$projectDir = $this->getParameter('kernel.project_dir');
$gitRoot = dirname($projectDir); // رفتن به ریشه پروژه
$uuid = uniqid();
$stateFile = $projectDir . '/../hesabixBackup/update_state_' . $uuid . '.json';
$stateFile = $gitRoot . '/hesabixBackup/update_state_' . $uuid . '.json';
if (!file_exists(dirname($stateFile))) {
mkdir(dirname($stateFile), 0755, true);
@ -41,7 +42,7 @@ final class UpdateCoreController extends AbstractController
'COMPOSER_HOME' => '/var/www/.composer',
]);
$process = new Process(['php', 'bin/console', 'hesabix:update', $stateFile], $projectDir, $env);
$process = new Process(['php', 'hesabixCore/bin/console', 'hesabix:update', $stateFile], $gitRoot, $env);
$process->setTimeout(7200); // افزایش تایم‌اوت به 2 ساعت
$process->start(function ($type, $buffer) use ($stateFile) {
$state = json_decode(file_get_contents($stateFile), true) ?? ['uuid' => uniqid(), 'log' => ''];
@ -70,7 +71,9 @@ final class UpdateCoreController extends AbstractController
], 400);
}
$stateFile = $this->getParameter('kernel.project_dir') . '/../hesabixBackup/update_state_' . $uuid . '.json';
$projectDir = $this->getParameter('kernel.project_dir');
$gitRoot = dirname($projectDir);
$stateFile = $gitRoot . '/hesabixBackup/update_state_' . $uuid . '.json';
if (!file_exists($stateFile)) {
return new JsonResponse([
@ -97,7 +100,7 @@ final class UpdateCoreController extends AbstractController
}
if (!$isRunning) {
$backupDir = $this->getParameter('kernel.project_dir') . '/../hesabixBackup';
$backupDir = $gitRoot . '/hesabixBackup';
$stateFiles = glob($backupDir . '/update_state_*.json');
foreach ($stateFiles as $file) {
if (is_file($file)) {
@ -128,7 +131,9 @@ final class UpdateCoreController extends AbstractController
return new JsonResponse(['status' => 'error', 'message' => 'UUID is required'], 400);
}
$stateFile = $this->getParameter('kernel.project_dir') . '/../hesabixBackup/update_state_' . $uuid . '.json';
$projectDir = $this->getParameter('kernel.project_dir');
$gitRoot = dirname($projectDir);
$stateFile = $gitRoot . '/hesabixBackup/update_state_' . $uuid . '.json';
return new StreamedResponse(function () use ($stateFile) {
header('Content-Type: text/event-stream');
@ -167,13 +172,14 @@ final class UpdateCoreController extends AbstractController
public function api_admin_updatecore_commits(): JsonResponse
{
$projectDir = $this->getParameter('kernel.project_dir');
$gitRoot = dirname($projectDir); // رفتن به ریشه پروژه
$currentProcess = new Process(['git', 'rev-parse', 'HEAD'], $projectDir);
$currentProcess = new Process(['git', 'rev-parse', 'HEAD'], $gitRoot);
$currentProcess->setTimeout(7200); // افزایش تایم‌اوت
$currentProcess->run();
$currentCommit = $currentProcess->isSuccessful() ? trim($currentProcess->getOutput()) : 'unknown';
$targetProcess = new Process(['git', 'ls-remote', 'origin', 'HEAD'], $projectDir);
$targetProcess = new Process(['git', 'ls-remote', 'origin', 'HEAD'], $gitRoot);
$targetProcess->setTimeout(7200); // افزایش تایم‌اوت
$targetProcess->run();
$targetOutput = $targetProcess->isSuccessful() ? explode("\t", trim($targetProcess->getOutput()))[0] : 'unknown';
@ -428,4 +434,170 @@ final class UpdateCoreController extends AbstractController
], 500);
}
}
#[Route('/api/admin/updatecore/current-source', name: 'api_admin_updatecore_current_source', methods: ['GET'])]
public function api_admin_updatecore_current_source(): JsonResponse
{
$projectDir = $this->getParameter('kernel.project_dir');
$gitRoot = dirname($projectDir); // رفتن به ریشه پروژه
$output = '';
try {
// بررسی اینکه آیا پروژه یک مخزن Git است
if (!is_dir($gitRoot . '/.git')) {
return new JsonResponse([
'status' => 'error',
'message' => 'این پروژه یک مخزن Git نیست',
'sourceUrl' => '',
], 400);
}
// دریافت آدرس مخزن origin فعلی
$process = new Process(['git', 'remote', 'get-url', 'origin'], $gitRoot);
$process->setTimeout(7200); // افزایش تایم‌اوت
$process->run();
if (!$process->isSuccessful()) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در دریافت آدرس مخزن: ' . $process->getErrorOutput(),
'sourceUrl' => '',
], 500);
}
$sourceUrl = trim($process->getOutput());
$output .= "آدرس مخزن فعلی: $sourceUrl\n";
return new JsonResponse([
'status' => 'success',
'sourceUrl' => $sourceUrl,
'output' => $output,
]);
} catch (\Exception $e) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در بررسی مخزن: ' . $e->getMessage(),
'sourceUrl' => '',
], 500);
}
}
#[Route('/api/admin/updatecore/change-source', name: 'api_admin_updatecore_change_source', methods: ['POST'])]
public function api_admin_updatecore_change_source(Request $request): JsonResponse
{
$sourceUrl = $request->getPayload()->get('sourceUrl');
$output = '';
if (!$sourceUrl || !filter_var($sourceUrl, FILTER_VALIDATE_URL) && !preg_match('/^git@[^:]+:[^\/]+\/[^\/]+\.git$/', $sourceUrl)) {
return new JsonResponse([
'status' => 'error',
'message' => 'آدرس مخزن نامعتبر است. لطفاً یک آدرس HTTP یا SSH معتبر وارد کنید.',
'output' => $output,
], 400);
}
$projectDir = $this->getParameter('kernel.project_dir');
$gitRoot = dirname($projectDir); // رفتن به ریشه پروژه
try {
// بررسی اینکه آیا پروژه یک مخزن Git است
if (!is_dir($gitRoot . '/.git')) {
return new JsonResponse([
'status' => 'error',
'message' => 'این پروژه یک مخزن Git نیست',
'output' => $output,
], 400);
}
$output .= "شروع تغییر آدرس مخزن...\n";
// دریافت آدرس مخزن فعلی
$currentProcess = new Process(['git', 'remote', 'get-url', 'origin'], $gitRoot);
$currentProcess->setTimeout(7200);
$currentProcess->run();
$currentUrl = $currentProcess->isSuccessful() ? trim($currentProcess->getOutput()) : '';
if ($currentUrl) {
$output .= "آدرس مخزن فعلی: $currentUrl\n";
}
// تغییر آدرس مخزن origin
$changeProcess = new Process(['git', 'remote', 'set-url', 'origin', $sourceUrl], $gitRoot);
$changeProcess->setTimeout(7200);
$changeProcess->run();
if (!$changeProcess->isSuccessful()) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در تغییر آدرس مخزن: ' . $changeProcess->getErrorOutput(),
'output' => $output,
], 500);
}
$output .= "آدرس مخزن به $sourceUrl تغییر یافت\n";
// بررسی اتصال به مخزن جدید
$testProcess = new Process(['git', 'remote', 'show', 'origin'], $gitRoot);
$testProcess->setTimeout(7200);
$testProcess->run();
if (!$testProcess->isSuccessful()) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در اتصال به مخزن جدید: ' . $testProcess->getErrorOutput(),
'output' => $output,
], 500);
}
$output .= "اتصال به مخزن جدید با موفقیت برقرار شد\n";
// دریافت اطلاعات مخزن جدید
$fetchProcess = new Process(['git', 'fetch', 'origin'], $gitRoot);
$fetchProcess->setTimeout(7200);
$fetchProcess->run();
if (!$fetchProcess->isSuccessful()) {
$output .= "هشدار: خطا در دریافت اطلاعات از مخزن جدید: " . $fetchProcess->getErrorOutput() . "\n";
} else {
$output .= "اطلاعات مخزن جدید با موفقیت دریافت شد\n";
}
// بررسی branch های موجود
$branchProcess = new Process(['git', 'branch', '-r'], $gitRoot);
$branchProcess->setTimeout(7200);
$branchProcess->run();
if ($branchProcess->isSuccessful()) {
$branches = trim($branchProcess->getOutput());
if ($branches) {
$output .= "شاخه‌های موجود در مخزن جدید:\n$branches\n";
} else {
$output .= "هیچ شاخه‌ای در مخزن جدید یافت نشد\n";
}
}
// پاک کردن کش Git
$cleanProcess = new Process(['git', 'gc', '--prune=now'], $gitRoot);
$cleanProcess->setTimeout(7200);
$cleanProcess->run();
if ($cleanProcess->isSuccessful()) {
$output .= "کش Git پاک شد\n";
}
return new JsonResponse([
'status' => 'success',
'message' => 'آدرس مخزن با موفقیت تغییر یافت و اتصال برقرار شد',
'output' => $output,
]);
} catch (\Exception $e) {
return new JsonResponse([
'status' => 'error',
'message' => 'خطا در تغییر آدرس مخزن: ' . $e->getMessage(),
'output' => $output,
], 500);
}
}
}

View file

@ -0,0 +1,203 @@
<?php
namespace App\Controller;
use App\Entity\AIConversation;
use App\Entity\AIMessage;
use App\Service\AGI\AGIService;
use App\Service\Access;
use App\Service\Extractor;
use App\Service\Log;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
class wizardController extends AbstractController
{
private AGIService $agiService;
public function __construct(AGIService $agiService)
{
$this->agiService = $agiService;
}
#[Route('/api/wizard/talk', name: 'wizard_talk', methods: ['POST'])]
public function wizard_talk(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
Log $log
): JsonResponse
{
try {
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json([
'success' => false,
'error' => 'دسترسی غیرمجاز',
'debug_info' => [
'access' => $acc
]
]);
}
// بررسی دسترسی هوش مصنوعی
if (!$acc['ai']) {
return $this->json([
'success' => false,
'error' => 'شما دسترسی استفاده از هوش مصنوعی را ندارید',
'debug_info' => [
'ai_access' => $acc['ai']
]
]);
}
$params = json_decode($request->getContent(), true) ?? [];
if (!isset($params['message']) || empty($params['message'])) {
return $this->json([
'success' => false,
'error' => 'پیام الزامی است',
'debug_info' => [
'params' => $params
]
]);
}
$message = $params['message'];
$options = $params['options'] ?? [];
$conversationId = $params['conversationId'] ?? null;
// بررسی فعال بودن هوش مصنوعی
$aiStatus = $this->agiService->checkAIServiceStatus();
if (!$aiStatus['isEnabled']) {
return $this->json([
'success' => false,
'error' => 'سرویس هوش مصنوعی غیرفعال است',
'debug_info' => [
'ai_status' => $aiStatus
]
]);
}
// بررسی اعتبار کسب و کار
$business = $acc['bid'];
$currentBalance = (float) ($business->getSmsCharge() ?? 0);
$estimatedCost = 100;
if ($currentBalance < $estimatedCost) {
return $this->json([
'success' => false,
'error' => "اعتبار شما کافی نیست (اعتبار فعلی: {$currentBalance} ریال). برای شارژ حساب خود به بخش شارژ مراجعه کنید.",
'balance' => $currentBalance,
'required' => $estimatedCost,
'showChargeButton' => true,
'debug_info' => [
'balance' => $currentBalance,
'required' => $estimatedCost
]
]);
}
// استفاده از AGIService برای مدیریت گفتگو و ارسال درخواست
$result = $this->agiService->sendRequest($message, $business, $acc['user'], $conversationId, $acc);
if ($result['success']) {
$responseContent = $result['response'] ?? $result['message'] ?? 'عملیات با موفقیت انجام شد';
$response = [
'success' => true,
'response' => $responseContent,
'conversationId' => $result['conversation_id'] ?? null,
'model' => $result['model'] ?? null,
'usage' => $result['usage'] ?? null,
'cost' => $result['cost'] ?? null,
'debug_info' => $result['debug_info'] ?? null
];
// محاسبه هزینه در صورت وجود اطلاعات usage
if (isset($result['cost'])) {
$cost = $result['cost'];
// کسر اعتبار از کسب و کار
$totalCost = $cost['total_cost'] ?? 0;
if ($totalCost > 0) {
// گرد کردن هزینه به عدد صحیح
$totalCost = (int) round($totalCost);
$newBalance = (int) ($currentBalance - $totalCost);
$business->setSmsCharge((string) $newBalance);
$entityManager->persist($business);
// ثبت لاگ کسر اعتبار
$log->insert(
'هوش مصنوعی',
"کسر اعتبار برای استفاده از هوش مصنوعی: {$totalCost} ریال (اعتبار قبلی: {$currentBalance} ریال، اعتبار جدید: {$newBalance} ریال)",
$acc['user'],
$business
);
$entityManager->flush();
}
}
return $this->json($response);
} else {
return $this->json([
'success' => false,
'error' => $result['error'] ?? 'خطای نامشخص در سرویس هوش مصنوعی',
'debug_info' => $result['debug_info'] ?? ['fallback' => 'no debug info from service', 'result' => $result]
]);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'error' => 'خطا در پردازش درخواست: ' . $e->getMessage(),
'debug_info' => [
'ai_response' => null,
'tool_commands' => [],
'has_commands' => false,
'error_details' => $e->getMessage()
]
]);
}
}
#[Route('/api/wizard/status', name: 'wizard_status', methods: ['GET'])]
public function wizard_status(): JsonResponse
{
try {
$serviceStatus = $this->agiService->checkAIServiceStatus();
// بررسی وضعیت کامل
$status = 'available';
$message = 'سرویس هوش مصنوعی در دسترس است';
if (!$serviceStatus['isEnabled']) {
$status = 'disabled';
$message = 'سرویس هوش مصنوعی غیرفعال است';
} elseif (!$serviceStatus['hasApiKey']) {
$status = 'no_api_key';
$message = 'کلید API تنظیم نشده است';
}
return $this->json([
'success' => true,
'status' => $status,
'service_name' => $serviceStatus['service'],
'message' => $message,
'connection_status' => $serviceStatus['connection_status'] ?? 'unknown',
'connection_message' => $serviceStatus['connection_message'] ?? ''
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'error' => 'خطا در بررسی وضعیت: ' . $e->getMessage()
]);
}
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace App\Entity;
use App\Repository\AIConversationRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AIConversationRepository::class)]
class AIConversation
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'aiConversations')]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
#[ORM\ManyToOne(inversedBy: 'aiConversations')]
#[ORM\JoinColumn(nullable: false)]
private ?Business $business = null;
#[ORM\Column(length: 255, options: ["charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci"])]
private ?string $title = null;
#[ORM\Column(length: 255, nullable: true, options: ["charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci"])]
private ?string $category = null;
#[ORM\Column]
private ?int $createdAt = null;
#[ORM\Column]
private ?int $updatedAt = null;
#[ORM\Column(nullable: true)]
private ?bool $isActive = true;
#[ORM\Column(nullable: true)]
private ?bool $deleted = false;
#[ORM\OneToMany(mappedBy: 'conversation', targetEntity: AIMessage::class, orphanRemoval: true)]
private Collection $messages;
public function __construct()
{
$this->messages = new ArrayCollection();
$this->createdAt = time();
$this->updatedAt = time();
}
public function getId(): ?int
{
return $this->id;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
public function getBusiness(): ?Business
{
return $this->business;
}
public function setBusiness(?Business $business): static
{
$this->business = $business;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getCategory(): ?string
{
return $this->category;
}
public function setCategory(?string $category): static
{
$this->category = $category;
return $this;
}
public function getCreatedAt(): ?int
{
return $this->createdAt;
}
public function setCreatedAt(int $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?int
{
return $this->updatedAt;
}
public function setUpdatedAt(int $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
public function isActive(): ?bool
{
return $this->isActive;
}
public function setIsActive(?bool $isActive): static
{
$this->isActive = $isActive;
return $this;
}
public function isDeleted(): ?bool
{
return $this->deleted;
}
public function setDeleted(?bool $deleted): static
{
$this->deleted = $deleted;
return $this;
}
/**
* @return Collection<int, AIMessage>
*/
public function getMessages(): Collection
{
return $this->messages;
}
public function addMessage(AIMessage $message): static
{
if (!$this->messages->contains($message)) {
$this->messages->add($message);
$message->setConversation($this);
}
return $this;
}
public function removeMessage(AIMessage $message): static
{
if ($this->messages->removeElement($message)) {
// set the owning side to null (unless already changed)
if ($message->getConversation() === $this) {
$message->setConversation(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,183 @@
<?php
namespace App\Entity;
use App\Repository\AIMessageRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: AIMessageRepository::class)]
class AIMessage
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'messages')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?AIConversation $conversation = null;
#[ORM\Column(length: 20)]
private ?string $role = null; // 'user' یا 'assistant'
#[ORM\Column(type: Types::TEXT, options: ["charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci"])]
private ?string $content = null;
#[ORM\Column]
private ?int $createdAt = null;
#[ORM\Column(nullable: true)]
private ?int $inputTokens = null;
#[ORM\Column(nullable: true)]
private ?int $outputTokens = null;
#[ORM\Column(nullable: true)]
private ?float $inputCost = null;
#[ORM\Column(nullable: true)]
private ?float $outputCost = null;
#[ORM\Column(nullable: true)]
private ?float $totalCost = null;
#[ORM\Column(length: 255, nullable: true, options: ["charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci"])]
private ?string $model = null;
#[ORM\Column(length: 255, nullable: true, options: ["charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci"])]
private ?string $agentSource = null;
public function __construct()
{
$this->createdAt = time();
}
public function getId(): ?int
{
return $this->id;
}
public function getConversation(): ?AIConversation
{
return $this->conversation;
}
public function setConversation(?AIConversation $conversation): static
{
$this->conversation = $conversation;
return $this;
}
public function getRole(): ?string
{
return $this->role;
}
public function setRole(string $role): static
{
$this->role = $role;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): static
{
$this->content = $content;
return $this;
}
public function getCreatedAt(): ?int
{
return $this->createdAt;
}
public function setCreatedAt(int $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getInputTokens(): ?int
{
return $this->inputTokens;
}
public function setInputTokens(?int $inputTokens): static
{
$this->inputTokens = $inputTokens;
return $this;
}
public function getOutputTokens(): ?int
{
return $this->outputTokens;
}
public function setOutputTokens(?int $outputTokens): static
{
$this->outputTokens = $outputTokens;
return $this;
}
public function getInputCost(): ?float
{
return $this->inputCost;
}
public function setInputCost(?float $inputCost): static
{
$this->inputCost = $inputCost;
return $this;
}
public function getOutputCost(): ?float
{
return $this->outputCost;
}
public function setOutputCost(?float $outputCost): static
{
$this->outputCost = $outputCost;
return $this;
}
public function getTotalCost(): ?float
{
return $this->totalCost;
}
public function setTotalCost(?float $totalCost): static
{
$this->totalCost = $totalCost;
return $this;
}
public function getModel(): ?string
{
return $this->model;
}
public function setModel(?string $model): static
{
$this->model = $model;
return $this;
}
public function getAgentSource(): ?string
{
return $this->agentSource;
}
public function setAgentSource(?string $agentSource): static
{
$this->agentSource = $agentSource;
return $this;
}
}

View file

@ -26,6 +26,9 @@ class APIToken
#[ORM\JoinColumn(nullable: false)]
private ?User $submitter = null;
#[ORM\Column(type: 'boolean', nullable: true)]
private ?bool $isForAi = null;
public function getId(): ?int
{
return $this->id;
@ -78,4 +81,16 @@ class APIToken
return $this;
}
public function isForAi(): ?bool
{
return $this->isForAi;
}
public function setIsForAi(?bool $isForAi): static
{
$this->isForAi = $isForAi;
return $this;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Entity;
use App\Repository\AccountToShebaInquiryRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AccountToShebaInquiryRepository::class)]
#[ORM\Table(name: 'account_to_sheba_inquiry')]
class AccountToShebaInquiry
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 50, unique: true)]
private ?string $cacheKey = null;
#[ORM\Column(type: 'json')]
private array $shebaData = [];
#[ORM\Column]
private ?\DateTimeImmutable $updatedAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getCacheKey(): ?string
{
return $this->cacheKey;
}
public function setCacheKey(string $cacheKey): static
{
$this->cacheKey = $cacheKey;
return $this;
}
public function getShebaData(): array
{
return $this->shebaData;
}
public function setShebaData(array $shebaData): static
{
$this->shebaData = $shebaData;
return $this;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeImmutable $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
}

View file

@ -300,6 +300,9 @@ class Business
#[ORM\OneToMany(mappedBy: 'business', targetEntity: PlugHrmDoc::class)]
private Collection $plugHrmDocs;
#[ORM\OneToMany(mappedBy: 'business', targetEntity: AIConversation::class, orphanRemoval: true)]
private Collection $aiConversations;
public function __construct()
{
$this->logs = new ArrayCollection();
@ -343,6 +346,7 @@ class Business
$this->accountingPackageOrders = new ArrayCollection();
$this->PlugGhestaDocs = new ArrayCollection();
$this->plugHrmDocs = new ArrayCollection();
$this->aiConversations = new ArrayCollection();
}
public function getId(): ?int
@ -2086,4 +2090,34 @@ class Business
}
return $this;
}
/**
* @return Collection<int, AIConversation>
*/
public function getAiConversations(): Collection
{
return $this->aiConversations;
}
public function addAiConversation(AIConversation $aiConversation): static
{
if (!$this->aiConversations->contains($aiConversation)) {
$this->aiConversations->add($aiConversation);
$aiConversation->setBusiness($this);
}
return $this;
}
public function removeAiConversation(AIConversation $aiConversation): static
{
if ($this->aiConversations->removeElement($aiConversation)) {
// set the owning side to null (unless already changed)
if ($aiConversation->getBusiness() === $this) {
$aiConversation->setBusiness(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Entity;
use App\Repository\CardToShebaInquiryRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CardToShebaInquiryRepository::class)]
#[ORM\Table(name: 'card_to_sheba_inquiry')]
class CardToShebaInquiry
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 16, unique: true)]
private ?string $cardNumber = null;
#[ORM\Column(type: 'json')]
private array $shebaData = [];
#[ORM\Column]
private ?\DateTimeImmutable $updatedAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getCardNumber(): ?string
{
return $this->cardNumber;
}
public function setCardNumber(string $cardNumber): static
{
$this->cardNumber = $cardNumber;
return $this;
}
public function getShebaData(): array
{
return $this->shebaData;
}
public function setShebaData(array $shebaData): static
{
$this->shebaData = $shebaData;
return $this;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeImmutable $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
}

View file

@ -6,6 +6,7 @@ use App\Repository\InvoiceTypeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: InvoiceTypeRepository::class)]
class InvoiceType
@ -28,12 +29,14 @@ class InvoiceType
private ?string $type = null;
#[ORM\OneToMany(mappedBy: 'InvoiceLabel', targetEntity: HesabdariDoc::class)]
#[Ignore]
private Collection $hesabdariDocs;
/**
* @var Collection<int, PreInvoiceDoc>
*/
#[ORM\OneToMany(mappedBy: 'invoiceLabel', targetEntity: PreInvoiceDoc::class)]
#[Ignore]
private Collection $preInvoiceDocs;
public function __construct()

View file

@ -132,6 +132,12 @@ class Permission
#[ORM\Column(nullable: true)]
private ?bool $plugTaxSettings = null;
#[ORM\Column(nullable: true)]
private ?bool $inquiry = null;
#[ORM\Column(nullable: true)]
private ?bool $ai = null;
public function getId(): ?int
{
return $this->id;
@ -605,4 +611,27 @@ class Permission
return $this;
}
public function isInquiry(): ?bool
{
return $this->inquiry;
}
public function setInquiry(?bool $inquiry): static
{
$this->inquiry = $inquiry;
return $this;
}
public function isAi(): ?bool
{
return $this->ai;
}
public function setAi(?bool $ai): static
{
$this->ai = $ai;
return $this;
}
}

View file

@ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugGhestaDocRepository::class)]
class PlugGhestaDoc
@ -18,9 +19,11 @@ class PlugGhestaDoc
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Business $bid = null;
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
#[Ignore]
private ?User $submitter = null;
#[ORM\Column(length: 25)]
@ -43,16 +46,19 @@ class PlugGhestaDoc
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Person $person = null;
/**
* @var Collection<int, PlugGhestaItem>
*/
#[ORM\OneToMany(targetEntity: PlugGhestaItem::class, mappedBy: 'doc', orphanRemoval: true)]
#[Ignore]
private Collection $plugGhestaItems;
#[ORM\ManyToOne(inversedBy: 'plugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?HesabdariDoc $mainDoc = null;
public function __construct()

View file

@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\PlugGhestaItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugGhestaItemRepository::class)]
class PlugGhestaItem
@ -15,6 +16,7 @@ class PlugGhestaItem
#[ORM\ManyToOne(inversedBy: 'plugGhestaItems')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?PlugGhestaDoc $doc = null;
#[ORM\Column(length: 25)]
@ -27,6 +29,7 @@ class PlugGhestaItem
private ?int $num = null;
#[ORM\ManyToOne(inversedBy: 'plugGhestaItems')]
#[Ignore]
private ?HesabdariDoc $hesabdariDoc = null;
public function getId(): ?int

View file

@ -6,6 +6,7 @@ use App\Repository\PlugHrmDocRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugHrmDocRepository::class)]
class PlugHrmDoc
@ -23,19 +24,23 @@ class PlugHrmDoc
#[ORM\ManyToOne(inversedBy: 'plugHrmDocs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Business $business = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?User $creator = null;
#[ORM\Column]
private ?int $createDate = null;
#[ORM\OneToMany(mappedBy: 'doc', targetEntity: PlugHrmDocItem::class, orphanRemoval: true)]
#[Ignore]
private Collection $items;
#[ORM\ManyToOne(inversedBy: 'plugHrmDocs')]
#[Ignore]
private ?HesabdariDoc $hesabdariDoc = null;
public function __construct()

View file

@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\PlugHrmDocItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugHrmDocItemRepository::class)]
class PlugHrmDocItem
@ -15,10 +16,12 @@ class PlugHrmDocItem
#[ORM\ManyToOne(inversedBy: 'items')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?PlugHrmDoc $doc = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Person $person = null;
#[ORM\Column]

View file

@ -0,0 +1,83 @@
<?php
namespace App\Entity;
use App\Repository\PostalCodeInquiryRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PostalCodeInquiryRepository::class)]
#[ORM\Table(name: 'postal_code_inquiry')]
class PostalCodeInquiry
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 10, unique: true)]
private ?string $postalCode = null;
#[ORM\Column(type: 'json')]
private array $addressData = [];
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column]
private ?\DateTimeImmutable $updatedAt = null;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getPostalCode(): ?string
{
return $this->postalCode;
}
public function setPostalCode(string $postalCode): static
{
$this->postalCode = $postalCode;
return $this;
}
public function getAddressData(): array
{
return $this->addressData;
}
public function setAddressData(array $addressData): static
{
$this->addressData = $addressData;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(\DateTimeImmutable $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
}

View file

@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\PreInvoiceItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PreInvoiceItemRepository::class)]
class PreInvoiceItem
@ -15,6 +16,7 @@ class PreInvoiceItem
#[ORM\ManyToOne(inversedBy: 'preInvoiceItems')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Commodity $commodity = null;
#[ORM\Column(length: 100, nullable: true)]
@ -46,6 +48,7 @@ class PreInvoiceItem
#[ORM\ManyToOne(inversedBy: 'preInvoiceItems')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?PreInvoiceDoc $doc = null;
public function getId(): ?int

View file

@ -19,7 +19,7 @@ class Registry
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(length: 255)]
#[ORM\Column(type: "text", nullable: true)]
private ?string $valueOfKey = null;
public function getId(): ?int

View file

@ -137,6 +137,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'submitter')]
private Collection $PlugGhestaDocs;
#[ORM\OneToMany(mappedBy: 'user', targetEntity: AIConversation::class, orphanRemoval: true)]
private Collection $aiConversations;
public function __construct()
{
$this->userTokens = new ArrayCollection();
@ -162,6 +165,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
$this->accountingPackageOrders = new ArrayCollection();
$this->backBuiltModules = new ArrayCollection();
$this->PlugGhestaDocs = new ArrayCollection();
$this->aiConversations = new ArrayCollection();
}
public function getId(): ?int
@ -997,4 +1001,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
/**
* @return Collection<int, AIConversation>
*/
public function getAiConversations(): Collection
{
return $this->aiConversations;
}
public function addAiConversation(AIConversation $aiConversation): static
{
if (!$this->aiConversations->contains($aiConversation)) {
$this->aiConversations->add($aiConversation);
$aiConversation->setUser($this);
}
return $this;
}
public function removeAiConversation(AIConversation $aiConversation): static
{
if ($this->aiConversations->removeElement($aiConversation)) {
// set the owning side to null (unless already changed)
if ($aiConversation->getUser() === $this) {
$aiConversation->setUser(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace App\Repository;
use App\Entity\AIConversation;
use App\Entity\Business;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AIConversation>
*
* @method AIConversation|null find($id, $lockMode = null, $lockVersion = null)
* @method AIConversation|null findOneBy(array $criteria, array $orderBy = null)
* @method AIConversation[] findAll()
* @method AIConversation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AIConversationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AIConversation::class);
}
public function save(AIConversation $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(AIConversation $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* حذف نرم گفتگو (تنها علامت‌گذاری به عنوان حذف شده)
*/
public function softDelete(AIConversation $entity, bool $flush = false): void
{
$entity->setDeleted(true);
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* دریافت گفتگوهای کاربر در یک کسب و کار خاص
*/
public function findByUserAndBusiness(User $user, Business $business, bool $activeOnly = true): array
{
$criteria = [
'user' => $user,
'business' => $business,
'deleted' => false
];
if ($activeOnly) {
$criteria['isActive'] = true;
}
return $this->findBy($criteria, ['updatedAt' => 'DESC']);
}
/**
* دریافت گفتگوهای فعال کاربر در کسب و کار فعلی
*/
public function findActiveConversations(User $user, Business $business): array
{
return $this->findBy([
'user' => $user,
'business' => $business,
'isActive' => true,
'deleted' => false
], ['updatedAt' => 'DESC']);
}
/**
* جستجو در گفتگوها بر اساس عنوان
*/
public function searchByTitle(User $user, Business $business, string $searchTerm): array
{
$qb = $this->createQueryBuilder('c')
->where('c.user = :user')
->andWhere('c.business = :business')
->andWhere('c.isActive = :active')
->andWhere('c.deleted = :deleted')
->andWhere('c.title LIKE :search')
->setParameter('user', $user)
->setParameter('business', $business)
->setParameter('active', true)
->setParameter('deleted', false)
->setParameter('search', '%' . $searchTerm . '%')
->orderBy('c.updatedAt', 'DESC');
return $qb->getQuery()->getResult();
}
/**
* دریافت گفتگوها بر اساس دسته‌بندی
*/
public function findByCategory(User $user, Business $business, string $category): array
{
return $this->findBy([
'user' => $user,
'business' => $business,
'category' => $category,
'isActive' => true,
'deleted' => false
], ['updatedAt' => 'DESC']);
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace App\Repository;
use App\Entity\AIMessage;
use App\Entity\AIConversation;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AIMessage>
*
* @method AIMessage|null find($id, $lockMode = null, $lockVersion = null)
* @method AIMessage|null findOneBy(array $criteria, array $orderBy = null)
* @method AIMessage[] findAll()
* @method AIMessage[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AIMessageRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AIMessage::class);
}
public function save(AIMessage $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(AIMessage $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* دریافت پیام‌های یک گفتگو
*/
public function findByConversation(AIConversation $conversation): array
{
return $this->findBy(['conversation' => $conversation], ['createdAt' => 'ASC']);
}
/**
* دریافت آخرین پیام یک گفتگو
*/
public function findLastMessageByConversation(AIConversation $conversation): ?AIMessage
{
return $this->findOneBy(['conversation' => $conversation], ['createdAt' => 'DESC']);
}
/**
* محاسبه آمار استفاده برای یک گفتگو
*/
public function getConversationStats(AIConversation $conversation): array
{
$qb = $this->createQueryBuilder('m')
->select('SUM(m.inputTokens) as totalInputTokens')
->addSelect('SUM(m.outputTokens) as totalOutputTokens')
->addSelect('SUM(m.inputCost) as totalInputCost')
->addSelect('SUM(m.outputCost) as totalOutputCost')
->addSelect('SUM(m.totalCost) as totalCost')
->where('m.conversation = :conversation')
->setParameter('conversation', $conversation);
$result = $qb->getQuery()->getSingleResult();
$totalInputTokens = $result['totalInputTokens'] ?? 0;
$totalOutputTokens = $result['totalOutputTokens'] ?? 0;
$totalTokens = $totalInputTokens + $totalOutputTokens;
return [
'totalInputTokens' => $totalInputTokens,
'totalOutputTokens' => $totalOutputTokens,
'totalTokens' => $totalTokens,
'totalInputCost' => $result['totalInputCost'] ?? 0,
'totalOutputCost' => $result['totalOutputCost'] ?? 0,
'totalCost' => $result['totalCost'] ?? 0,
];
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Repository;
use App\Entity\AccountToShebaInquiry;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AccountToShebaInquiry>
*
* @method AccountToShebaInquiry|null find($id, $lockMode = null, $lockVersion = null)
* @method AccountToShebaInquiry|null findOneBy(array $criteria, array $orderBy = null)
* @method AccountToShebaInquiry[] findAll()
* @method AccountToShebaInquiry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AccountToShebaInquiryRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AccountToShebaInquiry::class);
}
public function findByCacheKey(string $cacheKey): ?AccountToShebaInquiry
{
return $this->findOneBy(['cacheKey' => $cacheKey]);
}
public function save(AccountToShebaInquiry $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(AccountToShebaInquiry $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Repository;
use App\Entity\CardToShebaInquiry;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<CardToShebaInquiry>
*
* @method CardToShebaInquiry|null find($id, $lockMode = null, $lockVersion = null)
* @method CardToShebaInquiry|null findOneBy(array $criteria, array $orderBy = null)
* @method CardToShebaInquiry[] findAll()
* @method CardToShebaInquiry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class CardToShebaInquiryRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, CardToShebaInquiry::class);
}
public function findByCardNumber(string $cardNumber): ?CardToShebaInquiry
{
return $this->findOneBy(['cardNumber' => $cardNumber]);
}
public function save(CardToShebaInquiry $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(CardToShebaInquiry $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Repository;
use App\Entity\PostalCodeInquiry;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<PostalCodeInquiry>
*
* @method PostalCodeInquiry|null find($id, $lockMode = null, $lockVersion = null)
* @method PostalCodeInquiry|null findOneBy(array $criteria, array $orderBy = null)
* @method PostalCodeInquiry[] findAll()
* @method PostalCodeInquiry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PostalCodeInquiryRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PostalCodeInquiry::class);
}
public function findByPostalCode(string $postalCode): ?PostalCodeInquiry
{
return $this->findOneBy(['postalCode' => $postalCode]);
}
public function save(PostalCodeInquiry $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(PostalCodeInquiry $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}

View file

@ -0,0 +1,838 @@
<?php
namespace App\Service\AGI;
use App\Entity\Business;
use App\Entity\AIConversation;
use App\Entity\AIMessage;
use App\Service\registryMGR;
use App\Service\Log;
use App\Service\Provider;
use App\Service\AGI\Promps\PromptService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
class AGIService
{
private $em;
private $registryMGR;
private $log;
private $provider;
private $promptService;
private $httpClient;
private $httpKernel;
public function __construct(
EntityManagerInterface $entityManager,
registryMGR $registryMGR,
Log $log,
Provider $provider,
PromptService $promptService,
HttpClientInterface $httpClient,
HttpKernelInterface $httpKernel
) {
$this->em = $entityManager;
$this->registryMGR = $registryMGR;
$this->log = $log;
$this->provider = $provider;
$this->promptService = $promptService;
$this->httpClient = $httpClient;
$this->httpKernel = $httpKernel;
}
/**
* ارسال درخواست به هوش مصنوعی
* @param string $message پیام کاربر
* @param Business|null $business کسب و کار
* @param mixed $user کاربر
* @param int|null $conversationId شناسه گفتگو (اختیاری)
* @param array|null $acc اطلاعات دسترسی
* @return array
*/
public function sendRequest(string $message, ?Business $business = null, $user = null, ?int $conversationId = null, ?array $acc = null): array
{
// بررسی فعال بودن هوش مصنوعی
$status = $this->checkAIServiceStatus();
if (!$status['enabled']) {
return [
'success' => false,
'error' => 'سرویس هوش مصنوعی غیرفعال است.',
'debug_info' => [
'context' => 'sendRequest',
'service_status' => $status,
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
];
}
try {
// مدیریت گفتگو و تاریخچه
$conversation = $this->manageConversation($conversationId, $business, $user, $message);
$conversationHistory = $this->getConversationHistory($conversation);
// ذخیره پیام کاربر
$this->saveUserMessage($conversation, $message);
// ساخت پرامپ هوشمند
$prompt = $this->buildSmartPrompt($message, $business, $conversationHistory);
$service = $this->getAIAgentSource();
$apiKey = $this->getAIApiKey($service);
if (!$apiKey) {
return [
'success' => false,
'error' => 'کلید API برای سرویس هوش مصنوعی تنظیم نشده است.',
'debug_info' => [
'context' => 'sendRequest',
'service' => $service,
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
];
}
// ارسال درخواست با function calling
$result = $this->sendToAIServiceWithFunctionCalling($prompt, $apiKey, $service, $conversationHistory, $acc);
if (!$result['success']) {
return $result;
}
// استخراج پاسخ نهایی
$aiResponse = $this->extractAIResponse($result['data']);
$cost = $this->calculateCostFromResponse($result['data']);
// ذخیره پاسخ هوش مصنوعی
$this->saveAIMessage($conversation, $aiResponse, $result['data'], $cost);
return [
'success' => true,
'response' => $aiResponse,
'conversationId' => $conversation->getId(),
'model' => $this->getAIModel(),
'usage' => $result['data']['usage'] ?? [],
'cost' => $cost,
'debug_info' => [
'context' => 'sendRequest',
'function_calls' => $result['function_calls'] ?? [],
'tool_results' => $result['tool_results'] ?? []
]
];
} catch (\Exception $e) {
$this->log->error('خطا در ارسال درخواست به هوش مصنوعی: ' . $e->getMessage(), [
'context' => 'AGIService::sendRequest',
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'error' => 'خطا در پردازش درخواست: ' . $e->getMessage(),
'debug_info' => [
'context' => 'sendRequest',
'exception' => $e->getMessage(),
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
];
}
}
/**
* ارسال درخواست به سرویس هوش مصنوعی با پشتیبانی از function calling
*/
private function sendToAIServiceWithFunctionCalling(string $prompt, string $apiKey, string $service, array $conversationHistory = [], ?array $acc = null): array
{
$urls = $this->getServiceUrls($service);
$model = $this->getAIModel();
// ساخت messages با تاریخچه
$messages = [];
// اضافه کردن تاریخچه گفتگو
foreach ($conversationHistory as $historyItem) {
$messages[] = [
'role' => $historyItem['role'],
'content' => $historyItem['content']
];
}
// اضافه کردن پیام فعلی
$messages[] = [
'role' => 'user',
'content' => $prompt
];
// تعریف ابزارهای موجود
$tools = $this->buildToolsFromPromptServices();
$data = [
'model' => $model,
'messages' => $messages,
'tools' => $tools,
'tool_choice' => 'auto', // اجازه انتخاب ابزار به مدل
'max_tokens' => 12000,
'temperature' => 0.1
];
$maxIterations = 5; // حداکثر تعداد تکرار برای جلوگیری از حلقه بی‌نهایت
$iteration = 0;
$functionCalls = [];
$toolResults = [];
while ($iteration < $maxIterations) {
$iteration++;
// تلاش برای ارسال به URL های مختلف
$result = null;
foreach ($urls as $url) {
$result = $this->makeHttpRequest($url, $data, $apiKey);
if ($result['success']) {
break;
}
}
if (!$result || !$result['success']) {
return [
'success' => false,
'error' => 'خطا در ارتباط با سرور هوش مصنوعی.',
'debug_info' => [
'context' => 'sendToAIServiceWithFunctionCalling',
'url_list' => $urls,
'data' => $data,
'iteration' => $iteration
]
];
}
$responseData = $result['data'];
$choices = $responseData['choices'] ?? [];
if (empty($choices)) {
return [
'success' => false,
'error' => 'پاسخ نامعتبر از سرور هوش مصنوعی.',
'debug_info' => [
'context' => 'sendToAIServiceWithFunctionCalling',
'response_data' => $responseData,
'iteration' => $iteration
]
];
}
$choice = $choices[0];
$message = $choice['message'] ?? [];
$toolCalls = $message['tool_calls'] ?? [];
// اگر ابزاری فراخوانی نشده، پاسخ نهایی است
if (empty($toolCalls)) {
return [
'success' => true,
'data' => $responseData,
'function_calls' => $functionCalls,
'tool_results' => $toolResults
];
}
// اجرای ابزارهای فراخوانی شده
$toolResults = [];
foreach ($toolCalls as $toolCall) {
$functionCall = $toolCall['function'] ?? [];
$functionName = $functionCall['name'] ?? '';
$functionArgs = json_decode($functionCall['arguments'] ?? '{}', true);
if ($acc && !isset($functionArgs['acc'])) {
$functionArgs['acc'] = $acc;
}
$functionCalls[] = [
'name' => $functionName,
'arguments' => $functionArgs
];
// اجرای ابزار
$toolResult = $this->callTool($functionName, $functionArgs);
$toolResults[] = [
'tool_call_id' => $toolCall['id'] ?? '',
'role' => 'tool',
'content' => json_encode($toolResult, JSON_UNESCAPED_UNICODE)
];
}
// اضافه کردن نتایج ابزارها به messages برای ارسال مجدد
$messages[] = [
'role' => 'assistant',
'content' => null,
'tool_calls' => $toolCalls
];
foreach ($toolResults as $toolResult) {
$messages[] = $toolResult;
}
// به‌روزرسانی data برای ارسال مجدد
$data['messages'] = $messages;
}
// اگر به حداکثر تعداد تکرار رسیدیم
return [
'success' => false,
'error' => 'تعداد تکرار بیش از حد مجاز.',
'debug_info' => [
'context' => 'sendToAIServiceWithFunctionCalling',
'max_iterations' => $maxIterations,
'function_calls' => $functionCalls,
'tool_results' => $toolResults
]
];
}
/**
* اجرای ابزار
*/
private function callTool(string $tool, array $params, array $context = [])
{
try {
switch ($tool) {
case 'getPersonInfo':
// استفاده مستقیم از سرویس Cog\PersonService
return $this->callGetPersonInfoFromCog($params);
default:
return [
'error' => 'ابزار ناشناخته: ' . $tool
];
}
} catch (\Exception $e) {
$this->log->error('خطا در اجرای ابزار: ' . $e->getMessage(), [
'context' => 'AGIService::callTool',
'tool' => $tool,
'params' => $params,
'exception' => $e->getMessage()
]);
return [
'error' => 'خطا در اجرای ابزار: ' . $e->getMessage()
];
}
}
/**
* اجرای ابزار getPersonInfo با استفاده از سرویس Cog\PersonService
*/
private function callGetPersonInfoFromCog(array $params)
{
$code = $params['code'] ?? null;
if (!$code) {
return [
'error' => 'کد شخص الزامی است'
];
}
try {
// دریافت اطلاعات دسترسی (acc) از context یا پارامترها
$acc = $params['acc'] ?? null;
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
// استفاده از سرویس Cog\PersonService
$personService = new \App\Cog\PersonService($this->em, $this->provider->getAccessService());
$result = $personService->getPersonInfo($code, $acc);
return $result;
} catch (\Exception $e) {
$this->log->error('خطا در دریافت اطلاعات شخص از Cog: ' . $e->getMessage(), [
'context' => 'AGIService::callGetPersonInfoFromCog',
'code' => $code,
'exception' => $e->getMessage()
]);
return [
'error' => 'خطا در دریافت اطلاعات شخص: ' . $e->getMessage()
];
}
}
/**
* ساخت پرامپ هوشمند
*/
private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string
{
// دریافت پرامپ‌های پایه از PromptService
$basePrompts = $this->promptService->getAllBasePrompts();
$prompt = $basePrompts;
// قوانین خروجی JSON و مثال‌ها از سرویس مدیریت پرامپت‌ها
$prompt .= $this->promptService->getOutputFormatPrompt();
// اضافه کردن اطلاعات کسب و کار
if ($business) {
$prompt .= "\n\nاطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}.";
}
// اضافه کردن تاریخچه گفتگو
if (!empty($conversationHistory)) {
$prompt .= "\n\n📜 تاریخچه گفتگو:\n";
foreach ($conversationHistory as $historyItem) {
$role = $historyItem['role'] === 'user' ? 'کاربر' : 'دستیار';
$prompt .= "{$role}: {$historyItem['content']}\n";
}
$prompt .= "\n💡 نکته: لطفاً context گفتگو را حفظ کنید و به سوالات قبلی مراجعه کنید.";
}
$prompt .= "\n\nسوال کاربر: " . $message;
return $prompt;
}
/**
* دریافت URL های سرویس
*/
private function getServiceUrls(string $service): array
{
return match ($service) {
'gapgpt' => [
'https://api.gapgpt.ir/v1/chat/completions',
'https://api.gapgpt.app/v1/chat/completions'
],
'avalai' => ['https://api.avalapis.ir/v1/chat/completions'],
'local' => [$this->registryMGR->get('system', 'localModelAddress') ?? ''],
default => []
};
}
/**
* انجام درخواست HTTP
*/
private function makeHttpRequest(string $url, array $data, string $apiKey): array
{
$debugInfo = [
'request_url' => $url,
'request_data' => $data,
];
try {
$response = $this->httpClient->request('POST', $url, [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $apiKey,
],
'json' => $data,
'timeout' => 15,
'max_redirects' => 3,
]);
$statusCode = $response->getStatusCode();
$content = $response->getContent(false); // false: throw exception on 4xx/5xx نمی‌دهد
$debugInfo['http_code'] = $statusCode;
$debugInfo['raw_response'] = $content;
if ($statusCode !== 200) {
$debugInfo['http_error_message'] = $this->getHttpErrorMessage($statusCode);
return [
'success' => false,
'error' => 'خطای HTTP: ' . $this->getHttpErrorMessage($statusCode),
'debug_info' => $debugInfo
];
}
$responseData = json_decode($content, true);
if (!$responseData) {
$debugInfo['json_error'] = json_last_error_msg();
return [
'success' => false,
'error' => 'پاسخ نامعتبر از سرور: ' . substr($content, 0, 200),
'debug_info' => $debugInfo
];
}
return [
'success' => true,
'data' => $responseData,
'debug_info' => $debugInfo
];
} catch (\Throwable $e) {
$debugInfo['exception'] = $e->getMessage();
$debugInfo['exception_code'] = $e->getCode();
$debugInfo['exception_trace'] = $e->getTraceAsString();
return [
'success' => false,
'error' => 'خطا در ارتباط با سرور: ' . $e->getMessage(),
'debug_info' => $debugInfo
];
}
}
/**
* دریافت پیام خطای HTTP
*/
private function getHttpErrorMessage(int $httpCode): string
{
$messages = [
400 => 'درخواست نامعتبر',
401 => 'عدم احراز هویت',
403 => 'دسترسی ممنوع',
404 => 'منبع یافت نشد',
429 => 'تعداد درخواست‌ها بیش از حد مجاز',
500 => 'خطای داخلی سرور',
502 => 'خطای دروازه',
503 => 'سرویس در دسترس نیست',
504 => 'خطای timeout دروازه'
];
return $messages[$httpCode] ?? 'خطای نامشخص';
}
/**
* استخراج پاسخ از ساختارهای مختلف سرویس‌ها
*/
private function extractAIResponse(array $responseData): ?string
{
if (isset($responseData['choices'][0]['message']['content'])) {
return $responseData['choices'][0]['message']['content'];
}
if (isset($responseData['response'])) {
return $responseData['response'];
}
if (isset($responseData['content'])) {
return $responseData['content'];
}
return null;
}
/**
* محاسبه هزینه استفاده
*/
private function calculateCostFromResponse(array $responseData): array
{
$usage = $responseData['usage'] ?? [];
return $this->calculateCost($usage);
}
/**
* محاسبه هزینه بر اساس usage
*/
public function calculateCost(array $usage): array
{
$promptTokens = $usage['prompt_tokens'] ?? 0;
$completionTokens = $usage['completion_tokens'] ?? 0;
$inputCost = $promptTokens * $this->getInputTokenPrice();
$outputCost = $completionTokens * $this->getOutputTokenPrice();
return [
'input_cost' => round($inputCost, 4),
'output_cost' => round($outputCost, 4),
'total_cost' => round($inputCost + $outputCost, 4)
];
}
/**
* بررسی وضعیت سرویس هوش مصنوعی
*/
public function checkAIServiceStatus(): array
{
$enabled = $this->registryMGR->get('system', 'aiEnabled') ?? false;
$service = $this->registryMGR->get('system', 'aiAgentSource') ?? 'gapgpt';
$apiKey = $this->getAIApiKey($service);
return [
'isEnabled' => $enabled && $apiKey,
'enabled' => $enabled && $apiKey,
'service' => $this->getServiceDisplayName($service),
'hasApiKey' => !empty($apiKey),
'message' => $enabled && $apiKey ? 'سرویس فعال است' : 'سرویس غیرفعال است'
];
}
/**
* دریافت نام نمایشی سرویس
*/
public function getServiceDisplayName(string $service): string
{
$displayNames = [
'gapgpt' => 'گپ‌جی‌پی‌تی',
'avalai' => 'اول‌آی',
'local' => 'سرویس محلی'
];
return $displayNames[$service] ?? 'سرویس نامشخص';
}
/**
* دریافت کلید API برای سرویس مشخص
*/
private function getAIApiKey(string $service): ?string
{
return match ($service) {
'gapgpt', 'avalai' => $this->registryMGR->get('system', 'aiApiKey') ?? null,
'local' => $this->registryMGR->get('system', 'localModelAddress') ?? null,
default => null
};
}
/**
* دریافت مدل هوش مصنوعی
*/
public function getAIModel(): string
{
return $this->registryMGR->get('system', 'aiModel') ?? 'gpt-4o-2024-11-20';
}
/**
* دریافت منبع عامل هوش مصنوعی
*/
public function getAIAgentSource(): string
{
return $this->registryMGR->get('system', 'aiAgentSource') ?? 'gapgpt';
}
/**
* دریافت قیمت توکن ورودی
*/
public function getInputTokenPrice(): float
{
return (float) ($this->registryMGR->get('system', 'inputTokenPrice') ?? 0.00001);
}
/**
* دریافت قیمت توکن خروجی
*/
public function getOutputTokenPrice(): float
{
return (float) ($this->registryMGR->get('system', 'outputTokenPrice') ?? 0.00003);
}
/**
* بررسی فعال بودن هوش مصنوعی
*/
public function isAIEnabled(): bool
{
return (bool) ($this->registryMGR->get('system', 'aiEnabled') ?? false);
}
/**
* دریافت پرامپ هوش مصنوعی
*/
public function getAIPrompt(): string
{
return $this->registryMGR->get('system', 'aiPrompt') ?? 'شما یک دستیار هوشمند برای سیستم حسابداری هستید.';
}
/**
* مدیریت گفتگو - ایجاد یا بازیابی گفتگوی موجود
*/
private function manageConversation(?int $conversationId, ?Business $business, $user, string $message): AIConversation
{
if ($conversationId) {
// بازیابی گفتگوی موجود
$conversation = $this->em->getRepository(AIConversation::class)->find($conversationId);
if ($conversation && $conversation->getUser() === $user && $conversation->getBusiness() === $business) {
// به‌روزرسانی زمان آخرین تغییر
$conversation->setUpdatedAt(time());
$this->em->persist($conversation);
return $conversation;
}
}
// ایجاد گفتگوی جدید
$conversation = new AIConversation();
$conversation->setUser($user);
$conversation->setBusiness($business);
$conversation->setTitle($this->generateConversationTitle($message));
$conversation->setCategory('عمومی');
$conversation->setCreatedAt(time());
$conversation->setUpdatedAt(time());
$conversation->setIsActive(true);
$conversation->setDeleted(false);
$this->em->persist($conversation);
return $conversation;
}
/**
* دریافت تاریخچه گفتگو از دیتابیس
*/
private function getConversationHistory(AIConversation $conversation): array
{
$messages = $this->em->getRepository(AIMessage::class)->findBy(
['conversation' => $conversation],
['createdAt' => 'ASC']
);
$history = [];
foreach ($messages as $message) {
$history[] = [
'role' => $message->getRole(),
'content' => $message->getContent()
];
}
return $history;
}
/**
* ذخیره پیام کاربر
*/
private function saveUserMessage(AIConversation $conversation, string $message): void
{
$userMessage = new AIMessage();
$userMessage->setConversation($conversation);
$userMessage->setRole('user');
$userMessage->setContent($message);
$userMessage->setModel($this->getAIModel());
$userMessage->setAgentSource($this->getAIAgentSource());
$userMessage->setCreatedAt(time());
$this->em->persist($userMessage);
$this->em->flush();
}
/**
* ذخیره پاسخ هوش مصنوعی
*/
private function saveAIMessage(AIConversation $conversation, string $aiResponse, array $responseData, array $cost): void
{
$aiMessage = new AIMessage();
$aiMessage->setConversation($conversation);
$aiMessage->setRole('assistant');
$aiMessage->setContent($aiResponse);
$aiMessage->setModel($this->getAIModel());
$aiMessage->setAgentSource($this->getAIAgentSource());
$aiMessage->setCreatedAt(time());
// ذخیره اطلاعات usage
if (isset($responseData['usage'])) {
$aiMessage->setInputTokens($responseData['usage']['prompt_tokens'] ?? null);
$aiMessage->setOutputTokens($responseData['usage']['completion_tokens'] ?? null);
}
// ذخیره هزینه‌ها
$aiMessage->setInputCost($cost['input_cost'] ?? null);
$aiMessage->setOutputCost($cost['output_cost'] ?? null);
$aiMessage->setTotalCost($cost['total_cost'] ?? null);
$this->em->persist($aiMessage);
// به‌روزرسانی زمان آخرین تغییر گفتگو
$conversation->setUpdatedAt(time());
$this->em->persist($conversation);
$this->em->flush();
}
/**
* تولید عنوان گفتگو بر اساس پیام اول
*/
private function generateConversationTitle(string $message): string
{
// حذف کاراکترهای اضافی و محدود کردن طول
$title = trim($message);
$title = preg_replace('/\s+/', ' ', $title); // حذف فاصله‌های اضافی
// محدود کردن طول عنوان
if (mb_strlen($title, 'UTF-8') > 50) {
$title = mb_substr($title, 0, 47, 'UTF-8') . '...';
}
return $title ?: 'گفتگوی جدید';
}
/**
* دریافت گفتگوهای کاربر
*/
public function getUserConversations($user, ?Business $business, int $limit = 20): array
{
$conversations = $this->em->getRepository(AIConversation::class)->findBy(
[
'user' => $user,
'business' => $business,
'isActive' => true,
'deleted' => false
],
['updatedAt' => 'DESC'],
$limit
);
$result = [];
foreach ($conversations as $conversation) {
$messageCount = count($conversation->getMessages());
$lastMessage = $this->em->getRepository(AIMessage::class)
->findLastMessageByConversation($conversation);
$result[] = [
'id' => $conversation->getId(),
'title' => $conversation->getTitle(),
'category' => $conversation->getCategory(),
'createdAt' => $conversation->getCreatedAt(),
'updatedAt' => $conversation->getUpdatedAt(),
'messageCount' => $messageCount,
'lastMessage' => $lastMessage ? $lastMessage->getContent() : ''
];
}
return $result;
}
/**
* دریافت پیام‌های یک گفتگو
*/
public function getConversationMessages(int $conversationId, $user, ?Business $business): array
{
$conversation = $this->em->getRepository(AIConversation::class)->find($conversationId);
if (!$conversation || $conversation->getUser() !== $user || $conversation->getBusiness() !== $business) {
return [];
}
$messages = $this->em->getRepository(AIMessage::class)->findBy(
['conversation' => $conversation],
['createdAt' => 'ASC']
);
$result = [];
foreach ($messages as $message) {
$result[] = [
'id' => $message->getId(),
'role' => $message->getRole(),
'content' => $message->getContent(),
'createdAt' => $message->getCreatedAt(),
'inputTokens' => $message->getInputTokens(),
'outputTokens' => $message->getOutputTokens(),
'inputCost' => $message->getInputCost(),
'outputCost' => $message->getOutputCost(),
'totalCost' => $message->getTotalCost(),
'model' => $message->getModel(),
'agentSource' => $message->getAgentSource()
];
}
return $result;
}
/**
* ساخت ابزارها از سرویس‌های پرامپ
*/
private function buildToolsFromPromptServices(): array
{
return $this->promptService->getAllTools();
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class BankPromptService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* دریافت تمام ابزارهای بخش بانک‌ها برای function calling
* @return array
*/
public function getTools(): array
{
$tools = [];
// ابزار getBankAccountInfo
$bankAccountInfoPrompt = $this->getBankAccountInfoPrompt();
$bankAccountInfoData = json_decode($bankAccountInfoPrompt, true);
if ($bankAccountInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $bankAccountInfoData['tool'],
'description' => $bankAccountInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $bankAccountInfoData['input'],
'required' => array_keys($bankAccountInfoData['input'])
]
]
];
}
return $tools;
}
/**
* تولید تمام پرامپ‌های بخش بانک‌ها
* @return string
*/
public function getAllBankPrompts(): string
{
$prompts = [];
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getBankAccountInfoPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد
// $prompts[] = $this->getCreateBankAccountPrompt();
// $prompts[] = $this->getUpdateBankAccountPrompt();
// $prompts[] = $this->getBankTransactionPrompt();
// ترکیب تمام پرامپ‌ها
return implode("\n\n", $prompts);
}
/**
* پرامپ برای دریافت اطلاعات کامل حساب بانکی
* @return string
*/
public function getBankAccountInfoPrompt(): string
{
return '{
"tool": "getBankAccountInfo",
"description": "Get complete bank account information by code",
"endpoint": "/api/bank/account/info/{code}",
"method": "GET",
"input": {
"code": "string - Bank account code (e.g., 1001, 1002)"
},
"output": {
"id": "integer - Account ID",
"code": "string - Account code",
"name": "string - Account name",
"bankName": "string - Bank name",
"accountNumber": "string - Account number",
"shabaNumber": "string - Shaba number",
"cardNumber": "string - Card number",
"balance": "float - Current balance",
"currency": "string - Currency type",
"isActive": "boolean - Account active status",
"description": "string - Account description"
},
"examples": {
"input": {"code": "1001"},
"output": {
"id": 23,
"code": "1001",
"name": "حساب جاری اصلی",
"bankName": "ملت",
"accountNumber": "1234567890",
"shabaNumber": "IR123456789012345678901234",
"cardNumber": "6104337812345678",
"balance": 150000000,
"currency": "ریال",
"isActive": true,
"description": "حساب جاری اصلی شرکت"
}
}
}';
}
}

View file

@ -0,0 +1,150 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\Access;
use App\Entity\APIToken;
class BasePromptService
{
private $em;
private $access;
public function __construct(EntityManagerInterface $entityManager, Access $access)
{
$this->em = $entityManager;
$this->access = $access;
}
/**
* پرامپ پایه برای معرفی سیستم
* @return string
*/
public function getSystemIntroductionPrompt(): string
{
// دسترسی فعلی
$acc = $this->access->hasRole('join');
$apiToken = null;
if ($acc && isset($acc['bid']) && isset($acc['user'])) {
// جستجوی توکن AI معتبر برای این کاربر و کسب‌وکار
$now = time();
$apiToken = $this->em->getRepository(APIToken::class)->findOneBy([
'bid' => $acc['bid'],
'submitter' => $acc['user'],
'isForAi' => true
]);
if ($apiToken) {
$expire = $apiToken->getDateExpire();
if ($expire && $expire != '0' && $now > (int)$expire) {
$apiToken = null; // منقضی شده
}
}
if (!$apiToken) {
// ساخت توکن جدید با اعتبار ۳۰ دقیقه
$apiToken = $this->access->createAiToken($acc['bid'], $acc['user'], 1800);
}
}
$apiKey = $apiToken ? $apiToken->getToken() : '';
return '{
"tool": "system_introduction",
"description": "System introduction and authentication requirements",
"content": "You are an AI assistant for Hesabix accounting system. This system manages businesses, persons, accounting entries, inventory, and financial reports. You can help users with various tasks using the available tools and APIs.",
"capabilities": [
"Person management (customers, suppliers, employees)",
],
"authentication": {
"method": "API Key or Session Token",
"required_headers": {
"api-key": "' . $apiKey . ' (این کد را در هدر api-key قرار بده)",
},
},
"language": "Persian (فارسی)",
"currency": "Iranian Rial (ریال)"
}';
}
/**
* پرامپ پایه برای خطاها
* @return string
*/
public function getErrorHandlingPrompt(): string
{
return '{
"tool": "error_handling",
"description": "Error handling and authentication guidance",
"instructions": "When encountering errors, provide clear explanations in Persian and suggest solutions. Focus on authentication and access control issues.",
"error_types": {
"access_denied": "دسترسی غیرمجاز - کاربر فاقد مجوز لازم است",
"ai_permission_denied": "دسترسی هوش مصنوعی غیرمجاز - کاربر فاقد مجوز AI است",
"invalid_api_key": "کلید API نامعتبر - لطفاً کلید صحیح را وارد کنید",
"expired_token": "توکن منقضی شده - لطفاً توکن جدید دریافت کنید",
"invalid_business": "کسب و کار نامعتبر - شناسه کسب و کار صحیح نیست",
"invalid_year": "سال مالی نامعتبر - سال مالی انتخاب شده صحیح نیست",
"invalid_currency": "واحد پول نامعتبر - واحد پول انتخاب شده صحیح نیست",
"not_found": "مورد یافت نشد - کد یا شناسه وارد شده صحیح نیست",
"validation_error": "خطای اعتبارسنجی - اطلاعات وارد شده صحیح نیست",
"network_error": "خطای شبکه - اتصال اینترنت را بررسی کنید"
},
"authentication_solutions": {
"missing_api_key": "کلید API را در هدر api-key قرار دهید",
"expired_token": "توکن جدید از مدیر سیستم دریافت کنید",
"no_ai_permission": "مجوز هوش مصنوعی از مدیر کسب و کار دریافت کنید",
"wrong_business": "شناسه کسب و کار صحیح را در هدر activeBid قرار دهید",
"wrong_year": "سال مالی صحیح را در هدر activeYear قرار دهید"
},
"response_format": "Explain error in Persian, provide specific solution, and suggest next steps"
}';
}
/**
* پرامپ پایه برای راهنمایی کاربر
* @return string
*/
public function getHelpPrompt(): string
{
return '{
"tool": "help",
"description": "User help and guidance",
"instructions": "Provide helpful guidance to users about available features and how to use them. Be concise and clear in Persian.",
"common_queries": {
"person_info": "برای دریافت اطلاعات شخص، کد شخص را وارد کنید",
},
"response_format": "Provide step-by-step guidance in Persian with examples"
}';
}
/**
* پرامپ برای نمایش دامنه اصلی API
* @return string
*/
public function getApiBaseUrlPrompt(): string
{
// دریافت اولین رکورد تنظیمات
$settings = $this->em->getRepository(\App\Entity\Settings::class)->findAll();
$appSite = isset($settings[0]) ? $settings[0]->getAppSite() : '';
$domain = $appSite ? $appSite : '---';
return '{
"tool": "api_base_url",
"description": "آدرس پایه API",
"content": "تمام اندپوینت‌های سیستم از طریق دامنه زیر قابل دسترسی هستند:",
"base_url": "' . $domain . '"
}';
}
/**
* دریافت تمام پرامپ‌های پایه
* @return string
*/
public function getAllBasePrompts(): string
{
$prompts = [];
$prompts[] = $this->getSystemIntroductionPrompt();
$prompts[] = $this->getErrorHandlingPrompt();
$prompts[] = $this->getHelpPrompt();
$prompts[] = $this->getApiBaseUrlPrompt();
return implode("\n\n", $prompts);
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class InventoryPromptService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* دریافت تمام ابزارهای بخش کالاها برای function calling
* @return array
*/
public function getTools(): array
{
$tools = [];
// ابزار getItemInfo
$itemInfoPrompt = $this->getItemInfoPrompt();
$itemInfoData = json_decode($itemInfoPrompt, true);
if ($itemInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $itemInfoData['tool'],
'description' => $itemInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $itemInfoData['input'],
'required' => array_keys($itemInfoData['input'])
]
]
];
}
return $tools;
}
/**
* تولید تمام پرامپ‌های بخش کالاها
* @return string
*/
public function getAllInventoryPrompts(): string
{
$prompts = [];
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getItemInfoPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد
// $prompts[] = $this->getCreateItemPrompt();
// $prompts[] = $this->getUpdateItemPrompt();
// $prompts[] = $this->getSearchItemPrompt();
// $prompts[] = $this->getItemStockPrompt();
// ترکیب تمام پرامپ‌ها
return implode("\n\n", $prompts);
}
/**
* پرامپ برای دریافت اطلاعات کامل کالا
* @return string
*/
public function getItemInfoPrompt(): string
{
return '{
"tool": "getItemInfo",
"description": "Get complete item information by code",
"endpoint": "/api/item/info/{code}",
"method": "GET",
"input": {
"code": "string - Item code (e.g., 1001, 1002)"
},
"output": {
"id": "integer - Item ID",
"code": "string - Item code",
"name": "string - Item name",
"description": "string - Item description",
"category": "string - Item category",
"unit": "string - Unit of measurement",
"price": "float - Item price",
"stock": "float - Current stock quantity",
"minStock": "float - Minimum stock level",
"maxStock": "float - Maximum stock level",
"supplier": "string - Supplier name",
"barcode": "string - Barcode",
"isActive": "boolean - Item active status"
},
"examples": {
"input": {"code": "1001"},
"output": {
"id": 45,
"code": "1001",
"name": "لپ‌تاپ اپل",
"description": "لپ‌تاپ اپل مک‌بوک پرو 13 اینچ",
"category": "الکترونیک",
"unit": "عدد",
"price": 45000000,
"stock": 15,
"minStock": 5,
"maxStock": 50,
"supplier": "شرکت اپل",
"barcode": "1234567890123",
"isActive": true
}
}
}';
}
}

View file

@ -0,0 +1,185 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class PersonPromptService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* دریافت تمام ابزارهای بخش اشخاص برای function calling
* @return array
*/
public function getTools(): array
{
$tools = [];
// ابزار getPersonInfo
$personInfoPrompt = $this->getPersonInfoPrompt();
$personInfoData = json_decode($personInfoPrompt, true);
if ($personInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $personInfoData['tool'],
'description' => $personInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $personInfoData['input'],
'required' => array_keys($personInfoData['input'])
]
]
];
}
return $tools;
}
/**
* تولید تمام پرامپ‌های بخش اشخاص
* @return string
*/
public function getAllPersonPrompts(): string
{
$prompts = [];
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getPersonInfoPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد
// $prompts[] = $this->getCreatePersonPrompt();
// $prompts[] = $this->getUpdatePersonPrompt();
// $prompts[] = $this->getSearchPersonPrompt();
// $prompts[] = $this->getDeletePersonPrompt();
// ترکیب تمام پرامپ‌ها
return implode("\n\n", $prompts);
}
/**
* پرامپ برای دریافت اطلاعات کامل شخص
* @return string
*/
public function getPersonInfoPrompt(): string
{
return '{
"tool": "getPersonInfo",
"description": "Get complete person information by code",
"endpoint": "/api/person/info/{code}",
"method": "GET",
"input": {
"code": "string - Person code (e.g., 1001, 1002)"
},
"output": {
"id": "integer - Person ID",
"code": "string - Person code",
"nikename": "string - Person nickname",
"name": "string - Person name",
"tel": "string - Telephone number",
"mobile": "string - Mobile number",
"mobile2": "string|null - Secondary mobile",
"des": "string - Description",
"company": "string - Company name",
"shenasemeli": "string - National ID",
"sabt": "string - Registration number",
"shahr": "string - City",
"keshvar": "string - Country",
"ostan": "string - Province",
"postalcode": "string - Postal code",
"codeeghtesadi": "string - Economic code",
"email": "string - Email address",
"website": "string - Website",
"fax": "string - Fax number",
"birthday": "string|null - Birthday",
"speedAccess": "boolean - Quick access flag",
"address": "string - Address",
"prelabel": "string|null - Pre label",
"accounts": "array - Bank accounts information",
"types": "array - Person types with checked status",
"bs": "float - Credit balance (بستانکار)",
"bd": "float - Debit balance (بدهکار)",
"balance": "float - Net balance (bs - bd)"
},
"examples": {
"input": {"code": "1001"},
"output": {
"id": 65,
"code": "1000",
"nikename": "مشتری ۱",
"name": "",
"tel": "",
"mobile": "09183282405",
"mobile2": null,
"des": "",
"company": "",
"shenasemeli": "0025",
"sabt": "",
"shahr": "",
"keshvar": "",
"ostan": "",
"postalcode": "",
"codeeghtesadi": "",
"email": "",
"website": "",
"fax": "",
"birthday": null,
"speedAccess": true,
"address": "",
"prelabel": null,
"accounts": [
{
"bank": "حساب ۲",
"shabaNum": "شماره شبا",
"cardNum": "شماره کارت",
"accountNum": "شماره حساب"
}
],
"types": [
{
"label": "مشتری",
"code": "customer",
"checked": true
},
{
"label": "بازاریاب",
"code": "marketer",
"checked": true
},
{
"label": "کارمند",
"code": "emplyee",
"checked": false
},
{
"label": "تامین‌کننده",
"code": "supplier",
"checked": false
},
{
"label": "همکار",
"code": "colleague",
"checked": false
},
{
"label": "فروشنده",
"code": "salesman",
"checked": false
}
],
"bs": 752593632.55,
"bd": 283007419,
"balance": 469586213.54999995
}
}
}';
}
}

View file

@ -0,0 +1,167 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\AGI\Promps\InventoryPromptService;
use App\Service\AGI\Promps\BankPromptService;
class PromptService
{
private $em;
private $personPromptService;
private $basePromptService;
private $inventoryPromptService;
private $bankPromptService;
public function __construct(
EntityManagerInterface $entityManager,
PersonPromptService $personPromptService,
BasePromptService $basePromptService,
InventoryPromptService $inventoryPromptService,
BankPromptService $bankPromptService
) {
$this->em = $entityManager;
$this->personPromptService = $personPromptService;
$this->basePromptService = $basePromptService;
$this->inventoryPromptService = $inventoryPromptService;
$this->bankPromptService = $bankPromptService;
}
/**
* دریافت تمام ابزارهای موجود برای function calling
* @return array
*/
public function getAllTools(): array
{
$tools = [];
// ابزارهای بخش اشخاص
$personTools = $this->personPromptService->getTools();
$tools = array_merge($tools, $personTools);
// ابزارهای بخش کالاها
$inventoryTools = $this->inventoryPromptService->getTools();
$tools = array_merge($tools, $inventoryTools);
// ابزارهای بخش بانک‌ها
$bankTools = $this->bankPromptService->getTools();
$tools = array_merge($tools, $bankTools);
// در آینده ابزارهای بخش‌های دیگر اضافه خواهند شد
// $accountingTools = $this->accountingPromptService->getTools();
// $tools = array_merge($tools, $accountingTools);
return $tools;
}
/**
* دریافت تمام پرامپ‌های پایه (سیستم، API، و غیره)
* @return string
*/
public function getAllBasePrompts(): string
{
return $this->basePromptService->getAllBasePrompts();
}
/**
* دریافت پرامپ سیستمی بر اساس کلید
* @param string $key کلید پرامپ
* @return string|null
*/
public function getSystemPrompt(string $key): ?string
{
switch ($key) {
case 'person':
return $this->personPromptService->getAllPersonPrompts();
// در آینده موارد بیشتر اضافه خواهند شد
// case 'accounting':
// return $this->accountingPromptService->getAllAccountingPrompts();
// case 'inventory':
// return $this->inventoryPromptService->getAllInventoryPrompts();
// case 'reports':
// return $this->reportsPromptService->getAllReportsPrompts();
// case 'bank':
// return $this->bankPromptService->getAllBankPrompts();
default:
return null;
}
}
/**
* دریافت تمام پرامپ‌های سیستمی
* @return array
*/
public function getAllSystemPrompts(): array
{
$prompts = [];
// پرامپ‌های پایه (سیستم، API، و غیره)
$prompts['base'] = $this->basePromptService->getAllBasePrompts();
// پرامپ‌های بخش اشخاص
$prompts['person'] = $this->personPromptService->getAllPersonPrompts();
// پرامپ‌های بخش کالاها
$prompts['inventory'] = $this->inventoryPromptService->getAllInventoryPrompts();
// پرامپ‌های بخش بانک‌ها
$prompts['bank'] = $this->bankPromptService->getAllBankPrompts();
// در آینده بخش‌های دیگر اضافه خواهند شد
// $prompts['accounting'] = $this->accountingPromptService->getAllAccountingPrompts();
// $prompts['reports'] = $this->reportsPromptService->getAllReportsPrompts();
// $prompts['sales'] = $this->salesPromptService->getAllSalesPrompts();
// $prompts['purchases'] = $this->purchasesPromptService->getAllPurchasesPrompts();
// $prompts['bank'] = $this->bankPromptService->getAllBankPrompts();
return $prompts;
}
/**
* دریافت تمام پرامپ‌ها به صورت یک رشته واحد
* @return string
*/
public function getAllPromptsAsString(): string
{
$allPrompts = $this->getAllSystemPrompts();
return implode("\n\n", $allPrompts);
}
/**
* قوانین خروجی JSON و مثال برای پاسخ هوش مصنوعی
* @return string
*/
public function getOutputFormatPrompt(): string
{
$prompt = "\n\nقوانین مهم: همیشه پاسخ را فقط در قالب یک شیء JSON با ساختار زیر ارسال کن و هیچ توضیح اضافه‌ای ننویس:\n";
$prompt .= <<<EOT
{
"type": ["text", "table", "chart"],
"data": [
{ "type": "text", "content": "..." },
{ "type": "table", "headers": [...], "rows": [[...], ...] },
{ "type": "chart", "chartType": "bar|line|pie", "title": "عنوان نمودار", "labels": ["برچسب۱", "برچسب۲"], "series": [{"name": "عنوان سری", "data": [100, 200]}], "values": [100, 200] }
]
}
EOT;
$prompt .= "\nدر صورت نیاز می‌توانی چند نوع داده را همزمان ارسال کنی. اگر فقط متن است، فقط یک آبجکت با type=text و content بده.\n\nمثال خروجی صحیح برای نمودار (دقیقاً همین ساختار را رعایت کن و کلید type را فقط یک بار و مقدار آن را 'chart' قرار بده. نوع نمودار فقط در chartType باشد):\n";
$prompt .= <<<EOT2
{
"type": ["chart"],
"data": [
{
"type": "chart",
"chartType": "bar",
"title": "نمودار فروش ماهانه",
"labels": ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد"],
"series": [{"name": "فروش ماهانه", "data": [120, 150, 180, 200, 170]}],
"values": [120, 150, 180, 200, 170]
}
]
}
EOT2;
$prompt .= "\nحتماً کلیدهای type، title، labels، series، values و chartType را دقیقاً مطابق مثال بالا برای هر نمودار برگردان و فقط همین JSON را خروجی بده. کلید type را فقط یک بار و مقدار آن را 'chart' قرار بده و نوع نمودار را فقط در chartType بنویس.";
return $prompt;
}
}

View file

@ -6,6 +6,7 @@ use App\Entity\APIToken;
use App\Entity\Business;
use App\Entity\Money;
use App\Entity\Permission;
use App\Entity\User;
use App\Entity\UserToken;
use App\Entity\Year;
use Symfony\Component\Security\Core\User\UserInterface;
@ -54,6 +55,17 @@ class Access
'token'=>$this->request->headers->get('api-key')
]);
if(!$token) { return false; }
// بررسی تاریخ انقضا
$dateExpire = $token->getDateExpire();
if($dateExpire && $dateExpire != '0') {
$expireTimestamp = (int)$dateExpire;
$currentTimestamp = time();
if($currentTimestamp > $expireTimestamp) {
return false; // توکن منقضی شده
}
}
$bid = $token->getBid();
}
if ($this->request->headers->get('activeYear')) {
@ -96,8 +108,19 @@ class Access
'money'=>$money
];
// بررسی دسترسی هوش مصنوعی
$aiPermission = $this->em->getRepository(Permission::class)->findOneBy([
'bid'=>$bid,
'user'=>$this->user
]);
$accessArray['ai'] = false;
if($aiPermission && $aiPermission->isAi()){
$accessArray['ai'] = true;
}
if($bid->getOwner()->getEmail() === $this->user->getUserIdentifier()){
//user is owner
//user is owner - همه دسترسی‌ها را دارد
$accessArray['ai'] = true;
return $accessArray;
}
elseif ($this->user && $roll == 'join' && count($this->em->getRepository(Permission::class)->findBy(['user'=>$this->user,'bid'=>$bid]))){
@ -114,4 +137,36 @@ class Access
}
return false;
}
/**
* ایجاد توکن API برای هوش مصنوعی
* @param Business $business کسب و کار
* @param User $user کاربر ایجاد کننده
* @param int|null $expireSeconds زمان انقضا به ثانیه (null برای همیشه معتبر)
* @return APIToken
*/
public function createAiToken(Business $business, User $user, ?int $expireSeconds = null): APIToken
{
$token = new APIToken();
$token->setBid($business);
$token->setSubmitter($user);
$token->setIsForAi(true);
// ایجاد توکن تصادفی
$randomToken = bin2hex(random_bytes(32));
$token->setToken($randomToken);
// تنظیم تاریخ انقضا
if ($expireSeconds !== null) {
$expireTimestamp = time() + $expireSeconds;
$token->setDateExpire((string)$expireTimestamp);
} else {
$token->setDateExpire('0'); // همیشه معتبر
}
$this->em->persist($token);
$this->em->flush();
return $token;
}
}

View file

@ -0,0 +1,448 @@
<?php
namespace App\Service;
use App\Entity\PostalCodeInquiry;
use App\Entity\CardToShebaInquiry;
use App\Entity\AccountToShebaInquiry;
use Doctrine\ORM\EntityManagerInterface;
class Inquiry
{
public function __construct(private EntityManagerInterface $entityManager)
{
}
public function postalCodeToAddress($postalCode)
{
// ابتدا بررسی دیتابیس
$existingInquiry = $this->entityManager->getRepository(PostalCodeInquiry::class)->findByPostalCode($postalCode);
if ($existingInquiry) {
// اگر در دیتابیس موجود است، از آن استفاده کن
$addressData = $existingInquiry->getAddressData();
return [
'result' => 1,
'response_body' => [
'data' => [
'address' => $addressData
],
'message' => 'موفق (از کش)',
'error_code' => null
]
];
}
// اگر در دیتابیس موجود نیست، از API استفاده کن
$registryMGR = new RegistryMGR($this->entityManager);
$inquiryPanel = $registryMGR->get('system', key: 'inquiryPanel');
if($inquiryPanel == 'zohal'){
$inquiryZohalAPIKey = $registryMGR->get('system', key: 'inquiryZohalAPIKey');
// بررسی وجود API Key
if (empty($inquiryZohalAPIKey)) {
return [
'result' => 0,
'message' => 'API Key تنظیم نشده است',
'error_code' => 'API_KEY_MISSING'
];
}
// استفاده از API جدید زحل
$url = "https://service.zohal.io/api/v0/services/inquiry/postal_code_inquiry";
// آماده‌سازی داده‌های JSON
$postData = json_encode([
'postal_code' => $postalCode
]);
// تنظیمات cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData),
'Authorization: Bearer ' . $inquiryZohalAPIKey,
'X-API-Key: ' . $inquiryZohalAPIKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// بررسی خطای cURL
if ($curlError) {
return [
'result' => 0,
'message' => 'خطا در ارتباط با سرور: ' . $curlError,
'error_code' => 'CURL_ERROR'
];
}
if ($httpCode === 200) {
$data = json_decode($response, true);
// بررسی کدهای result
if (isset($data['result'])) {
switch ($data['result']) {
case 1:
// موفق - ذخیره در دیتابیس
if (isset($data['response_body']['data']['address'])) {
$this->saveToDatabase($postalCode, $data['response_body']['data']['address']);
}
return $data;
case 4:
return [
'result' => 4,
'message' => 'توکن غیر فعال شده است',
'error_code' => 'TOKEN_INACTIVE'
];
case 5:
return [
'result' => 5,
'message' => 'سرویس در دسترسی نمی‌باشد',
'error_code' => 'SERVICE_UNAVAILABLE'
];
case 6:
return [
'result' => 6,
'message' => 'فراخوانی وب‌سرویس با پارامترهای ورودی صحیح نمی‌باشد',
'error_code' => 'INVALID_PARAMETERS'
];
default:
return [
'result' => $data['result'],
'message' => $data['message'] ?? 'خطای نامشخص',
'error_code' => $data['error_code'] ?? 'UNKNOWN_ERROR'
];
}
}
return $data;
} else {
return [
'result' => 0,
'message' => 'خطا در ارتباط با سرویس استعلام کد پستی (کد خطا: ' . $httpCode . ')',
'error_code' => 'HTTP_ERROR_' . $httpCode
];
}
}
return [
'result' => 0,
'message' => 'سرویس استعلام کد پستی فعال نیست',
'error_code' => 'SERVICE_NOT_ACTIVE'
];
}
public function cardToSheba($cardNumber)
{
// بررسی دیتابیس برای کش
$existingInquiry = $this->entityManager->getRepository(CardToShebaInquiry::class)->findByCardNumber($cardNumber);
if ($existingInquiry) {
// اگر در دیتابیس موجود است، از آن استفاده کن
$shebaData = $existingInquiry->getShebaData();
return [
'result' => 1,
'response_body' => [
'data' => $shebaData,
'message' => 'موفق (از حافظه موقت)',
'error_code' => null
]
];
}
// اگر در دیتابیس موجود نیست، از API استفاده کن
$registryMGR = new RegistryMGR($this->entityManager);
$inquiryPanel = $registryMGR->get('system', key: 'inquiryPanel');
if($inquiryPanel == 'zohal'){
$inquiryZohalAPIKey = $registryMGR->get('system', key: 'inquiryZohalAPIKey');
// بررسی وجود API Key
if (empty($inquiryZohalAPIKey)) {
return [
'result' => 0,
'message' => 'API Key تنظیم نشده است',
'error_code' => 'API_KEY_MISSING'
];
}
// استفاده از API زحل برای تبدیل کارت به شبا
$url = "https://service.zohal.io/api/v0/services/inquiry/card_to_iban";
// آماده‌سازی داده‌های JSON
$postData = json_encode([
'card_number' => $cardNumber
]);
// تنظیمات cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData),
'Authorization: Bearer ' . $inquiryZohalAPIKey,
'X-API-Key: ' . $inquiryZohalAPIKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// بررسی خطای cURL
if ($curlError) {
return [
'result' => 0,
'message' => 'خطا در ارتباط با سرور: ' . $curlError,
'error_code' => 'CURL_ERROR'
];
}
if ($httpCode === 200) {
$data = json_decode($response, true);
// بررسی کدهای result
if (isset($data['result'])) {
switch ($data['result']) {
case 1:
// موفق - ذخیره در دیتابیس
if (isset($data['response_body']['data'])) {
$this->saveCardToShebaToDatabase($cardNumber, $data['response_body']['data']);
}
return $data;
case 4:
return [
'result' => 4,
'message' => 'توکن غیر فعال شده است',
'error_code' => 'TOKEN_INACTIVE'
];
case 5:
return [
'result' => 5,
'message' => 'سرویس در دسترسی نمی‌باشد',
'error_code' => 'SERVICE_UNAVAILABLE'
];
case 6:
return [
'result' => 6,
'message' => 'فراخوانی وب‌سرویس با پارامترهای ورودی صحیح نمی‌باشد',
'error_code' => 'INVALID_PARAMETERS'
];
default:
return [
'result' => $data['result'],
'message' => $data['message'] ?? 'خطای نامشخص',
'error_code' => $data['error_code'] ?? 'UNKNOWN_ERROR'
];
}
}
return $data;
} else {
return [
'result' => 0,
'message' => 'خطا در ارتباط با سرویس تبدیل کارت به شبا (کد خطا: ' . $httpCode . ')',
'error_code' => 'HTTP_ERROR_' . $httpCode
];
}
}
return [
'result' => 0,
'message' => 'سرویس تبدیل کارت به شبا فعال نیست',
'error_code' => 'SERVICE_NOT_ACTIVE'
];
}
private function saveToDatabase(string $postalCode, array $addressData): void
{
try {
$inquiry = new PostalCodeInquiry();
$inquiry->setPostalCode($postalCode);
$inquiry->setAddressData($addressData);
$inquiry->setUpdatedAt(new \DateTimeImmutable());
$this->entityManager->persist($inquiry);
$this->entityManager->flush();
// ثبت لاگ ذخیره موفق در دیتابیس
error_log("کد پستی {$postalCode} با موفقیت در کش ذخیره شد");
} catch (\Exception $e) {
// در صورت خطا در ذخیره، فقط لاگ کن و ادامه بده
error_log('خطا در ذخیره استعلام کد پستی: ' . $e->getMessage());
}
}
public function accountToSheba($bankCode, $accountNumber)
{
// بررسی دیتابیس برای کش
$cacheKey = $bankCode . '_' . $accountNumber;
$existingInquiry = $this->entityManager->getRepository(AccountToShebaInquiry::class)->findByCacheKey($cacheKey);
if ($existingInquiry) {
// اگر در دیتابیس موجود است، از آن استفاده کن
$shebaData = $existingInquiry->getShebaData();
return [
'result' => 1,
'response_body' => [
'data' => $shebaData,
'message' => 'موفق (از حافظه موقت)',
'error_code' => null
]
];
}
// اگر در دیتابیس موجود نیست، از API استفاده کن
$registryMGR = new RegistryMGR($this->entityManager);
$inquiryPanel = $registryMGR->get('system', key: 'inquiryPanel');
if($inquiryPanel == 'zohal'){
$inquiryZohalAPIKey = $registryMGR->get('system', key: 'inquiryZohalAPIKey');
// بررسی وجود API Key
if (empty($inquiryZohalAPIKey)) {
return [
'result' => 0,
'message' => 'API Key تنظیم نشده است',
'error_code' => 'API_KEY_MISSING'
];
}
// استفاده از API زحل برای تبدیل حساب به شبا
$url = "https://service.zohal.io/api/v0/services/inquiry/account_to_iban";
// آماده‌سازی داده‌های JSON
$postData = json_encode([
'bank_code' => $bankCode,
'bank_account' => $accountNumber
]);
// تنظیمات cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData),
'Authorization: Bearer ' . $inquiryZohalAPIKey,
'X-API-Key: ' . $inquiryZohalAPIKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// بررسی خطای cURL
if ($curlError) {
return [
'result' => 0,
'message' => 'خطا در ارتباط با سرور: ' . $curlError,
'error_code' => 'CURL_ERROR'
];
}
if ($httpCode === 200) {
$data = json_decode($response, true);
// بررسی کدهای result
if (isset($data['result'])) {
switch ($data['result']) {
case 1:
// موفق - ذخیره در دیتابیس
if (isset($data['response_body']['data'])) {
$this->saveAccountToShebaToDatabase($cacheKey, $data['response_body']['data']);
}
return $data;
case 4:
return [
'result' => 4,
'message' => 'توکن غیر فعال شده است',
'error_code' => 'TOKEN_INACTIVE'
];
case 5:
return [
'result' => 5,
'message' => 'سرویس در دسترسی نمی‌باشد',
'error_code' => 'SERVICE_UNAVAILABLE'
];
case 6:
return [
'result' => 6,
'message' => 'فراخوانی وب‌سرویس با پارامترهای ورودی صحیح نمی‌باشد',
'error_code' => 'INVALID_PARAMETERS'
];
default:
return [
'result' => $data['result'],
'message' => $data['message'] ?? 'خطای نامشخص',
'error_code' => $data['error_code'] ?? 'UNKNOWN_ERROR'
];
}
}
return $data;
} else {
return [
'result' => 0,
'message' => 'خطا در ارتباط با سرویس تبدیل حساب به شبا (کد خطا: ' . $httpCode . ')',
'error_code' => 'HTTP_ERROR_' . $httpCode
];
}
}
return [
'result' => 0,
'message' => 'سرویس تبدیل حساب به شبا فعال نیست',
'error_code' => 'SERVICE_NOT_ACTIVE'
];
}
private function saveCardToShebaToDatabase(string $cardNumber, array $shebaData): void
{
try {
$inquiry = new CardToShebaInquiry();
$inquiry->setCardNumber($cardNumber);
$inquiry->setShebaData($shebaData);
$inquiry->setUpdatedAt(new \DateTimeImmutable());
$this->entityManager->persist($inquiry);
$this->entityManager->flush();
// ثبت لاگ ذخیره موفق در دیتابیس
error_log("شماره کارت {$cardNumber} با موفقیت در حافظه موقت ذخیره شد");
} catch (\Exception $e) {
// در صورت خطا در ذخیره، فقط لاگ کن و ادامه بده
error_log('خطا در ذخیره استعلام کارت به شبا: ' . $e->getMessage());
}
}
private function saveAccountToShebaToDatabase(string $cacheKey, array $shebaData): void
{
try {
$inquiry = new AccountToShebaInquiry();
$inquiry->setCacheKey($cacheKey);
$inquiry->setShebaData($shebaData);
$inquiry->setUpdatedAt(new \DateTimeImmutable());
$this->entityManager->persist($inquiry);
$this->entityManager->flush();
// ثبت لاگ ذخیره موفق در دیتابیس
error_log("حساب با کلید {$cacheKey} با موفقیت در حافظه موقت ذخیره شد");
} catch (\Exception $e) {
// در صورت خطا در ذخیره، فقط لاگ کن و ادامه بده
error_log('خطا در ذخیره استعلام حساب به شبا: ' . $e->getMessage());
}
}
}

View file

@ -24,6 +24,13 @@ class registryMGR
}
$item->setRoot($root);
$item->setName($key);
// محدود کردن طول داده برای جلوگیری از خطای value_of_key
$maxLength = 65535; // حداکثر طول برای TEXT در MySQL
if (strlen($v) > $maxLength) {
$v = substr($v, 0, $maxLength - 100) . '... [برش داده شده]';
}
$item->setValueOfKey($v);
$this->em->persist($item);
$this->em->flush();
@ -36,12 +43,7 @@ class registryMGR
'name'=>$key
]);
if(! $item){
$item = new Registry();
$item->setRoot($root);
$item->setName($key);
$item->setValueOfKey('');
$this->em->persist($item);
$this->em->flush();
return null; // برگرداندن null به جای ایجاد آیتم جدید
}
return $item->getValueOfKey();
}

View file

@ -791,11 +791,11 @@ install_software() {
# Check if remote origin exists
if ! git remote get-url origin >/dev/null 2>&1; then
# Add remote repository if it doesn't exist
git remote add origin https://github.com/morrning/hesabixCore.git || \
git remote add origin https://source.hesabix.ir/morrning/hesabixCore.git || \
handle_error "Failed to add remote repository"
else
# Update remote URL if it exists
git remote set-url origin https://github.com/morrning/hesabixCore.git || \
git remote set-url origin https://source.hesabix.ir/morrning/hesabixCore.git || \
handle_error "Failed to update remote repository"
fi

View file

@ -17,6 +17,7 @@
"@tiptap/extension-text-align": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@tiptap/vue-3": "^2.11.7",
"@types/dompurify": "^3.0.5",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^13.1.0",
@ -27,12 +28,14 @@
"axios": "^1.8.4",
"date-fns": "^4.1.0",
"date-fns-jalali": "^3.2.0-0",
"dompurify": "^3.2.6",
"downloadjs": "^1.4.7",
"file-saver": "^2.0.5",
"html5-qrcode": "^2.3.8",
"jalali-moment": "^3.3.11",
"libphonenumber-js": "^1.12.7",
"lodash": "^4.17.21",
"marked": "^16.1.0",
"maska": "^3.1.1",
"maz-ui": "^3.50.1",
"pinia": "^3.0.2",

View file

@ -0,0 +1,322 @@
<template>
<div class="person-info">
<v-card v-if="person" class="person-card" elevation="2">
<v-card-title class="d-flex align-center">
<v-avatar color="primary" size="40" class="mr-3">
<v-icon color="white">mdi-account</v-icon>
</v-avatar>
<div>
<div class="text-h6">{{ person.nikename }}</div>
<div class="text-caption text-medium-emphasis">{{ person.code }}</div>
</div>
<v-spacer></v-spacer>
<v-chip
:color="getBalanceColor(person.balance.balance_status)"
size="small"
variant="flat"
>
{{ person.balance.balance_status }}
</v-chip>
</v-card-title>
<v-card-text>
<!-- اطلاعات اصلی -->
<div class="info-section mb-4">
<h4 class="text-subtitle-1 font-weight-medium mb-2">اطلاعات اصلی</h4>
<v-row>
<v-col cols="12" md="6">
<div class="info-item">
<span class="info-label">نام کامل:</span>
<span class="info-value">{{ person.name || 'نامشخص' }}</span>
</div>
<div class="info-item" v-if="person.company">
<span class="info-label">شرکت:</span>
<span class="info-value">{{ person.company }}</span>
</div>
<div class="info-item" v-if="person.mobile">
<span class="info-label">موبایل:</span>
<span class="info-value">{{ person.mobile }}</span>
</div>
<div class="info-item" v-if="person.tel">
<span class="info-label">تلفن:</span>
<span class="info-value">{{ person.tel }}</span>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="info-item" v-if="person.email">
<span class="info-label">ایمیل:</span>
<span class="info-value">{{ person.email }}</span>
</div>
<div class="info-item" v-if="person.address">
<span class="info-label">آدرس:</span>
<span class="info-value">{{ person.address }}</span>
</div>
<div class="info-item" v-if="person.shahr">
<span class="info-label">شهر:</span>
<span class="info-value">{{ person.shahr }}</span>
</div>
<div class="info-item" v-if="person.ostan">
<span class="info-label">استان:</span>
<span class="info-value">{{ person.ostan }}</span>
</div>
</v-col>
</v-row>
</div>
<!-- موجودی مالی -->
<div class="info-section mb-4">
<h4 class="text-subtitle-1 font-weight-medium mb-2">موجودی مالی</h4>
<v-row>
<v-col cols="12" md="4">
<v-card variant="outlined" class="balance-card">
<v-card-text class="text-center">
<div class="text-caption text-medium-emphasis">بستانکار</div>
<div class="text-h6 text-success">{{ formatNumber(person.balance.total_bs) }} ریال</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card variant="outlined" class="balance-card">
<v-card-text class="text-center">
<div class="text-caption text-medium-emphasis">بدهکار</div>
<div class="text-h6 text-error">{{ formatNumber(person.balance.total_bd) }} ریال</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card variant="outlined" class="balance-card">
<v-card-text class="text-center">
<div class="text-caption text-medium-emphasis">موجودی</div>
<div class="text-h6" :class="getBalanceTextColor(person.balance.balance_status)">
{{ formatNumber(person.balance.balance) }} ریال
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<div class="text-caption text-medium-emphasis mt-2">
تعداد تراکنشها: {{ person.balance.transaction_count }}
</div>
</div>
<!-- نوع اشخاص -->
<div class="info-section mb-4" v-if="person.types && person.types.length > 0">
<h4 class="text-subtitle-1 font-weight-medium mb-2">نوع اشخاص</h4>
<div class="d-flex flex-wrap gap-1">
<v-chip
v-for="type in person.types"
:key="type.id"
size="small"
variant="tonal"
color="primary"
>
{{ type.name }}
</v-chip>
</div>
</div>
<!-- کارتهای بانکی -->
<div class="info-section mb-4" v-if="person.cards && person.cards.length > 0">
<h4 class="text-subtitle-1 font-weight-medium mb-2">کارتهای بانکی</h4>
<v-row>
<v-col cols="12" md="6" v-for="card in person.cards" :key="card.bank">
<v-card variant="outlined" class="card-item">
<v-card-text>
<div class="d-flex align-center mb-2">
<v-icon color="primary" class="mr-2">mdi-credit-card</v-icon>
<span class="font-weight-medium">{{ card.bank }}</span>
</div>
<div class="text-caption" v-if="card.card_number">
شماره کارت: {{ maskCardNumber(card.card_number) }}
</div>
<div class="text-caption" v-if="card.account_number">
شماره حساب: {{ card.account_number }}
</div>
<div class="text-caption" v-if="card.shaba_number">
شماره شبا: {{ card.shaba_number }}
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
<!-- تراکنشهای اخیر -->
<div class="info-section" v-if="transactions.length > 0">
<h4 class="text-subtitle-1 font-weight-medium mb-2">تراکنشهای اخیر</h4>
<v-table density="compact">
<thead>
<tr>
<th>تاریخ</th>
<th>نوع</th>
<th>بستانکار</th>
<th>بدهکار</th>
<th>توضیحات</th>
</tr>
</thead>
<tbody>
<tr v-for="transaction in transactions" :key="transaction.id">
<td>{{ formatDate(transaction.doc_date) }}</td>
<td>
<v-chip size="x-small" variant="tonal" color="primary">
{{ transaction.doc_type }}
</v-chip>
</td>
<td class="text-success" v-if="transaction.bs > 0">
{{ formatNumber(transaction.bs) }}
</td>
<td v-else>-</td>
<td class="text-error" v-if="transaction.bd > 0">
{{ formatNumber(transaction.bd) }}
</td>
<td v-else>-</td>
<td>{{ transaction.description || transaction.doc_description }}</td>
</tr>
</tbody>
</v-table>
</div>
</v-card-text>
</v-card>
<v-card v-else class="no-data-card" elevation="1">
<v-card-text class="text-center">
<v-icon size="48" color="grey" class="mb-3">mdi-account-off</v-icon>
<div class="text-h6 text-grey">اطلاعات شخص یافت نشد</div>
<div class="text-caption text-grey">لطفاً نام یا کد شخص را بررسی کنید</div>
</v-card-text>
</v-card>
</div>
</template>
<script>
export default {
name: 'PersonInfo',
props: {
person: {
type: Object,
default: null
},
transactions: {
type: Array,
default: () => []
}
},
methods: {
formatNumber(number) {
return new Intl.NumberFormat('fa-IR').format(number);
},
formatDate(dateString) {
if (!dateString) return '-';
// تبدیل تاریخ شمسی به میلادی و نمایش
return dateString;
},
maskCardNumber(cardNumber) {
if (!cardNumber) return '-';
return cardNumber.replace(/(\d{4})(\d{4})(\d{4})(\d{4})/, '$1-$2-$3-$4');
},
getBalanceColor(status) {
switch (status) {
case 'بستانکار':
return 'success';
case 'بدهکار':
return 'error';
default:
return 'grey';
}
},
getBalanceTextColor(status) {
switch (status) {
case 'بستانکار':
return 'text-success';
case 'بدهکار':
return 'text-error';
default:
return 'text-grey';
}
}
}
};
</script>
<style scoped>
.person-info {
max-width: 100%;
}
.person-card {
border-radius: 12px;
}
.info-section {
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
padding-bottom: 16px;
}
.info-section:last-child {
border-bottom: none;
padding-bottom: 0;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-weight: 500;
color: rgba(0, 0, 0, 0.7);
min-width: 80px;
}
.info-value {
color: rgba(0, 0, 0, 0.9);
text-align: left;
}
.balance-card {
border-radius: 8px;
transition: all 0.3s ease;
}
.balance-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-item {
border-radius: 8px;
transition: all 0.3s ease;
}
.card-item:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.no-data-card {
border-radius: 12px;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.gap-1 {
gap: 4px;
}
.gap-2 {
gap: 8px;
}
</style>

View file

@ -0,0 +1,489 @@
<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>
</div>
</v-card-title>
<v-card-text class="pa-4">
<div class="chart-container">
<apexchart
v-if="chartSeries && chartSeries.length > 0 && chartOptions && chartOptions.xaxis && Array.isArray(chartOptions.xaxis.categories)"
ref="chart"
:type="chartType"
:height="chartHeight"
:options="chartOptions"
:series="chartSeries"
/>
<div v-else class="text-center pa-4" style="color: #888;">دادهای برای نمایش نمودار وجود ندارد.</div>
</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 {
chartId: '',
createdAt: new Date(),
chartType: 'bar',
chartTitle: 'نمودار',
chartOptions: {},
chartSeries: []
};
},
computed: {
chartHeight() {
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) {
console.debug('AIChart.vue watch chartData', newData);
this.initializeChart(newData);
},
immediate: true,
deep: true
}
},
mounted() {
console.debug('AIChart.vue mounted', this.chartData);
this.initializeChart(this.chartData);
},
methods: {
initializeChart(data) {
console.debug('AIChart.vue initializeChart data:', data);
if (!data) return;
this.chartType = data.chartType || '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 typeof value === 'number' ? value.toLocaleString('fa-IR') : value;
}
}
},
responsive: [
{
breakpoint: 480,
options: {
chart: { width: '100%' },
legend: { position: 'bottom' }
}
}
]
};
// تنظیمات خاص بر اساس نوع نمودار
this.setupChartSpecificOptions(data);
// تنظیم سریهای داده
this.setupChartSeries(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 typeof value === 'number' ? value.toLocaleString('fa-IR') : 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 typeof value === 'number' ? value.toLocaleString('fa-IR') : 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 (!data) {
this.chartSeries = [];
// مقداردهی پیشفرض به xaxis برای جلوگیری از خطا
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = { categories: [] };
return;
}
if (this.chartType === 'pie' || this.chartType === 'doughnut') {
if (data.values && Array.isArray(data.values)) {
this.chartSeries = data.values;
} else if (
data.series &&
Array.isArray(data.series) &&
data.series.length > 0 &&
Array.isArray(data.series[0].data)
) {
this.chartSeries = data.series[0].data;
} else {
this.chartSeries = [];
}
} else {
if (data.series && Array.isArray(data.series) && data.series.length > 0) {
this.chartSeries = data.series;
// مقداردهی categories اگر وجود دارد
if (data.labels && Array.isArray(data.labels)) {
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = this.chartOptions.xaxis || {};
this.chartOptions.xaxis.categories = data.labels;
}
} else if (data.labels && data.values && Array.isArray(data.labels) && Array.isArray(data.values)) {
this.chartSeries = [
{
name: this.chartTitle || 'داده‌ها',
data: data.values
}
];
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = this.chartOptions.xaxis || {};
this.chartOptions.xaxis.categories = data.labels;
} else {
this.chartSeries = [
{
name: 'داده‌ها',
data: []
}
];
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = { categories: [] };
}
}
},
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.dataURI) {
this.$refs.chart.dataURI().then(({ imgURI }) => {
const link = document.createElement('a');
link.href = imgURI;
link.download = 'chart.png';
link.click();
});
}
},
refreshChart() {
if (this.$refs.chart && this.$refs.chart.updateSeries) {
// داده فعلی را دوباره ست میکنیم تا رفرش شود
this.$refs.chart.updateSeries(this.chartSeries);
}
}
}
};
</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 !important;
height: 100vh !important;
z-index: 9999;
background: white;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container.fullscreen .apexcharts-canvas {
width: 100% !important;
height: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
}
.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>

View file

@ -0,0 +1,188 @@
<script>
import axios from "axios";
export default {
name: "PostalCode2Address",
props: {
postalCode: {
type: String,
default: ''
}
},
data() {
return {
dialog: false,
settings: {
inquiryPanelEnable: false,
enablePostalCodeToAddress: false,
postalCodeToAddressFee: 0
},
loading: false,
result: null,
error: null
};
},
computed: {
shouldShow() {
// اگر تنظیمات هنوز لود نشده، نمایش بده
if (!this.settings.inquiryPanelEnable && !this.settings.enablePostalCodeToAddress) {
return true;
}
return (this.settings.inquiryPanelEnable === '1' || this.settings.inquiryPanelEnable === true) &&
(this.settings.enablePostalCodeToAddress === '1' || this.settings.enablePostalCodeToAddress === true);
}
},
mounted() {
this.loadSettings();
},
methods: {
async loadSettings() {
try {
console.log('در حال دریافت تنظیمات...');
this.loading = true;
const response = await axios.get('/api/plugins/inquiry/settings/get');
console.log('تنظیمات دریافت شد:', response.data);
this.settings = response.data;
console.log('shouldShow:', this.shouldShow);
} catch (error) {
console.error('خطا در دریافت تنظیمات:', error);
} finally {
this.loading = false;
}
},
openDialog() {
this.dialog = true;
},
closeDialog() {
this.dialog = false;
},
async submitPostalCode() {
this.loading = true;
this.result = null;
this.error = null;
try {
console.log('ارسال درخواست برای کد پستی:', this.postalCode);
const response = await axios.post('/api/plugins/inquiry/postalcode-to-address', {
postal_code: this.postalCode
});
console.log('پاسخ دریافتی:', response.data);
if (response.data && response.data.success) {
this.result = response.data.data;
// ارسال دادهها به کامپوننت والد
const addressDataWithPostalCode = {
...response.data.data,
postalCode: this.postalCode
};
this.$emit('address-found', addressDataWithPostalCode);
// نمایش پیام مناسب بر اساس منبع داده
const message = response.data.from_cache
? 'اطلاعات آدرس از کش دریافت شد (بدون کسر کارمزد)'
: 'اطلاعات آدرس با موفقیت دریافت شد';
// بستن دیالوگ بعد از موفقیت
this.dialog = false;
// نمایش پیام موفقیت
this.$nextTick(() => {
this.showSnackbar(message, 'success');
});
} else {
this.error = (response.data && response.data.message) || "خطا در دریافت اطلاعات";
// مدیریت خطاهای خاص
if (response.data && response.data.error_code === 'HTTP_ERROR_504') {
this.error = "سرویس استعلام کد پستی در دسترس نیست. لطفاً بعداً تلاش کنید.";
}
}
} catch (e) {
console.error('خطا در ارسال درخواست:', e);
this.error = "خطا در ارتباط با سرور";
} finally {
this.loading = false;
}
},
showSnackbar(text, color = 'success', timeout = 3000) {
// ارسال event به کامپوننت والد برای نمایش snackbar
this.$emit('show-snackbar', { text, color, timeout });
}
},
};
</script>
<template>
<div v-if="shouldShow">
<v-tooltip text="تبدیل کد پستی به آدرس" location="top">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" size="x-small" color="primary" variant="text" @click="openDialog" class="px-1">
<v-icon>mdi-map-marker</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-dialog v-model="dialog" max-width="500px">
<v-card>
<v-toolbar color="primary" dark>
<v-toolbar-title>تبدیل کد پستی به آدرس</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="closeDialog">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text class="pt-4">
<div class="mb-4">
<p class="text-body-2">
با استفاده از این سرویس میتوانید کد پستی را به آدرس کامل تبدیل کنید.
</p>
</div>
<v-alert type="info" variant="tonal" class="mb-4">
<template v-slot:prepend>
<v-icon>mdi-information</v-icon>
</template>
<div>
<strong>کارمزد سرویس:</strong> {{ settings.postalCodeToAddressFee.toLocaleString() }} ریال
<br>
<small class="text-caption">
💡 اگر این کد پستی قبلاً استعلام شده باشد، کارمزدی کسر نمیشود
</small>
</div>
</v-alert>
<v-text-field
:model-value="postalCode"
label="کد پستی"
placeholder="مثال: 1234567890"
prepend-inner-icon="mdi-mailbox"
hide-details
class="mb-4"
readonly
></v-text-field>
<v-btn
color="primary"
block
:loading="loading"
:disabled="!postalCode || postalCode.length < 10"
@click="submitPostalCode"
>
تبدیل به آدرس
</v-btn>
<!-- نمایش خطا -->
<v-alert
v-if="error"
type="error"
variant="tonal"
class="mt-4"
border="start"
>
{{ error }}
</v-alert>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>

View file

@ -86,6 +86,7 @@ const en_lang = {
cheque_output: "Cheque Output",
presells: "Presells",
presell_view: "View Presell",
inquiry: "Inquiries",
hrm: 'HR & Payroll',
hrm_docs: 'Payroll Document',
},

View file

@ -194,6 +194,7 @@ const fa_lang = {
plugins_invoices: "صورت حساب‌ها",
repservice: "مدیریت تعمیرگاه",
repservice_reqs: "درخواست‌ها",
inquiry: "استعلامات",
hrm: 'منابع انسانی',
hrm_docs: 'سند حقوق',
buysellByPerson: "گزارش خرید و فروش های اشخاص",
@ -202,7 +203,7 @@ const fa_lang = {
},
time: {
month: "{id} ماه",
},
},
calendar: {
shamsi: "هجری شمسی",
gregorian: "میلادی",
@ -922,5 +923,15 @@ const fa_lang = {
numberinput: {
invalid_number: "فقط عدد انگلیسی مجاز است"
},
chart: {
download: "دانلود نمودار",
refresh: "نوسازی نمودار",
fullscreen: "تمام‌صفحه",
details: "جزئیات نمودار",
type: "نوع نمودار",
id: "شناسه نمودار",
dataPoints: "تعداد نقاط داده",
created: "تاریخ ایجاد"
},
};
export default fa_lang

View file

@ -1012,6 +1012,12 @@ const router = createRouter({
component: () =>
import('../views/acc/plugins/hrm/docs/view.vue'),
},
{
path: 'inquiry/panel',
name: 'inquiry_panel',
component: () =>
import('../views/acc/inquiry/panel.vue'),
},
],
},
{

View file

@ -0,0 +1,33 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useNavigationStore = defineStore('navigation', () => {
const drawer = ref(true)
const previousDrawerState = ref(true)
const closeDrawer = () => {
previousDrawerState.value = drawer.value
drawer.value = false
}
const openDrawer = () => {
drawer.value = previousDrawerState.value
}
const toggleDrawer = () => {
drawer.value = !drawer.value
}
const setDrawer = (value: boolean) => {
drawer.value = value
}
return {
drawer,
previousDrawerState,
closeDrawer,
openDrawer,
toggleDrawer,
setDrawer
}
})

View file

@ -4,6 +4,7 @@ import axios from "axios";
import Swal from "sweetalert2";
import { getApiUrl, getBasePath, getSiteName, getSiteSlogan } from "@/hesabixConfig";
import { ref } from 'vue';
import { useNavigationStore } from '@/stores/navigationStore';
import Profile_btn from '@/components/application/buttons/profile_btn.vue';
import Notifications_btn from '@/components/application/buttons/notifications_btn.vue';
import Year_cob from '@/components/application/combobox/year_cob.vue';
@ -15,7 +16,7 @@ import ShortcutsButton from '@/components/application/buttons/ShortcutsButton.vu
export default {
data() {
return {
drawer: ref(null),
navigationStore: useNavigationStore(),
plugins: [],
business: { id: '', name: '' },
timeNow: '',
@ -35,6 +36,16 @@ export default {
canFreeAccounting: true,
};
},
computed: {
drawer: {
get() {
return this.navigationStore.drawer;
},
set(value) {
this.navigationStore.setDrawer(value);
}
}
},
mounted() {
axios.post('/api/plugin/get/actives').then((response) => {
this.plugins = response.data;
@ -169,8 +180,10 @@ export default {
{ path: '/acc/business/extramoneys', key: '-', label: this.$t('drawer.extra_moneys'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('accpro') },
{ path: '/acc/business/logs', key: '=', label: this.$t('drawer.history'), ctrl: true, shift: true, permission: () => this.permissions.log },
{ path: '/acc/plugin/repservice/order/list', key: '[', label: this.$t('drawer.repservice_reqs'), ctrl: true, shift: true, permission: () => this.permissions.plugRepservice && this.isPluginActive('repservice') },
{ path: '/acc/sms/panel', key: ']', label: this.$t('drawer.sms_panel'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/inquiry/panel', key: ']', label: this.$t('drawer.inquiry'), ctrl: true, shift: true, permission: () => true },
{ path: '/acc/wizard/home', key: 'A', label: 'هوش مصنوعی', ctrl: true, shift: true, permission: () => this.permissions.ai },
{ path: '/acc/printers/list', key: ';', label: this.$t('drawer.cloud_printers'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/sms/panel', key: '`', label: this.$t('drawer.sms_panel'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/archive/list', key: '\'', label: this.$t('drawer.archive_files'), ctrl: true, shift: true, permission: () => this.permissions.archiveUpload || this.permissions.archiveMod || this.permissions.archiveDelete },
{ path: '/acc/archive/order/new', key: ',', label: this.$t('drawer.archive_order'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/archive/order/list', key: '.', label: this.$t('drawer.archive_log'), ctrl: true, shift: true, permission: () => this.permissions.owner },
@ -790,6 +803,20 @@ export default {
</template>
</v-list-item>
</v-list-group>
<v-list-item to="/acc/inquiry/panel">
<template v-slot:prepend><v-icon icon="mdi-magnify"></v-icon></template>
<v-list-item-title>
{{ $t('drawer.inquiry') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/inquiry/panel') }}</span>
</v-list-item-title>
</v-list-item>
<v-list-item v-if="permissions.ai" to="/acc/wizard/home">
<template v-slot:prepend><v-icon icon="mdi-robot"></v-icon></template>
<v-list-item-title>
هوش مصنوعی
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/wizard/home') }}</span>
</v-list-item-title>
</v-list-item>
<v-list-group v-show="isPluginActive('hrm') && permissions.plugHrmDocs">
<template v-slot:activator="{ props }">
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.hrm')">
@ -944,7 +971,7 @@ export default {
<span class="d-none d-sm-flex">{{ business.name }}</span>
</v-app-bar-title>
<v-spacer></v-spacer>
<v-tooltip text="جادوگر" location="bottom">
<v-tooltip text="هوش مصنوعی" location="bottom" v-if="permissions.ai">
<template v-slot:activator="{ props }">
<v-btn class="" stacked v-bind="props" to="/acc/wizard/home">
<v-icon>mdi-robot</v-icon>

View file

@ -300,22 +300,31 @@
</div>
<v-row class="mt-4">
<v-col cols="6">
<v-col cols="4">
<v-text-field
v-model="totalBd"
:model-value="calculatedTotalBd"
label="جمع بدهکار"
readonly
dense
></v-text-field>
</v-col>
<v-col cols="6">
<v-col cols="4">
<v-text-field
v-model="totalBs"
:model-value="calculatedTotalBs"
label="جمع بستانکار"
readonly
dense
></v-text-field>
</v-col>
<v-col cols="4">
<v-text-field
:model-value="differenceText"
readonly
dense
variant="outlined"
:color="differenceColor"
></v-text-field>
</v-col>
</v-row>
</v-form>
</v-container>
@ -407,6 +416,45 @@ export default {
computed: {
docId() {
return this.$route.params.id;
},
// محاسبه reactive جمعها
calculatedTotalBd() {
const total = this.form.rows.reduce((sum, row) => {
const value = parseInt(row.bd || 0);
return isNaN(value) ? sum : sum + value;
}, 0);
return total;
},
calculatedTotalBs() {
const total = this.form.rows.reduce((sum, row) => {
const value = parseInt(row.bs || 0);
return isNaN(value) ? sum : sum + value;
}, 0);
return total;
},
// محاسبه اختلاف جمع بدهکار و بستانکار
calculatedDifference() {
return this.calculatedTotalBd - this.calculatedTotalBs;
},
// تعیین رنگ مناسب برای اختلاف
differenceColor() {
if (this.calculatedDifference === 0) {
return 'success';
} else if (this.calculatedDifference > 0) {
return 'warning';
} else {
return 'error';
}
},
// تعیین متن مناسب برای اختلاف
differenceText() {
if (this.calculatedDifference === 0) {
return 'متوازن';
} else if (this.calculatedDifference > 0) {
return `بدهکار بیشتر: ${this.calculatedDifference.toLocaleString()}`;
} else {
return `بستانکار بیشتر: ${Math.abs(this.calculatedDifference).toLocaleString()}`;
}
}
},
mounted() {
@ -417,6 +465,17 @@ export default {
this.loading = false;
});
},
watch: {
// نظارت بر تغییرات در ردیفها برای validation خودکار
'form.rows': {
handler() {
this.$nextTick(() => {
this.calculateTotals();
});
},
deep: true
}
},
methods: {
showSnackbar(text, color = 'success') {
this.snackbar.text = text;
@ -489,7 +548,10 @@ export default {
}
}));
// فراخوانی calculateTotals بعد از اطمینان از بهروزرسانی تمام مقادیر
this.$nextTick(() => {
this.calculateTotals();
});
} else {
this.error = response.data.message || 'خطا در بارگذاری سند';
}
@ -499,12 +561,17 @@ export default {
},
addRow() {
this.form.rows.push({ ref: null, refName: '', bd: '0', bs: '0', des: '', detail: '', selectedAccounts: [], bankAccount: null, cashdesk: null, salary: null, commodity: null, commodityCount: null, person: null, tableType: null });
this.$nextTick(() => {
this.calculateTotals();
});
},
removeRow(item) {
const index = this.form.rows.indexOf(item);
if (index >= 0) {
this.form.rows.splice(index, 1);
this.$nextTick(() => {
this.calculateTotals();
});
}
},
calculateTotals() {
@ -518,8 +585,7 @@ export default {
return;
}
this.error = null;
this.totalBd = this.form.rows.reduce((sum, row) => sum + parseInt(row.bd || 0), 0);
this.totalBs = this.form.rows.reduce((sum, row) => sum + parseInt(row.bs || 0), 0);
// حالا از computed properties استفاده میکنیم، نیازی به محاسبه دستی نیست
},
validateDebitCredit(row) {
if (parseInt(row.bd) > 0 && parseInt(row.bs) > 0) {
@ -530,6 +596,10 @@ export default {
} else if (row.bs > 0) {
row.bs = '0';
}
// فراخوانی مجدد calculateTotals بعد از تغییر مقادیر
this.$nextTick(() => {
this.calculateTotals();
});
return false;
}
return true;
@ -587,7 +657,7 @@ export default {
return;
}
if (this.totalBd !== this.totalBs) {
if (this.calculatedTotalBd !== this.calculatedTotalBs) {
this.error = 'جمع بدهکار و بستانکار باید برابر باشد';
return;
}

View file

@ -211,7 +211,7 @@ export default defineComponent({
v-model="snackbar"
:color="snackbarColor"
timeout="3000"
location="top"
location="bottom"
>
{{ snackbarText }}
<template v-slot:actions>

View file

@ -0,0 +1,64 @@
# پنل استعلامات
این بخش شامل قابلیت‌های مختلف استعلام و تبدیل اطلاعات است که بر اساس تنظیمات سیستم فعال یا غیرفعال می‌شوند.
## قابلیت‌های موجود
### 1. تبدیل کد پستی به آدرس
- **وضعیت**: فعال/غیرفعال بر اساس تنظیمات سیستم
- **کارمزد**: قابل تنظیم در بخش مدیریت
- **API Endpoint**: `/api/plugins/inquiry/postalcode-to-address`
- **عملکرد**: تبدیل کد پستی 10 رقمی به آدرس کامل
### 2. تبدیل شماره کارت به شبا
- **وضعیت**: فعال/غیرفعال بر اساس تنظیمات سیستم
- **کارمزد**: قابل تنظیم در بخش مدیریت
- **API Endpoint**: `/api/plugins/inquiry/card-to-sheba`
- **عملکرد**: تبدیل شماره کارت 16 رقمی به شماره شبا (در حال توسعه)
### 3. تبدیل حساب به شبا
- **وضعیت**: فعال/غیرفعال بر اساس تنظیمات سیستم
- **کارمزد**: قابل تنظیم در بخش مدیریت
- **API Endpoint**: `/api/plugins/inquiry/account-to-sheba`
- **عملکرد**: تبدیل شماره حساب بانکی به شماره شبا (در حال توسعه)
## ویژگی‌های رابط کاربری
### نمایش سرویس‌های فعال
- نمایش کارت‌های رنگی برای هر سرویس فعال
- نمایش کارمزد هر سرویس
- نشان‌گذاری وضعیت فعال/غیرفعال
### دیالوگ‌های استعلام
- فرم‌های اعتبارسنجی شده
- نمایش کارمزد قبل از استعلام
- نمایش نتیجه در قالب پیام‌های زیبا
### مدیریت خطاها
- بررسی موجودی کافی
- نمایش پیام‌های خطای مناسب
- لاگ کردن عملیات‌ها
## تنظیمات مورد نیاز
برای فعال‌سازی این قابلیت‌ها، مدیر سیستم باید در بخش تنظیمات سیستم موارد زیر را تنظیم کند:
1. **فعال‌سازی پنل استعلامات**: `inquiryPanelEnable`
2. **انتخاب پنل**: `inquiryPanel` (فعلاً فقط زحل)
3. **کلید API پنل**: `inquiryZohalAPIKey`
4. **فعال‌سازی هر سرویس**: `enablePostalCodeToAddress`, `enableCardToSheba`, `enableAccountToSheba`
5. **تعیین کارمزد**: `postalCodeToAddressFee`, `cardToShebaFee`, `accountToShebaFee`
## نکات فنی
- تمام درخواست‌ها نیاز به احراز هویت دارند
- کارمزد از موجودی SMS کاربر کسر می‌شود
- نتایج در کش ذخیره می‌شوند تا از تکرار درخواست‌های مشابه جلوگیری شود
- تمام عملیات لاگ می‌شوند
## توسعه آینده
- تکمیل سرویس‌های تبدیل کارت و حساب به شبا
- اضافه کردن سرویس‌های جدید
- بهبود رابط کاربری
- اضافه کردن گزارش‌گیری

File diff suppressed because it is too large Load diff

View file

@ -141,7 +141,11 @@
</v-col>
<v-col cols="12" md="6">
<v-text-field v-model="person.postalcode" :label="$t('pages.person.postal_code')" dense
prepend-inner-icon="mdi-mailbox" hide-details />
prepend-inner-icon="mdi-mailbox" hide-details>
<template v-slot:append-inner>
<PostalCode2Address :postal-code="person.postalcode" @address-found="fillAddressFields" @show-snackbar="showSnackbarFromChild" />
</template>
</v-text-field>
</v-col>
<v-col cols="12">
<v-textarea v-model="person.address" :label="$t('pages.person.address')" dense
@ -211,15 +215,29 @@
<v-overlay :model-value="loading" contained class="align-center justify-center">
<v-progress-circular indeterminate size="64" />
</v-overlay>
<!-- Snackbar برای نمایش پیامها -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="snackbar.timeout">
{{ snackbar.text }}
<template v-slot:actions>
<v-btn color="white" text @click="snackbar.show = false">
بستن
</v-btn>
</template>
</v-snackbar>
</template>
<script>
import Swal from "sweetalert2";
import axios from "axios";
import { ref } from "vue";
import PostalCode2Address from "@/components/widgets/inquiry/postalcode2address.vue";
export default {
name: "insert",
components: {
PostalCode2Address
},
data() {
return {
tabs: '0',
@ -250,6 +268,12 @@ export default {
accounts: [],
prelabel: ref(null),
speedAccess: false
},
snackbar: {
show: false,
text: '',
color: '',
timeout: 3000
}
};
},
@ -362,7 +386,7 @@ export default {
try {
const response = await axios.post('/api/person/mod/' + this.person.code, this.person);
this.loading = false;
if (response.data.result === 2) {
if (response.data && response.data.result === 2) {
Swal.fire({
text: this.$t('pages.person.already_exists'),
icon: 'error',
@ -386,7 +410,72 @@ export default {
});
}
}
}
},
showSnackbar(text, color = 'success', timeout = 3000) {
this.snackbar.show = true;
this.snackbar.text = text;
this.snackbar.color = color;
this.snackbar.timeout = timeout;
},
fillAddressFields(addressData) {
// پر کردن فیلدهای آدرس
this.person.keshvar = 'ایران'; // کشور به صورت پیشفرض ایران
this.person.ostan = addressData.province || '';
this.person.shahr = addressData.town || '';
this.person.postalcode = addressData.postalCode || '';
// ساخت آدرس کامل
let fullAddress = '';
if (addressData.district) {
fullAddress += addressData.district;
}
if (addressData.street) {
if (fullAddress) fullAddress += ' - ';
fullAddress += addressData.street;
}
if (addressData.street2) {
if (fullAddress) fullAddress += ' - ';
fullAddress += addressData.street2;
}
if (addressData.number) {
if (fullAddress) fullAddress += ' - پلاک ';
fullAddress += addressData.number;
}
if (addressData.floor && addressData.floor !== 'همکف') {
if (fullAddress) fullAddress += ' - طبقه ';
fullAddress += addressData.floor;
}
if (addressData.side_floor) {
if (fullAddress) fullAddress += ' - واحد ';
fullAddress += addressData.side_floor;
}
if (addressData.building_name) {
if (fullAddress) fullAddress += ' - ';
fullAddress += addressData.building_name;
}
if (addressData.description) {
if (fullAddress) fullAddress += ' - ';
fullAddress += addressData.description;
}
this.person.address = fullAddress;
// نمایش پیام موفقیت
this.$nextTick(() => {
this.showSnackbar('اطلاعات آدرس با موفقیت تکمیل شد', 'success');
});
},
showSnackbarFromChild(snackbarData) {
this.showSnackbar(snackbarData.text, snackbarData.color, snackbarData.timeout);
}
}
};
</script>

View file

@ -230,7 +230,7 @@
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="top"
location="bottom"
>
{{ snackbar.text }}
<template v-slot:actions>

View file

@ -241,6 +241,12 @@ const router = createRouter({
component: () =>
import ('../views/settings/extramoneys.vue'),
},
{
path: '/acc/business/tax-settings',
name: 'business_tax_settings',
component: () =>
import ('../views/settings/tax-settings.vue'),
},
{
path: '/acc/business/logs',
name: 'business_logs',

View file

@ -0,0 +1,321 @@
<template>
<div>
<v-toolbar color="toolbar" title="تنظیمات مالیاتی">
<template v-slot:prepend>
<v-tooltip text="بازگشت" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-btn :loading="loading" @click="saveSettings()" icon="" color="green">
<v-tooltip activator="parent" text="ذخیره تنظیمات" location="bottom" />
<v-icon icon="mdi-content-save"></v-icon>
</v-btn>
</v-toolbar>
<v-container>
<v-card :loading="loading" :disabled="loading">
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-btn
color="primary"
@click="showCSRDialog = true"
prepend-icon="mdi-key-plus"
>
ساخت کلید و CSR
</v-btn>
</v-col>
<v-col cols="12" md="6">
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.taxMemoryId"
label="شناسه یکتای حافظه مالیاتی"
hide-details
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.economicCode"
label="کد اقتصادی"
hide-details
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col cols="12">
<v-textarea
v-model="settings.privateKey"
label="Private Key"
rows="15"
variant="outlined"
hide-details
placeholder="کلید خصوصی اینجا قرار می‌گیرد..."
></v-textarea>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-container>
<!-- Dialog برای ساخت کلید و CSR -->
<v-dialog v-model="showCSRDialog" max-width="600px">
<v-card>
<v-card-title class="text-h6">
ساخت کلید و CSR
</v-card-title>
<v-card-text>
<v-form ref="csrForm">
<div class="mb-4">
<div class="text-subtitle-2 mb-2">شخص</div>
<v-radio-group
v-model="csrData.personType"
inline
hide-details
>
<v-radio
v-for="type in personTypes"
:key="type.value"
:label="type.title"
:value="type.value"
:disabled="type.value === 'natural'"
></v-radio>
</v-radio-group>
</div>
<v-text-field
v-model="csrData.nationalId"
label="شناسه ملی"
hide-details
class="mb-4"
></v-text-field>
<v-text-field
v-model="csrData.nameFa"
label="نام (فارسی)"
hide-details
class="mb-4"
></v-text-field>
<v-text-field
v-model="csrData.nameEn"
label="نام (انگلیسی)"
hide-details
class="mb-4"
></v-text-field>
<v-text-field
v-model="csrData.email"
label="ایمیل"
type="email"
hide-details
class="mb-4"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="showCSRDialog = false" variant="text">
انصراف
</v-btn>
<v-btn @click="generateCSR()" color="primary" :loading="csrLoading">
تایید
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showResultDialog" max-width="900px">
<v-card>
<v-card-title class="text-h6 pb-0">ساخت کلید و CSR</v-card-title>
<v-card-text>
<v-alert type="info" color="blue" class="mb-4" icon="mdi-alert">
<span class="font-weight-bold">توجه: لطفا این اطلاعات را دانلود کنید و در یک جای امن نگهداری کنید. به دلایل امنیتی اطلاعات شما را نگهداری نمیکنیم، در صورتی که این اطلاعات را گم کنید، امکان بازیابی آن وجود ندارد.</span>
</v-alert>
<v-row>
<v-col cols="12" md="4">
<div class="mb-2 font-weight-bold">CSR</div>
<v-textarea readonly rows="10" :value="resultData.csr" variant="outlined"></v-textarea>
<v-row class="mt-2">
<v-col cols="6">
<v-btn color="success" block @click="copyToClipboard(resultData.csr)"><v-icon start>mdi-content-copy</v-icon>کپی</v-btn>
</v-col>
<v-col cols="6">
<v-btn color="success" block @click="downloadFile(resultData.csr, 'csr.txt')"><v-icon start>mdi-download</v-icon>دانلود</v-btn>
</v-col>
</v-row>
</v-col>
<v-col cols="12" md="4">
<div class="mb-2 font-weight-bold">Public Key</div>
<v-textarea readonly rows="10" :value="resultData.publicKey" variant="outlined"></v-textarea>
<v-row class="mt-2">
<v-col cols="6">
<v-btn color="success" block @click="copyToClipboard(resultData.publicKey)"><v-icon start>mdi-content-copy</v-icon>کپی</v-btn>
</v-col>
<v-col cols="6">
<v-btn color="success" block @click="downloadFile(resultData.publicKey, 'public_key.txt')"><v-icon start>mdi-download</v-icon>دانلود</v-btn>
</v-col>
</v-row>
</v-col>
<v-col cols="12" md="4">
<div class="mb-2 font-weight-bold">Private Key</div>
<v-textarea readonly rows="10" :value="resultData.privateKey" variant="outlined"></v-textarea>
<v-row class="mt-2">
<v-col cols="6">
<v-btn color="success" block @click="copyToClipboard(resultData.privateKey)"><v-icon start>mdi-content-copy</v-icon>کپی</v-btn>
</v-col>
<v-col cols="6">
<v-btn color="success" block @click="downloadFile(resultData.privateKey, 'private_key.txt')"><v-icon start>mdi-download</v-icon>دانلود</v-btn>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="showResultDialog = false" color="primary">بستن</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
import axios from 'axios';
import Swal from 'sweetalert2';
export default {
name: 'TaxSettings',
data: () => ({
loading: false,
csrLoading: false,
showCSRDialog: false,
showResultDialog: false,
settings: {
taxMemoryId: '',
economicCode: '',
privateKey: '',
},
csrData: {
personType: 'legal',
nationalId: '',
nameFa: '',
nameEn: '',
email: '',
},
resultData: {
csr: '',
publicKey: '',
privateKey: ''
},
personTypes: [
{ title: 'حقیقی', value: 'natural' },
{ title: 'حقوقی', value: 'legal' }
],
snackbar: {
show: false,
text: '',
color: 'success'
}
}),
methods: {
async loadSettings() {
this.loading = true;
try {
const response = await axios.get('/api/plugins/tax-settings/get');
this.settings = {
...this.settings,
...response.data
};
} catch (error) {
this.showSnackbar('خطا در بارگذاری تنظیمات', 'error');
} finally {
this.loading = false;
}
},
async saveSettings() {
this.loading = true;
try {
const dataToSave = { ...this.settings };
await axios.post('/api/plugins/tax-settings/save', dataToSave);
this.showSnackbar('تنظیمات با موفقیت ذخیره شد', 'success');
} catch (error) {
this.showSnackbar('خطا در ذخیره تنظیمات', 'error');
} finally {
this.loading = false;
}
},
async generateCSR() {
this.csrLoading = true;
try {
const response = await axios.post('/api/plugins/tax-settings/generate-csr', this.csrData);
if (response.data.success) {
// this.settings.privateKey = response.data.privateKey;
// نمایش دیالوگ نتیجه
this.resultData.csr = response.data.csr;
this.resultData.privateKey = response.data.privateKey;
this.resultData.publicKey = response.data.publicKey || '';
this.showResultDialog = true;
this.showCSRDialog = false;
this.showSnackbar('کلید و CSR با موفقیت تولید شد', 'success');
} else {
this.showSnackbar(response.data.message || 'خطا در تولید کلید و CSR', 'error');
}
} catch (error) {
this.showSnackbar('خطا در تولید کلید و CSR', 'error');
} finally {
this.csrLoading = false;
}
},
copyToClipboard(text) {
navigator.clipboard.writeText(text);
this.showSnackbar('کپی شد');
},
downloadFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
},
showSnackbar(text, color = 'success') {
this.snackbar = {
show: true,
text,
color
};
}
},
mounted() {
this.loadSettings();
}
};
</script>

View file

@ -59,6 +59,7 @@
</v-alert>
</div>
</v-alert>
</v-card-text>
<v-row>
<v-col cols="12" md="4">
@ -161,6 +162,30 @@
:disabled="loadingSwitches.archiveView"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.inquiry"
label="سرویس استعلام"
@change="savePerms('inquiry')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.inquiry"
:disabled="loadingSwitches.inquiry"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.ai"
label="هوش مصنوعی"
@change="savePerms('ai')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.ai"
:disabled="loadingSwitches.ai"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
@ -601,7 +626,32 @@
</v-card>
</v-col>
</v-row>
</v-card-text>
<v-row v-if="isPluginActive('taxsettings')" class="mt-4">
<v-col cols="12">
<v-card-title class="text-h6 font-weight-bold mb-4">افزونه تنظیمات مالیاتی</v-card-title>
</v-col>
<v-col cols="12" md="4">
<v-card variant="outlined" class="h-100">
<v-card-text>
<v-list>
<v-list-item>
<v-switch
v-model="info.plugTaxSettings"
label="مدیریت تنظیمات مالیاتی"
@change="savePerms('plugTaxSettings')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugTaxSettings"
:disabled="loadingSwitches.plugTaxSettings"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card>
</v-container>
<v-snackbar
@ -679,7 +729,10 @@ export default {
plugNoghreSell: false,
plugCCAdmin: false,
plugHrmDocs: false,
plugGhestaManager: false
plugGhestaManager: false,
plugTaxSettings: false,
inquiry: false,
ai: false
};
axios.post('/api/business/get/user/permissions',

View file

@ -56,7 +56,6 @@
<ul class="mb-0 ps-4">
<li class="mb-2">به مبالغ انتخاب شده ۱۰ درصد مالیات بر ارزش افزوده اضافه میگردد.</li>
<li class="mb-2">اعتبار خریداری شده بلافاصله به حساب شما اضافه خواهد شد.</li>
<li>این اعتبار صرفاً برای استفاده از سرویس پیامک کوتاه قابل استفاده است و برای سایر خدمات قابل استفاده نمیباشد.</li>
</ul>
</div>
</v-alert>

View file

@ -0,0 +1,173 @@
<template>
<div>
<v-toolbar color="toolbar" title="صورتحساب‌های ارسالی به سامانه مودیان مالیاتی">
<template v-slot:prepend>
<v-tooltip text="بازگشت" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-btn :loading="loading" @click="loadData()" icon="" color="primary">
<v-tooltip activator="parent" text="بازخوانی" location="bottom" />
<v-icon icon="mdi-refresh"></v-icon>
</v-btn>
</v-toolbar>
<v-container>
<v-card :loading="loading" :disabled="loading">
<v-card-text>
<v-alert type="info" color="blue" class="mb-4" icon="mdi-information">
<span class="font-weight-bold">این بخش برای نمایش لیست صورتحسابهایی است که به سامانه مودیان مالیاتی ارسال شدهاند.</span>
</v-alert>
<v-data-table
:headers="headers"
:items="invoices"
:loading="loading"
class="elevation-1"
:items-per-page="10"
:items-per-page-options="[10, 25, 50, 100]"
>
<template v-slot:item.status="{ item }">
<v-chip
:color="getStatusColor(item.status)"
:text="getStatusText(item.status)"
size="small"
></v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
icon="mdi-eye"
variant="text"
size="small"
@click="viewInvoice(item)"
color="primary"
></v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'TaxInvoicesList',
data: () => ({
loading: false,
invoices: [],
headers: [
{ title: 'شماره فاکتور', key: 'invoiceNumber', sortable: true },
{ title: 'تاریخ', key: 'date', sortable: true },
{ title: 'مشتری', key: 'customerName', sortable: true },
{ title: 'مبلغ کل', key: 'totalAmount', sortable: true },
{ title: 'وضعیت ارسال', key: 'status', sortable: true },
{ title: 'تاریخ ارسال', key: 'sentDate', sortable: true },
{ title: 'عملیات', key: 'actions', sortable: false }
],
snackbar: {
show: false,
text: '',
color: 'success'
}
}),
methods: {
async loadData() {
this.loading = true;
try {
// اینجا باید API مربوط به دریافت لیست صورتحسابهای ارسالی را فراخوانی کنید
// const response = await axios.get('/api/plugins/tax-settings/invoices');
// this.invoices = response.data;
// فعلاً دادههای نمونه
this.invoices = [
{
id: 1,
invoiceNumber: 'INV-001',
date: '1402/12/15',
customerName: 'شرکت نمونه',
totalAmount: '1,500,000',
status: 'sent',
sentDate: '1402/12/16'
},
{
id: 2,
invoiceNumber: 'INV-002',
date: '1402/12/14',
customerName: 'فروشگاه نمونه',
totalAmount: '2,300,000',
status: 'pending',
sentDate: '-'
}
];
} catch (error) {
this.showSnackbar('خطا در بارگذاری داده‌ها', 'error');
} finally {
this.loading = false;
}
},
getStatusColor(status) {
switch (status) {
case 'sent':
return 'success';
case 'pending':
return 'warning';
case 'failed':
return 'error';
default:
return 'grey';
}
},
getStatusText(status) {
switch (status) {
case 'sent':
return 'ارسال شده';
case 'pending':
return 'در انتظار';
case 'failed':
return 'ناموفق';
default:
return 'نامشخص';
}
},
viewInvoice(item) {
// اینجا میتوانید به صفحه جزئیات فاکتور بروید
this.showSnackbar('نمایش جزئیات فاکتور: ' + item.invoiceNumber);
},
showSnackbar(text, color = 'success') {
this.snackbar = {
show: true,
text,
color
};
}
},
mounted() {
this.loadData();
}
};
</script>

View file

@ -0,0 +1,321 @@
<template>
<div>
<v-toolbar color="toolbar" title="تنظیمات مالیاتی">
<template v-slot:prepend>
<v-tooltip text="بازگشت" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-btn :loading="loading" @click="saveSettings()" icon="" color="green">
<v-tooltip activator="parent" text="ذخیره تنظیمات" location="bottom" />
<v-icon icon="mdi-content-save"></v-icon>
</v-btn>
</v-toolbar>
<v-container>
<v-card :loading="loading" :disabled="loading">
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-btn
color="primary"
@click="showCSRDialog = true"
prepend-icon="mdi-key-plus"
>
ساخت کلید و CSR
</v-btn>
</v-col>
<v-col cols="12" md="6">
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.taxMemoryId"
label="شناسه یکتای حافظه مالیاتی"
hide-details
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.economicCode"
label="کد اقتصادی"
hide-details
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col cols="12">
<v-textarea
v-model="settings.privateKey"
label="Private Key"
rows="15"
variant="outlined"
hide-details
placeholder="کلید خصوصی اینجا قرار می‌گیرد..."
></v-textarea>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-container>
<!-- Dialog برای ساخت کلید و CSR -->
<v-dialog v-model="showCSRDialog" max-width="600px">
<v-card>
<v-card-title class="text-h6">
ساخت کلید و CSR
</v-card-title>
<v-card-text>
<v-form ref="csrForm">
<div class="mb-4">
<div class="text-subtitle-2 mb-2">شخص</div>
<v-radio-group
v-model="csrData.personType"
inline
hide-details
>
<v-radio
v-for="type in personTypes"
:key="type.value"
:label="type.title"
:value="type.value"
:disabled="type.value === 'natural'"
></v-radio>
</v-radio-group>
</div>
<v-text-field
v-model="csrData.nationalId"
label="شناسه ملی"
hide-details
class="mb-4"
></v-text-field>
<v-text-field
v-model="csrData.nameFa"
label="نام (فارسی)"
hide-details
class="mb-4"
></v-text-field>
<v-text-field
v-model="csrData.nameEn"
label="نام (انگلیسی)"
hide-details
class="mb-4"
></v-text-field>
<v-text-field
v-model="csrData.email"
label="ایمیل"
type="email"
hide-details
class="mb-4"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="showCSRDialog = false" variant="text">
انصراف
</v-btn>
<v-btn @click="generateCSR()" color="primary" :loading="csrLoading">
تایید
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showResultDialog" max-width="900px">
<v-card>
<v-card-title class="text-h6 pb-0">ساخت کلید و CSR</v-card-title>
<v-card-text>
<v-alert type="info" color="blue" class="mb-4" icon="mdi-alert">
<span class="font-weight-bold">توجه: لطفا این اطلاعات را دانلود کنید و در یک جای امن نگهداری کنید. به دلایل امنیتی اطلاعات شما را نگهداری نمیکنیم، در صورتی که این اطلاعات را گم کنید، امکان بازیابی آن وجود ندارد.</span>
</v-alert>
<v-row>
<v-col cols="12" md="4">
<div class="mb-2 font-weight-bold">CSR</div>
<v-textarea readonly rows="10" :value="resultData.csr" variant="outlined"></v-textarea>
<v-row class="mt-2">
<v-col cols="6">
<v-btn color="success" block @click="copyToClipboard(resultData.csr)"><v-icon start>mdi-content-copy</v-icon>کپی</v-btn>
</v-col>
<v-col cols="6">
<v-btn color="success" block @click="downloadFile(resultData.csr, 'csr.txt')"><v-icon start>mdi-download</v-icon>دانلود</v-btn>
</v-col>
</v-row>
</v-col>
<v-col cols="12" md="4">
<div class="mb-2 font-weight-bold">Public Key</div>
<v-textarea readonly rows="10" :value="resultData.publicKey" variant="outlined"></v-textarea>
<v-row class="mt-2">
<v-col cols="6">
<v-btn color="success" block @click="copyToClipboard(resultData.publicKey)"><v-icon start>mdi-content-copy</v-icon>کپی</v-btn>
</v-col>
<v-col cols="6">
<v-btn color="success" block @click="downloadFile(resultData.publicKey, 'public_key.txt')"><v-icon start>mdi-download</v-icon>دانلود</v-btn>
</v-col>
</v-row>
</v-col>
<v-col cols="12" md="4">
<div class="mb-2 font-weight-bold">Private Key</div>
<v-textarea readonly rows="10" :value="resultData.privateKey" variant="outlined"></v-textarea>
<v-row class="mt-2">
<v-col cols="6">
<v-btn color="success" block @click="copyToClipboard(resultData.privateKey)"><v-icon start>mdi-content-copy</v-icon>کپی</v-btn>
</v-col>
<v-col cols="6">
<v-btn color="success" block @click="downloadFile(resultData.privateKey, 'private_key.txt')"><v-icon start>mdi-download</v-icon>دانلود</v-btn>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="showResultDialog = false" color="primary">بستن</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
import axios from 'axios';
import Swal from 'sweetalert2';
export default {
name: 'TaxSettings',
data: () => ({
loading: false,
csrLoading: false,
showCSRDialog: false,
showResultDialog: false,
settings: {
taxMemoryId: '',
economicCode: '',
privateKey: '',
},
csrData: {
personType: 'legal',
nationalId: '',
nameFa: '',
nameEn: '',
email: '',
},
resultData: {
csr: '',
publicKey: '',
privateKey: ''
},
personTypes: [
{ title: 'حقیقی', value: 'natural' },
{ title: 'حقوقی', value: 'legal' }
],
snackbar: {
show: false,
text: '',
color: 'success'
}
}),
methods: {
async loadSettings() {
this.loading = true;
try {
const response = await axios.get('/api/plugins/tax-settings/get');
this.settings = {
...this.settings,
...response.data
};
} catch (error) {
this.showSnackbar('خطا در بارگذاری تنظیمات', 'error');
} finally {
this.loading = false;
}
},
async saveSettings() {
this.loading = true;
try {
const dataToSave = { ...this.settings };
await axios.post('/api/plugins/tax-settings/save', dataToSave);
this.showSnackbar('تنظیمات با موفقیت ذخیره شد', 'success');
} catch (error) {
this.showSnackbar('خطا در ذخیره تنظیمات', 'error');
} finally {
this.loading = false;
}
},
async generateCSR() {
this.csrLoading = true;
try {
const response = await axios.post('/api/plugins/tax-settings/generate-csr', this.csrData);
if (response.data.success) {
// this.settings.privateKey = response.data.privateKey;
// نمایش دیالوگ نتیجه
this.resultData.csr = response.data.csr;
this.resultData.privateKey = response.data.privateKey;
this.resultData.publicKey = response.data.publicKey || '';
this.showResultDialog = true;
this.showCSRDialog = false;
this.showSnackbar('کلید و CSR با موفقیت تولید شد', 'success');
} else {
this.showSnackbar(response.data.message || 'خطا در تولید کلید و CSR', 'error');
}
} catch (error) {
this.showSnackbar('خطا در تولید کلید و CSR', 'error');
} finally {
this.csrLoading = false;
}
},
copyToClipboard(text) {
navigator.clipboard.writeText(text);
this.showSnackbar('کپی شد');
},
downloadFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
},
showSnackbar(text, color = 'success') {
this.snackbar = {
show: true,
text,
color
};
}
},
mounted() {
this.loadSettings();
}
};
</script>

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,7 @@
<p><span class="font-weight-bold primary--text">{{ $t('updateSoftware.distroVersion') }}:</span> {{ systemInfo.distroVersion }}</p>
<p><span class="font-weight-bold primary--text">{{ $t('updateSoftware.webServer') }}:</span> {{ systemInfo.webServer }}</p>
<p><span class="font-weight-bold primary--text">{{ $t('updateSoftware.dbName') }}:</span> {{ systemInfo.dbName }}</p>
<p><span class="font-weight-bold primary--useStateFiletext">{{ $t('updateSoftware.dbVersion') }}:</span> {{ systemInfo.dbVersion }}</p>
<p><span class="font-weight-bold primary--text">{{ $t('updateSoftware.dbVersion') }}:</span> {{ systemInfo.dbVersion }}</p>
<p><span class="font-weight-bold primary--text">{{ $t('updateSoftware.currentEnv') }}:</span> {{ selectedEnv }}</p>
</v-card-text>
</v-card>
@ -76,6 +76,39 @@
<v-window-item>
<v-card flat>
<v-card-text>
<!-- بخش تنظیمات منبع بهروزرسانی -->
<v-card class="mb-4" variant="outlined">
<v-card-title class="text-subtitle-1">
{{ $t('updateSoftware.updateSourceTitle') }}
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="8">
<v-text-field
v-model="updateSourceUrl"
:label="$t('updateSoftware.updateSourceLabel')"
placeholder="https://github.com/username/repository.git"
outlined
dense
:disabled="isUpdating || isChangingSource"
:loading="isChangingSource"
></v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-btn
color="secondary"
:loading="isChangingSource"
:disabled="isUpdating || isChangingSource || !updateSourceUrl.trim()"
@click="changeUpdateSource"
block
>
{{ $t('updateSoftware.changeSourceButton') }}
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-row justify="end" class="mb-4">
<v-col cols="auto">
<v-btn-group size="small">
@ -201,6 +234,8 @@ export default {
const isLoadingLogs = ref(false);
const isClearingLogs = ref(false);
const isPolling = ref(false);
const updateSourceUrl = ref('');
const isChangingSource = ref(false);
return {
isUpdating,
@ -229,6 +264,8 @@ export default {
isLoadingLogs,
isClearingLogs,
isPolling,
updateSourceUrl,
isChangingSource,
};
},
computed: {
@ -508,6 +545,32 @@ export default {
};
}
},
async fetchCurrentSource() {
try {
const response = await axios.get('/api/admin/updatecore/current-source', {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
timeout: 7200000 // تایماوت 2 ساعته
});
if (response.data.status === 'success') {
this.updateSourceUrl = response.data.sourceUrl || '';
} else {
console.error('Failed to fetch current source:', response.data.message);
this.updateSourceUrl = '';
}
} catch (error) {
console.error('Failed to fetch current source:', error);
this.updateSourceUrl = '';
// نمایش پیام خطا به کاربر
if (error.response?.data?.message) {
this.showResultDialog = true;
this.dialogTitle = this.$t('updateSoftware.dialogErrorTitle');
this.dialogMessage = error.response.data.message;
this.dialogColor = 'error';
}
}
},
async fetchCurrentEnv() {
try {
const response = await axios.get('/api/admin/updatecore/current-env', {
@ -570,6 +633,42 @@ export default {
this.isClearingLogs = false;
}
},
async changeUpdateSource() {
if (!this.updateSourceUrl.trim()) {
this.showResultDialog = true;
this.dialogTitle = this.$t('updateSoftware.dialogErrorTitle');
this.dialogMessage = this.$t('updateSoftware.sourceUrlRequired');
this.dialogColor = 'error';
return;
}
this.isChangingSource = true;
this.showOutput = true;
this.output = this.$t('updateSoftware.changingSourceMessage') + '\n';
try {
const response = await axios.post('/api/admin/updatecore/change-source', {
sourceUrl: this.updateSourceUrl.trim()
}, {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
timeout: 7200000 // تایماوت 2 ساعته
});
this.output += response.data.output || response.data.message + '\n';
this.showResultDialog = true;
this.dialogTitle = this.$t('updateSoftware.dialogSuccessTitle');
this.dialogMessage = response.data.message || this.$t('updateSoftware.repositoryChangeSuccess');
this.dialogColor = 'success';
} catch (error) {
this.output += 'خطا: ' + (error.response?.data?.message || error.message) + '\n';
this.showResultDialog = true;
this.dialogTitle = this.$t('updateSoftware.dialogErrorTitle');
this.dialogMessage = error.response?.data?.message || this.$t('updateSoftware.sourceChangeError');
this.dialogColor = 'error';
} finally {
this.isChangingSource = false;
}
},
copyLogsToClipboard() {
const plainLogs = this.systemLogs.replace(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+\+\d{2}:\d{2}\]/g, '\n[$&]')
.replace(/\s+\[\]/g, ' []')
@ -593,6 +692,7 @@ export default {
this.fetchCommits();
this.fetchSystemInfo();
this.fetchCurrentEnv();
this.fetchCurrentSource();
this.buttonText = this.$t('updateSoftware.startButton');
this.refreshLogs();
},

View file

@ -42,7 +42,7 @@
<td>{{ item.totalIncome }}</td>
<td>{{ calculateStatus(item) }}</td>
<td>
<v-tooltip v-if="calculateStatus(item) === 'در صف تسویه'" location="top">
<v-tooltip v-if="calculateStatus(item) === 'در انتظار پرداخت'" location="top">
<template v-slot:activator="{ props }">
<v-btn variant="text" icon v-bind="props" @click="openTransactionDialog(item)">
<v-icon>mdi-cash-register</v-icon>
@ -80,6 +80,7 @@
<td>{{ item.bidName }}</td>
<td>{{ item.bankAcName }}</td>
<td>{{ item.type === 'pay' ? 'پرداخت' : 'دریافت' }}</td>
<td>{{ $filters.formatNumber(item.amount) }}</td>
<td>{{ item.gatePay }}</td>
<td>{{ item.refID }}</td>
<td>{{ item.shaba }}</td>
@ -186,6 +187,7 @@ export default {
{ title: "کسب‌و‌کار", key: "bidName", sortable: false, align: 'center' },
{ title: "بانک", key: "bankAcName", sortable: false, align: 'center' },
{ title: "نوع", key: "type", sortable: false, align: 'center' },
{ title: "مبلغ", key: "amount", sortable: false, align: 'center' },
{ title: "درگاه پرداخت", key: "gatePay", sortable: false, align: 'center' },
{ title: "شناسه تراکنش", key: "refID", sortable: false, align: 'center' },
{ title: "شبا", key: "shaba", sortable: false, align: 'center' },

View file

@ -1,464 +1,575 @@
<template>
<v-toolbar color="toolbar" title="هوش مصنوعی حسابیکس">
</v-toolbar>
<div class="page-container">
<div class="content-container">
<v-card class="chat-container" elevation="0">
<div class="chat-box">
<div class="messages-container" ref="messagesContainer">
<!-- پیام هوش مصنوعی -->
<div class="message ai-message" v-if="displayWelcome">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">{{ displayWelcome }}</div>
</div>
</div>
<div class="chat-container">
<div class="message ai-message" v-if="displayThanks">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">{{ displayThanks }}</div>
</div>
</div>
<div class="message ai-message" v-if="displayCapabilities">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">{{ displayCapabilities }}</div>
</div>
</div>
<div class="message ai-message" v-for="(capability, index) in displayCapabilitiesList" :key="index">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">
<v-icon color="#1a237e" size="16" class="mr-2">mdi-check-circle</v-icon>
{{ capability }}
</div>
</div>
</div>
<div class="message ai-message" v-if="displayPrompt">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">{{ displayPrompt }}</div>
</div>
</div>
<!-- پیامهای کاربر و پاسخهای هوش مصنوعی -->
<template v-for="(message, index) in userMessages" :key="index">
<!-- پیام کاربر -->
<div class="message user-message" v-if="typeof message === 'string'">
<div class="message-content">
<div class="message-text">{{ message }}</div>
</div>
<v-avatar color="grey lighten-2" size="36" class="ml-2">
<v-icon color="grey darken-1" size="20">mdi-account</v-icon>
</v-avatar>
</div>
<!-- پیام هوش مصنوعی -->
<div class="message ai-message" v-else-if="message && message.isAI">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content" :class="{ 'details-message': message.isDetails }">
<div class="message-text" v-html="message.text.replace(/\n/g, '<br>')"></div>
</div>
<!-- ناحیه پیامها -->
<div class="messages-container" ref="messagesContainer">
<div
v-for="(message, index) in messages"
:key="index"
:class="['message', message.type]"
>
<div class="message-avatar">
<v-icon v-if="message.type === 'user'" size="24" color="white">mdi-account</v-icon>
<v-icon v-else size="24" color="primary">mdi-robot</v-icon>
</div>
<div class="message-content">
<div class="message-bubble">
<!-- تغییر: رندر داینامیک بر اساس نوع داده -->
<template v-if="message.type === 'ai' && message.data">
<div v-for="(item, idx) in message.data.data" :key="idx">
<div v-if="item.type === 'text'">{{ item.content }}</div>
<v-table v-else-if="item.type === 'table'" class="my-2" density="compact">
<thead>
<tr>
<th v-for="h in item.headers" :key="h">{{ h }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rIdx) in item.rows" :key="rIdx">
<td v-for="(cell, cIdx) in row" :key="cIdx">{{ cell }}</td>
</tr>
</tbody>
</v-table>
<AIChart v-else-if="item.type === 'chart' || item.chartType" :chartData="item" height="300" class="my-2" />
</div>
</template>
<!-- نشانگر تایپ -->
<div class="message ai-message" v-if="isTyping">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text">
<span class="typing-indicator">
<span></span>
<span></span>
<span></span>
</span>
</div>
</div>
</div>
</div>
<!-- باکس ورودی پیام -->
<div class="input-container">
<v-textarea v-model="userMessage" placeholder="پیام خود را اینجا بنویسید..." rows="1" auto-grow hide-details
variant="plain" class="message-input" @keydown.enter.prevent="sendMessage"></v-textarea>
<v-btn color="#1a237e" icon :loading="isLoading" @click="sendMessage" class="send-button"
:disabled="!userMessage.trim()">
<v-icon>mdi-send</v-icon>
</v-btn>
<template v-else>
<p class="message-text">{{ message.text }}</p>
</template>
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
</div>
</div>
</v-card>
</div>
<!-- نشانگر تایپ -->
<div v-if="isTyping" class="message ai">
<div class="message-avatar">
<v-icon size="24" color="primary">mdi-robot</v-icon>
</div>
<div class="message-content">
<div class="message-bubble typing">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>
<!-- ناحیه ورودی -->
<div class="input-container">
<div class="input-wrapper">
<v-textarea
v-model="newMessage"
:placeholder="aiEnabled ? 'پیام خود را بنویسید...' : 'هوش مصنوعی غیرفعال است'"
variant="outlined"
rows="1"
auto-grow
hide-details
class="message-input"
:disabled="!aiEnabled"
@keydown.enter="sendMessage"
></v-textarea>
<v-btn
:disabled="!newMessage.trim() || isTyping || !aiEnabled"
color="primary"
icon
size="small"
class="send-button"
@click="sendMessage"
>
<v-icon>mdi-send</v-icon>
</v-btn>
</div>
<!-- دکمههای سریع -->
<div v-if="aiEnabled" class="quick-actions">
<v-chip
v-for="suggestion in quickSuggestions"
:key="suggestion"
variant="outlined"
size="small"
class="quick-chip"
@click="sendQuickMessage(suggestion)"
>
{{ suggestion }}
</v-chip>
</div>
<!-- نشانگر وضعیت -->
<div v-if="!aiEnabled && aiStatus !== 'checking'" class="status-indicator">
<v-alert
:type="aiStatus === 'error' ? 'error' : 'warning'"
variant="tonal"
density="compact"
class="status-alert"
>
{{ getStatusMessage(aiStatus, '') }}
</v-alert>
</div>
</div>
</div>
</template>
<script>
import { useNavigationStore } from '@/stores/navigationStore';
import axios from 'axios';
import AIChart from '@/components/widgets/AIChart.vue';
export default {
name: 'WizardHome',
name: "WizardHome",
components: { AIChart },
data() {
return {
userMessage: '',
isLoading: false,
isTyping: true,
userMessages: [],
aiResponses: [
navigationStore: useNavigationStore(),
messages: [
{
message: 'با عرض پوزش، در حال حاضر سخت‌افزار پردازش داده متصل نشده است. لطفاً با پشتیبانی فنی تماس بگیرید تا در اسرع وقت مشکل را برطرف کنیم.',
details: 'برای اتصال سخت‌افزار پردازش داده، نیاز به تنظیمات خاصی است که باید توسط تیم فنی انجام شود. این تنظیمات شامل:\n- اتصال به سرور پردازش\n- تنظیم پارامترهای امنیتی\n- راه‌اندازی ماژول‌های پردازشی\nمیباشد.'
},
{
message: 'متأسفانه در حال حاضر سیستم پردازش داده در دسترس نیست. این مشکل موقت است و به زودی برطرف خواهد شد.',
details: 'برای فعال‌سازی کامل سیستم، نیاز به انجام مراحل زیر است:\n- تأیید اتصال به سرور مرکزی\n- راه‌اندازی ماژول‌های پردازشی\n- تنظیم پارامترهای امنیتی\nلطفاً با پشتیبانی فنی تماس بگیرید.'
},
{
message: 'با کمال تأسف، سخت‌افزار پردازش داده هنوز آماده بهره‌برداری نیست. این مشکل به زودی برطرف خواهد شد.',
details: 'برای راه‌اندازی کامل سیستم، تیم فنی در حال انجام مراحل زیر است:\n- نصب و پیکربندی سرور پردازش\n- تنظیم پارامترهای امنیتی\n- راه‌اندازی ماژول‌های پردازشی\nلطفاً با پشتیبانی فنی تماس بگیرید.'
},
{
message: 'در حال حاضر سیستم پردازش داده در حالت تعمیر و نگهداری است. به زودی سرویس‌دهی از سر گرفته خواهد شد.',
details: 'برای فعال‌سازی مجدد سیستم، نیاز به انجام مراحل زیر است:\n- بررسی اتصال به سرور مرکزی\n- به‌روزرسانی ماژول‌های پردازشی\n- تنظیم مجدد پارامترهای امنیتی\nلطفاً با پشتیبانی فنی تماس بگیرید.'
},
{
message: 'با عرض پوزش، سخت‌افزار پردازش داده در حال حاضر غیرفعال است. تیم فنی در حال بررسی و رفع مشکل است.',
details: 'برای فعال‌سازی سیستم، نیاز به انجام مراحل زیر است:\n- تأیید اتصال به سرور پردازش\n- راه‌اندازی ماژول‌های پردازشی\n- تنظیم پارامترهای امنیتی\nلطفاً با پشتیبانی فنی تماس بگیرید.'
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'سلام! من دستیار هوشمند شما هستم. چطور می‌تونم کمکتون کنم؟' }] },
timestamp: new Date()
}
],
welcomePatterns: [
{
welcome: 'سلام! 👋',
thanks: 'از اینکه از هوش مصنوعی حسابیکس استفاده می‌کنید، بسیار خوشحالم! من یک هوش مصنوعی مستقل هستم که به صورت کامل در سرورهای داخلی حسابیکس میزبانی می‌شوم و نیازی به سرویس‌های خارجی ندارم.'
},
{
welcome: 'درود! 🌟',
thanks: 'به هوش مصنوعی حسابیکس خوش آمدید! من یک دستیار هوشمند مستقل هستم که به صورت کامل در سرورهای داخلی حسابیکس میزبانی می‌شوم و آماده خدمت‌رسانی به شما هستم.'
},
{
welcome: 'سلام و وقت بخیر! ✨',
thanks: 'خوشحالم که از هوش مصنوعی حسابیکس استفاده می‌کنید. من یک دستیار هوشمند مستقل هستم که به صورت کامل در سرورهای داخلی حسابیکس میزبانی می‌شوم و می‌توانم در زمینه‌های مختلف به شما کمک کنم.'
},
{
welcome: 'به حسابیکس خوش آمدید! 🚀',
thanks: 'من هوش مصنوعی مستقل حسابیکس هستم که به صورت کامل در سرورهای داخلی میزبانی می‌شوم. خوشحالم که می‌توانم به شما در استفاده از این نرم‌افزار کمک کنم.'
},
{
welcome: 'سلام! من دستیار هوشمند شما هستم 🤖',
thanks: 'به عنوان یک هوش مصنوعی مستقل که به صورت کامل در سرورهای داخلی حسابیکس میزبانی می‌شوم، آماده‌ام تا در هر زمینه‌ای که نیاز دارید به شما کمک کنم.'
}
],
selectedPattern: null,
capabilities: 'من می‌توانم به شما در موارد زیر کمک کنم:',
capabilitiesList: [
'ساخت گزارش‌های سفارشی با استفاده از هوش مصنوعی داخلی',
'ایجاد ماژول‌های جدید بدون نیاز به کدنویسی',
'پاسخ به سؤالات شما درباره نرم‌افزار با استفاده از دانش داخلی',
'راهنمایی در استفاده از امکانات مختلف با هوش مصنوعی مستقل',
'تجزیه و تحلیل داده‌های مالی با استفاده از الگوریتم‌های داخلی',
'پیش‌بینی روندهای مالی با استفاده از هوش مصنوعی اختصاصی'
],
prompt: 'لطفاً سؤال یا درخواست خود را در باکس زیر بنویسید. من با استفاده از هوش مصنوعی مستقل خود، به شما کمک خواهم کرد.',
displayWelcome: '',
displayThanks: '',
displayCapabilities: '',
displayCapabilitiesList: [],
displayPrompt: ''
}
},
async mounted() {
this.selectRandomPattern()
await this.startTypingAnimation()
},
watch: {
userMessages: {
handler() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
deep: true
},
displayWelcome() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
displayThanks() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
displayCapabilities() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
displayCapabilitiesList: {
handler() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
deep: true
},
displayPrompt() {
this.$nextTick(() => {
this.scrollToBottom()
})
newMessage: '',
isTyping: false,
aiEnabled: false,
aiStatus: 'checking',
conversationId: null,
quickSuggestions: [
'چطور می‌تونم کمکتون کنم؟',
'سوالی دارید؟',
'نیاز به راهنمایی دارید؟',
'مشکلی پیش اومده؟'
]
}
},
methods: {
selectRandomPattern() {
const randomIndex = Math.floor(Math.random() * this.welcomePatterns.length)
this.selectedPattern = this.welcomePatterns[randomIndex]
this.welcome = this.selectedPattern.welcome
this.thanks = this.selectedPattern.thanks
},
async startTypingAnimation() {
// تایپ پیام خوشآمدگویی
await this.typeText(this.welcome, (text) => {
this.displayWelcome = text
}, 15)
await this.delay(100)
// تایپ پیام تشکر
await this.typeText(this.thanks, (text) => {
this.displayThanks = text
}, 15)
await this.delay(100)
// تایپ معرفی قابلیتها
await this.typeText(this.capabilities, (text) => {
this.displayCapabilities = text
}, 15)
await this.delay(100)
// تایپ لیست قابلیتها
for (const capability of this.capabilitiesList) {
this.displayCapabilitiesList.push('')
await this.typeText(capability, (text) => {
this.displayCapabilitiesList[this.displayCapabilitiesList.length - 1] = text
}, 15)
await this.delay(50)
}
// تایپ پیام نهایی
await this.typeText(this.prompt, (text) => {
this.displayPrompt = text
}, 15)
this.isTyping = false
},
async typeText(text, callback, speed = 50) {
let currentText = ''
for (let i = 0; i < text.length; i++) {
currentText += text[i]
callback(currentText)
await this.delay(speed)
async checkAIStatus() {
try {
this.aiStatus = 'checking';
const response = await axios.get('/api/wizard/status');
const data = response.data;
if (data.success) {
this.aiEnabled = data.status === 'available';
this.aiStatus = data.status;
if (!this.aiEnabled) {
// تغییر پیام اولیه بر اساس وضعیت
this.messages[0] = {
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: this.getStatusMessage(data.status, data.message) }] },
timestamp: new Date()
};
}
} else {
this.aiEnabled = false;
this.aiStatus = 'error';
this.messages[0] = {
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'خطا در بررسی وضعیت هوش مصنوعی. لطفاً دوباره تلاش کنید.' }] },
timestamp: new Date()
};
}
} catch (error) {
this.aiEnabled = false;
this.aiStatus = 'error';
this.messages[0] = {
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید.' }] },
timestamp: new Date()
};
}
},
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
getStatusMessage(status, message) {
switch (status) {
case 'disabled':
return 'سرویس هوش مصنوعی در حال حاضر غیرفعال است. لطفاً با مدیر سیستم تماس بگیرید.';
case 'no_api_key':
return 'کلید API هوش مصنوعی تنظیم نشده است. لطفاً با مدیر سیستم تماس بگیرید.';
case 'error':
return 'خطا در سرویس هوش مصنوعی. لطفاً دوباره تلاش کنید.';
default:
return message || 'سرویس هوش مصنوعی در دسترس نیست.';
}
},
async sendMessage() {
if (!this.userMessage.trim()) return
const message = this.userMessage.trim()
this.userMessages.push(message)
this.userMessage = ''
this.isLoading = true
// انتخاب پاسخ رندوم
const randomResponse = this.aiResponses[Math.floor(Math.random() * this.aiResponses.length)]
// نمایش پاسخ اصلی
await this.delay(1000)
this.userMessages.push({
text: randomResponse.message,
isAI: true
})
// نمایش جزئیات
await this.delay(500)
this.userMessages.push({
text: randomResponse.details,
isAI: true,
isDetails: true
})
this.isLoading = false
this.$nextTick(() => {
this.scrollToBottom()
})
},
scrollToBottom() {
const container = this.$refs.messagesContainer
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
})
if (!this.newMessage.trim() || this.isTyping || !this.aiEnabled) return;
// اضافه کردن پیام کاربر
this.messages.push({
type: 'user',
text: this.newMessage.trim(),
timestamp: new Date()
});
const userMessage = this.newMessage.trim();
this.newMessage = '';
// اسکرول به پایین بعد از اضافه کردن پیام کاربر
setTimeout(() => {
this.scrollToBottom();
}, 100);
// شبیهسازی تایپ کردن AI
this.isTyping = true;
// اسکرول به پایین برای نشان دادن نشانگر تایپ
setTimeout(() => {
this.scrollToBottom();
}, 300);
try {
// ارسال پیام به سرور
const response = await axios.post('/api/wizard/talk', {
message: userMessage,
conversationId: this.conversationId || null
});
this.isTyping = false;
if (response.data.success) {
// --- تغییر: پردازش پاسخ JSON ---
let aiData = response.data.response;
let parsed = null;
try {
// اگر پاسخ داخل بلاک کد markdown است، فقط بخش json را جدا کن
if (typeof aiData === 'string' && aiData.trim().startsWith('```json')) {
aiData = aiData.replace(/^```json[\r\n]*/i, '').replace(/```$/i, '').trim();
}
// پارس چند مرحلهای تا رسیدن به آبجکت واقعی
parsed = aiData;
let safety = 0;
while (typeof parsed === 'string' && safety < 5) {
parsed = JSON.parse(parsed);
safety++;
}
// اگر باز هم data.data[0] رشته بود، دوباره پارس کن
if (
parsed &&
parsed.data &&
Array.isArray(parsed.data) &&
typeof parsed.data[0] === 'string'
) {
let safety2 = 0;
while (typeof parsed.data[0] === 'string' && safety2 < 5) {
parsed.data[0] = JSON.parse(parsed.data[0]);
safety2++;
}
}
} catch (e) {
// اگر JSON نبود، به صورت متن نمایش بده
parsed = { type: ['text'], data: [{ type: 'text', content: aiData }] };
}
console.debug('home.vue AI message parsed:', parsed);
this.messages.push({
type: 'ai',
data: parsed,
timestamp: new Date()
});
// ذخیره conversationId برای ادامه گفتگو
if (response.data.conversationId) {
this.conversationId = response.data.conversationId;
}
// نمایش اطلاعات هزینه در صورت وجود
if (response.data.cost) {
console.log('هزینه استفاده:', response.data.cost);
}
} else {
// نمایش خطا
this.messages.push({
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: `خطا: ${response.data.error}` }] },
timestamp: new Date()
});
}
// اسکرول به پایین بعد از دریافت پاسخ
setTimeout(() => {
this.scrollToBottom();
}, 200);
} catch (error) {
this.isTyping = false;
let errorMessage = 'خطا در ارتباط با سرور';
if (error.response) {
if (error.response.data && error.response.data.error) {
errorMessage = error.response.data.error;
} else if (error.response.status === 403) {
errorMessage = 'دسترسی غیرمجاز';
} else if (error.response.status === 500) {
errorMessage = 'خطای سرور';
}
} else if (error.request) {
errorMessage = 'خطا در اتصال به سرور';
}
this.messages.push({
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: errorMessage }] },
timestamp: new Date()
});
setTimeout(() => {
this.scrollToBottom();
}, 200);
}
},
async sendQuickMessage(suggestion) {
this.newMessage = suggestion;
await this.sendMessage();
},
generateAIResponse(userMessage) {
// این تابع دیگر استفاده نمیشود چون از API استفاده میکنیم
return 'پاسخ از سرور دریافت می‌شود';
},
formatTime(timestamp) {
return timestamp.toLocaleTimeString('fa-IR', {
hour: '2-digit',
minute: '2-digit'
});
},
scrollToBottom() {
this.$nextTick(() => {
setTimeout(() => {
const container = this.$refs.messagesContainer;
if (container) {
const rect = container.getBoundingClientRect();
const scrollTop = window.pageYOffset + rect.top + container.scrollHeight - window.innerHeight;
window.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
}
}, 100);
});
}
},
async mounted() {
// بستن منو در دسکتاپ
if (!this.$vuetify.display.mobile) {
this.navigationStore.closeDrawer();
}
// بررسی وضعیت هوش مصنوعی
await this.checkAIStatus();
this.scrollToBottom();
},
beforeUnmount() {
// باز کردن منو در دسکتاپ
if (!this.$vuetify.display.mobile) {
this.navigationStore.openDrawer();
}
},
updated() {
this.scrollToBottom();
}
}
</script>
<style scoped>
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.content-container {
flex: 1;
height: 100vh;
}
.chat-container {
height: 100%;
background-color: #f5f5f5;
}
.chat-box {
height: 100%;
display: flex;
flex-direction: column;
background: white;
}
.messages-container {
flex: 1;
padding: 24px;
padding-bottom: 160px;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
}
.message {
display: flex;
align-items: flex-start;
gap: 12px;
max-width: 80%;
}
.ai-message {
align-self: flex-start;
}
.user-message {
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message.ai {
align-self: flex-start;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.message.user .message-avatar {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
}
.message.ai .message-avatar {
background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%);
border: 2px solid #e0e0e0;
}
.message-content {
background-color: white;
padding: 12px 16px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
flex: 1;
}
.ai-message .message-content {
background-color: #e3f2fd;
border-bottom-right-radius: 4px;
.message-bubble {
background: #f8f9fa;
padding: 14px 18px;
border-radius: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
position: relative;
border: 1px solid #e9ecef;
}
.user-message .message-content {
background-color: #1a237e;
.message.user .message-bubble {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
color: white;
border-bottom-left-radius: 4px;
border: none;
}
.message-text {
font-size: 1rem;
margin: 0 0 4px 0;
line-height: 1.5;
font-size: 14px;
}
.input-container {
padding: 16px;
background-color: white;
border-top: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 12px;
}
.message-input {
background-color: #f5f5f5 !important;
border-radius: 24px;
padding: 8px 16px !important;
}
.message-input :deep(.v-field__input) {
padding: 8px !important;
font-size: 1rem;
color: #424242;
}
.send-button {
transition: all 0.3s ease;
}
.send-button:hover {
transform: scale(1.1);
.message-time {
font-size: 11px;
opacity: 0.7;
display: block;
}
.typing-indicator {
display: flex;
gap: 4px;
align-items: center;
height: 24px;
padding: 8px 0;
}
.typing-indicator span {
width: 8px;
height: 8px;
background-color: #1a237e;
background: #6c757d;
border-radius: 50%;
animation: typing 1s infinite ease-in-out;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.details-message {
background-color: #f5f5f5 !important;
border: 1px solid #e0e0e0;
font-size: 0.9rem;
color: #616161;
.input-container {
background: white;
padding: 20px 24px;
border-top: 1px solid #e9ecef;
box-shadow: 0 -4px 20px rgba(0,0,0,0.08);
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
}
.details-message .message-text {
white-space: pre-line;
.input-wrapper {
display: flex;
gap: 12px;
align-items: flex-end;
margin-bottom: 12px;
}
.message-input {
flex: 1;
}
.message-input :deep(.v-field__outline) {
border-radius: 24px;
border-color: #e9ecef;
}
.message-input :deep(.v-field--focused .v-field__outline) {
border-color: #1976d2;
}
.send-button {
border-radius: 50%;
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.3);
transition: all 0.3s ease;
}
.send-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.4);
}
.send-button .v-icon {
transform: rotate(180deg);
}
.quick-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.quick-chip {
cursor: pointer;
transition: all 0.3s ease;
border-color: #e9ecef;
color: #6c757d;
}
.quick-chip:hover {
background: #f8f9fa;
border-color: #1976d2;
color: #1976d2;
transform: translateY(-1px);
}
.status-indicator {
margin-top: 12px;
}
.status-alert {
border-radius: 12px;
font-size: 13px;
}
/* اسکرول‌بار سفارشی */
.messages-container::-webkit-scrollbar {
width: 6px;
}
@ -468,11 +579,27 @@ export default {
}
.messages-container::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
background: rgba(0,0,0,0.2);
border-radius: 3px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
background: rgba(0,0,0,0.3);
}
/* ریسپانسیو */
@media (max-width: 768px) {
.messages-container {
padding: 16px;
padding-bottom: 160px;
}
.input-container {
padding: 12px 16px;
}
.message {
max-width: 90%;
}
}
</style>