This commit is contained in:
Gloomy 2025-08-05 21:52:54 +00:00
commit da40826beb
37 changed files with 7778 additions and 1011 deletions

View file

@ -1,127 +0,0 @@
# هوش مصنوعی حسابیکس - یکپارچه‌سازی با اطلاعات اشخاص
## خلاصه
این پروژه قابلیت‌های جدیدی به سیستم هوش مصنوعی حسابیکس اضافه کرده است که به کاربران امکان دسترسی پویا به اطلاعات اشخاص را می‌دهد. هوش مصنوعی حالا می‌تواند به سوالات مربوط به اشخاص، موجودی‌ها و تراکنش‌های مالی پاسخ دهد.
## ویژگی‌های جدید
### 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

@ -21,6 +21,9 @@ framework:
#esi: true
#fragments: true
http_client:
default_options:
timeout: 30
php_errors:
log: true

View file

@ -40,6 +40,10 @@ services:
- '../src/Entity/'
- '../src/Kernel.php'
App\Controller\System\DebugController:
arguments:
$kernelLogsDir: '%kernel.logs_dir%'
doctrine.orm.default_attribute_driver:
class: Doctrine\ORM\Mapping\Driver\AttributeDriver
arguments:
@ -122,7 +126,37 @@ services:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
App\Cog\TicketService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$explore: '@App\Service\Explore'
$jdate: '@Jdate'
$registryMGR: '@registryMGR'
$sms: '@SMS'
$uploadDirectory: '%SupportFilesDir%'
App\Service\Explore: ~
App\AiTool\AccountingDocService:
arguments:
$em: '@doctrine.orm.entity_manager'
$cogAccountingDocService: '@App\Cog\AccountingDocService'
App\AiTool\TicketService:
arguments:
$em: '@doctrine.orm.entity_manager'
$cogTicketService: '@App\Cog\TicketService'
App\Service\AGI\AGIService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$registryMGR: '@registryMGR'
$log: '@Log'
$provider: '@Provider'
$promptService: '@App\Service\AGI\Promps\PromptService'
$httpClient: '@http_client'
$httpKernel: '@kernel'
$explore: '@App\Service\Explore'
$jdate: '@Jdate'
$sms: '@SMS'
$uploadDirectory: '%SupportFilesDir%'

View file

@ -14,24 +14,89 @@ final class Version20241201000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create postal_code_inquiry table';
return 'Create chat tables';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE postal_code_inquiry (
// Create chat_channel table
$this->addSql('CREATE TABLE chat_channel (
id INT AUTO_INCREMENT NOT NULL,
postal_code VARCHAR(10) NOT NULL,
address_data JSON NOT NULL,
name VARCHAR(255) NOT NULL,
description LONGTEXT DEFAULT NULL,
channel_id VARCHAR(50) NOT NULL,
is_public TINYINT(1) NOT NULL,
is_active TINYINT(1) NOT NULL,
created_by_id INT 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),
updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
avatar VARCHAR(255) DEFAULT NULL,
message_count INT NOT NULL,
last_message_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
UNIQUE INDEX UNIQ_CHANNEL_ID (channel_id),
INDEX IDX_CHANNEL_CREATED_BY (created_by_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
// Create chat_channel_member table
$this->addSql('CREATE TABLE chat_channel_member (
id INT AUTO_INCREMENT NOT NULL,
channel_id INT NOT NULL,
user_id INT NOT NULL,
is_admin TINYINT(1) NOT NULL,
is_active TINYINT(1) NOT NULL,
joined_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
last_seen_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
unread_count INT NOT NULL,
INDEX IDX_MEMBER_CHANNEL (channel_id),
INDEX IDX_MEMBER_USER (user_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
// Create chat_message table
$this->addSql('CREATE TABLE chat_message (
id INT AUTO_INCREMENT NOT NULL,
channel_id INT NOT NULL,
sender_id INT NOT NULL,
content LONGTEXT NOT NULL,
message_type VARCHAR(20) NOT NULL,
sent_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
edited_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
is_edited TINYINT(1) NOT NULL,
is_deleted TINYINT(1) NOT NULL,
quoted_message_id INT DEFAULT NULL,
attachments JSON DEFAULT NULL,
reactions JSON DEFAULT NULL,
reply_count INT NOT NULL,
view_count INT NOT NULL,
INDEX IDX_MESSAGE_CHANNEL (channel_id),
INDEX IDX_MESSAGE_SENDER (sender_id),
INDEX IDX_MESSAGE_QUOTED (quoted_message_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
// Add foreign key constraints
$this->addSql('ALTER TABLE chat_channel ADD CONSTRAINT FK_CHANNEL_CREATED_BY FOREIGN KEY (created_by_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE chat_channel_member ADD CONSTRAINT FK_MEMBER_CHANNEL FOREIGN KEY (channel_id) REFERENCES chat_channel (id)');
$this->addSql('ALTER TABLE chat_channel_member ADD CONSTRAINT FK_MEMBER_USER FOREIGN KEY (user_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE chat_message ADD CONSTRAINT FK_MESSAGE_CHANNEL FOREIGN KEY (channel_id) REFERENCES chat_channel (id)');
$this->addSql('ALTER TABLE chat_message ADD CONSTRAINT FK_MESSAGE_SENDER FOREIGN KEY (sender_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE chat_message ADD CONSTRAINT FK_MESSAGE_QUOTED FOREIGN KEY (quoted_message_id) REFERENCES chat_message (id)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE postal_code_inquiry');
// Drop foreign key constraints
$this->addSql('ALTER TABLE chat_message DROP FOREIGN KEY FK_MESSAGE_QUOTED');
$this->addSql('ALTER TABLE chat_message DROP FOREIGN KEY FK_MESSAGE_SENDER');
$this->addSql('ALTER TABLE chat_message DROP FOREIGN KEY FK_MESSAGE_CHANNEL');
$this->addSql('ALTER TABLE chat_channel_member DROP FOREIGN KEY FK_MEMBER_USER');
$this->addSql('ALTER TABLE chat_channel_member DROP FOREIGN KEY FK_MEMBER_CHANNEL');
$this->addSql('ALTER TABLE chat_channel DROP FOREIGN KEY FK_CHANNEL_CREATED_BY');
// Drop tables
$this->addSql('DROP TABLE chat_message');
$this->addSql('DROP TABLE chat_channel_member');
$this->addSql('DROP TABLE chat_channel');
}
}

View file

@ -0,0 +1,40 @@
<?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 Version20241220000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add memberCount field to chat_channel table';
}
public function up(Schema $schema): void
{
// Add memberCount column to chat_channel table
$this->addSql('ALTER TABLE chat_channel ADD member_count INT NOT NULL DEFAULT 0');
// Update existing channels with correct member count
$this->addSql('
UPDATE chat_channel c
SET member_count = (
SELECT COUNT(*)
FROM chat_channel_member m
WHERE m.channel_id = c.id AND m.is_active = 1
)
');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chat_channel DROP member_count');
}
}

View file

@ -0,0 +1,31 @@
<?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 Version20250804133410 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE commodity CHANGE code code VARCHAR(255) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE commodity CHANGE code code BIGINT NOT NULL');
}
}

View file

@ -3,13 +3,115 @@
namespace App\AiTool;
use App\Cog\TicketService as CogTicketService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class TicketService
{
public function __construct(
private readonly CogTicketService $cogTicketService
) {
private EntityManagerInterface $em;
private CogTicketService $cogTicketService;
public function __construct(EntityManagerInterface $em, CogTicketService $cogTicketService)
{
$this->em = $em;
$this->cogTicketService = $cogTicketService;
}
/**
* دریافت لیست تیکت‌ها برای ابزار هوش مصنوعی
*/
public function getTicketsListAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق دریافت لیست تیکت‌ها پیاده‌سازی شود
// فعلاً یک پیام موقت برمی‌گردانیم
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت لیست تیکت‌ها: ' . $e->getMessage()
];
}
}
/**
* دریافت اطلاعات تیکت بر اساس کد
*/
public function getTicketInfoByCode($code, $acc): array
{
if (!$code) {
return [
'error' => 'کد تیکت الزامی است'
];
}
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق دریافت اطلاعات تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت اطلاعات تیکت: ' . $e->getMessage()
];
}
}
/**
* افزودن یا ویرایش تیکت برای ابزار هوش مصنوعی
*/
public function addOrUpdateTicketAi(array $params, $acc = null, $code = 0): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق افزودن/ویرایش تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در افزودن/ویرایش تیکت: ' . $e->getMessage()
];
}
}
/**
* پاسخ به تیکت برای ابزار هوش مصنوعی
*/
public function replyToTicketAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق پاسخ به تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در پاسخ به تیکت: ' . $e->getMessage()
];
}
}
/**

View file

@ -86,6 +86,7 @@ class PersonService
$search = $params['search'] ?? '';
$types = $params['types'] ?? null;
$transactionFilters = $params['transactionFilters'] ?? null;
$sortBy = $params['sortBy'] ?? null;
$queryBuilder = $this->entityManager->getRepository(Person::class)
->createQueryBuilder('p')
@ -104,18 +105,66 @@ class PersonService
->setParameter('types', $types);
}
// بررسی اینکه آیا سورت روی فیلدهای محاسبه‌شده است
$hasCalculatedSort = false;
$calculatedSortField = null;
$calculatedSortOrder = null;
if ($sortBy && is_array($sortBy) && !empty($sortBy)) {
foreach ($sortBy as $sort) {
if (isset($sort['key']) && in_array($sort['key'], ['bs', 'bd', 'balance'])) {
$hasCalculatedSort = true;
$calculatedSortField = $sort['key'];
$calculatedSortOrder = $sort['order'];
break;
}
}
}
// اگر سورت روی فیلدهای محاسبه‌شده است، ابتدا همه داده‌ها را دریافت کن
if ($hasCalculatedSort) {
$persons = $queryBuilder
->select('p')
->getQuery()
->getResult();
} else {
// اعمال سورت کردن برای فیلدهای مستقیم
if ($sortBy && is_array($sortBy) && !empty($sortBy)) {
foreach ($sortBy as $sort) {
if (isset($sort['key']) && isset($sort['order'])) {
$field = $sort['key'];
$order = strtoupper($sort['order']);
// بررسی فیلدهای مجاز برای سورت
$allowedFields = [
'code', 'nikename', 'name', 'birthday', 'company',
'shenasemeli', 'codeeghtesadi', 'sabt', 'keshvar',
'ostan', 'shahr', 'postalcode', 'tel', 'mobile',
'mobile2', 'email', 'website', 'fax', 'speedAccess'
];
if (in_array($field, $allowedFields)) {
$queryBuilder->addOrderBy("p.$field", $order);
}
}
}
} else {
// سورت پیش‌فرض بر اساس کد
$queryBuilder->orderBy('p.code', 'ASC');
}
$persons = $queryBuilder
->select('p')
->setFirstResult(($page - 1) * $itemsPerPage)
->setMaxResults($itemsPerPage)
->getQuery()
->getResult();
}
$totalItems = (clone $queryBuilder)
->select('COUNT(p.id)')
->getQuery()
->getSingleScalarResult();
$persons = $queryBuilder
->select('p')
->setFirstResult(($page - 1) * $itemsPerPage)
->setMaxResults($itemsPerPage)
->getQuery()
->getResult();
$response = [];
foreach ($persons as $person) {
$rows = $this->entityManager->getRepository(HesabdariRow::class)->findBy([
@ -158,10 +207,27 @@ class PersonService
}
}
// اگر سورت روی فیلدهای محاسبه‌شده است، اینجا سورت کن
if ($hasCalculatedSort && $calculatedSortField && $calculatedSortOrder) {
usort($response, function($a, $b) use ($calculatedSortField, $calculatedSortOrder) {
$aVal = $a[$calculatedSortField] ?? 0;
$bVal = $b[$calculatedSortField] ?? 0;
if ($calculatedSortOrder === 'ASC') {
return $aVal <=> $bVal;
} else {
return $bVal <=> $aVal;
}
});
// اعمال صفحه‌بندی بعد از سورت
$response = array_slice($response, ($page - 1) * $itemsPerPage, $itemsPerPage);
}
$filteredTotal = count($response);
return [
'items' => array_slice($response, 0, $itemsPerPage),
'items' => $response,
'total' => $filteredTotal,
'unfilteredTotal' => $totalItems,
];

View file

@ -0,0 +1,672 @@
<?php
namespace App\Controller;
use App\Entity\ChatChannel;
use App\Entity\ChatMessage;
use App\Entity\User;
use App\Repository\ChatChannelRepository;
use App\Repository\ChatMessageRepository;
use App\Repository\UserRepository;
use App\Service\ChatService;
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\Bundle\SecurityBundle\Security;
#[Route('/api/chat')]
class ChatController extends AbstractController
{
public function __construct(
private ChatService $chatService,
private EntityManagerInterface $entityManager,
private ChatChannelRepository $channelRepository,
private ChatMessageRepository $messageRepository,
private UserRepository $userRepository,
private Security $security
) {}
#[Route('/channels', name: 'chat_channels', methods: ['GET'])]
public function getUserChannels(): JsonResponse
{
/** @var User $user */
$user = $this->security->getUser();
if (!$user) {
return $this->json([
'success' => false,
'message' => 'کاربر احراز هویت نشده است'
], Response::HTTP_UNAUTHORIZED);
}
$channels = $this->chatService->getUserChannels($user);
$data = [];
foreach ($channels as $channel) {
$data[] = [
'id' => $channel->getId(),
'channelId' => $channel->getChannelId(),
'name' => $channel->getName(),
'description' => $channel->getDescription(),
'isPublic' => $channel->isPublic(),
'avatar' => $channel->getAvatar(),
'messageCount' => $channel->getMessageCount(),
'memberCount' => $channel->getMemberCount(),
'lastMessageAt' => $channel->getLastMessageAt()?->format('Y-m-d H:i:s'),
'createdAt' => $channel->getCreatedAt()->format('Y-m-d H:i:s'),
'isAdmin' => $this->chatService->isUserAdmin($channel, $user),
];
}
return $this->json([
'success' => true,
'data' => $data
]);
}
#[Route('/channels', name: 'chat_create_channel', methods: ['POST'])]
public function createChannel(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
if (!isset($data['name']) || empty($data['name'])) {
return $this->json([
'success' => false,
'message' => 'نام کانال الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
try {
$channel = $this->chatService->createChannel(
$data['name'],
$data['description'] ?? '',
$data['isPublic'] ?? true,
$user
);
return $this->json([
'success' => true,
'data' => [
'id' => $channel->getId(),
'channelId' => $channel->getChannelId(),
'name' => $channel->getName(),
'description' => $channel->getDescription(),
'isPublic' => $channel->isPublic(),
]
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/search', name: 'chat_search_channels', methods: ['GET'])]
public function searchChannels(Request $request): JsonResponse
{
$searchTerm = $request->query->get('q', '');
if (empty($searchTerm)) {
// Return popular public channels when search term is empty
$channels = $this->chatService->getPopularPublicChannels(10);
} else {
$channels = $this->chatService->searchPublicChannels($searchTerm);
}
$data = [];
foreach ($channels as $channel) {
$data[] = [
'id' => $channel->getId(),
'channelId' => $channel->getChannelId(),
'name' => $channel->getName(),
'description' => $channel->getDescription(),
'isPublic' => $channel->isPublic(),
'messageCount' => $channel->getMessageCount(),
'memberCount' => $channel->getMemberCount(),
'lastMessageAt' => $channel->getLastMessageAt()?->format('Y-m-d H:i:s'),
];
}
return $this->json([
'success' => true,
'data' => $data
]);
}
#[Route('/channels/{channelId}/join', name: 'chat_join_channel', methods: ['POST'])]
public function joinChannel(string $channelId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $user */
$user = $this->security->getUser();
try {
$success = $this->chatService->joinChannel($channel, $user);
if ($success) {
return $this->json([
'success' => true,
'message' => 'با موفقیت به کانال پیوستید'
]);
} else {
return $this->json([
'success' => false,
'message' => 'قبلاً عضو این کانال هستید'
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/{channelId}/members', name: 'chat_add_member', methods: ['POST'])]
public function addMember(string $channelId, Request $request): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['userId']) || empty($data['userId'])) {
return $this->json([
'success' => false,
'message' => 'شناسه کاربر الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $admin */
$admin = $this->security->getUser();
// Check if admin is actually an admin of this channel
if (!$this->chatService->isUserAdmin($channel, $admin)) {
return $this->json([
'success' => false,
'message' => 'شما دسترسی لازم برای اضافه کردن عضو ندارید'
], Response::HTTP_FORBIDDEN);
}
$user = $this->userRepository->find($data['userId']);
if (!$user) {
return $this->json([
'success' => false,
'message' => 'کاربر یافت نشد'
], Response::HTTP_NOT_FOUND);
}
try {
$success = $this->chatService->addMemberToChannel($channel, $user, $admin);
if ($success) {
return $this->json([
'success' => true,
'message' => 'عضو با موفقیت به کانال اضافه شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'کاربر قبلاً عضو این کانال است'
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/{channelId}/members/{userId}', name: 'chat_remove_member', methods: ['DELETE'])]
public function removeMember(string $channelId, int $userId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $admin */
$admin = $this->security->getUser();
// Check if admin is actually an admin of this channel
if (!$this->chatService->isUserAdmin($channel, $admin)) {
return $this->json([
'success' => false,
'message' => 'شما دسترسی لازم برای حذف عضو ندارید'
], Response::HTTP_FORBIDDEN);
}
$user = $this->userRepository->find($userId);
if (!$user) {
return $this->json([
'success' => false,
'message' => 'کاربر یافت نشد'
], Response::HTTP_NOT_FOUND);
}
try {
$success = $this->chatService->removeMemberFromChannel($channel, $user, $admin);
if ($success) {
return $this->json([
'success' => true,
'message' => 'عضو با موفقیت از کانال حذف شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'کاربر عضو این کانال نیست'
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/{channelId}/members', name: 'chat_get_members', methods: ['GET'])]
public function getChannelMembers(string $channelId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $user */
$user = $this->security->getUser();
// Check if user is member
if (!$this->chatService->isUserMember($channel, $user)) {
return $this->json([
'success' => false,
'message' => 'شما عضو این کانال نیستید'
], Response::HTTP_FORBIDDEN);
}
$members = $this->chatService->getChannelMembers($channel);
$data = [];
foreach ($members as $member) {
$data[] = [
'id' => $member->getUser()->getId(),
'fullName' => $member->getUser()->getFullName(),
'email' => $member->getUser()->getEmail(),
'isAdmin' => $member->isAdmin(),
'joinedAt' => $member->getJoinedAt()->format('Y-m-d H:i:s'),
];
}
return $this->json([
'success' => true,
'data' => $data
]);
}
#[Route('/channels/{channelId}/leave', name: 'chat_leave_channel', methods: ['POST'])]
public function leaveChannel(string $channelId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $user */
$user = $this->security->getUser();
try {
$success = $this->chatService->leaveChannel($channel, $user);
if ($success) {
return $this->json([
'success' => true,
'message' => 'با موفقیت از کانال خارج شدید'
]);
} else {
return $this->json([
'success' => false,
'message' => 'شما عضو این کانال نیستید'
], Response::HTTP_BAD_REQUEST);
}
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/channels/{channelId}/messages', name: 'chat_channel_messages', methods: ['GET'])]
public function getChannelMessages(string $channelId, Request $request): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
/** @var User $user */
$user = $this->security->getUser();
// For private channels, check if user is member
if (!$channel->isPublic() && !$this->chatService->isUserMember($channel, $user)) {
return $this->json([
'success' => false,
'message' => 'شما عضو این کانال نیستید'
], Response::HTTP_FORBIDDEN);
}
$limit = (int) $request->query->get('limit', 30);
$offset = (int) $request->query->get('offset', 0);
$messages = $this->chatService->getChannelMessages($channel, $limit, $offset);
// Get total message count for pagination info
$totalMessages = $this->chatService->getChannelMessageCount($channel);
$data = [];
foreach ($messages as $message) {
$data[] = [
'id' => $message->getId(),
'content' => $message->getContent(),
'messageType' => $message->getMessageType(),
'sentAt' => $message->getSentAt()->format('Y-m-d H:i:s'),
'isEdited' => $message->isEdited(),
'editedAt' => $message->getEditedAt()?->format('Y-m-d H:i:s'),
'sender' => [
'id' => $message->getSender()->getId(),
'fullName' => $message->getSender()->getFullName(),
'email' => $message->getSender()->getEmail(),
],
'quotedMessage' => $message->getQuotedMessage() ? [
'id' => $message->getQuotedMessage()->getId(),
'content' => $message->getQuotedMessage()->getContent(),
'sender' => $message->getQuotedMessage()->getSender()->getFullName(),
] : null,
'reactions' => $message->getReactions() ?: [],
'attachments' => $message->getAttachments(),
];
}
return $this->json([
'success' => true,
'data' => $data,
'pagination' => [
'limit' => $limit,
'offset' => $offset,
'total' => $totalMessages,
'hasMore' => ($offset + $limit) < $totalMessages
]
]);
}
#[Route('/channels/{channelId}/messages', name: 'chat_send_message', methods: ['POST'])]
public function sendMessage(string $channelId, Request $request): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['content']) || empty($data['content'])) {
return $this->json([
'success' => false,
'message' => 'متن پیام الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
// Check if user is member (required for sending messages)
if (!$this->chatService->isUserMember($channel, $user)) {
return $this->json([
'success' => false,
'message' => 'برای ارسال پیام باید عضو کانال باشید'
], Response::HTTP_FORBIDDEN);
}
try {
$quotedMessage = null;
if (isset($data['quotedMessageId'])) {
$quotedMessage = $this->messageRepository->find($data['quotedMessageId']);
}
$message = $this->chatService->sendMessage(
$channel,
$user,
$data['content'],
$data['messageType'] ?? 'text',
$quotedMessage
);
return $this->json([
'success' => true,
'data' => [
'id' => $message->getId(),
'content' => $message->getContent(),
'messageType' => $message->getMessageType(),
'sentAt' => $message->getSentAt()->format('Y-m-d H:i:s'),
'sender' => [
'id' => $message->getSender()->getId(),
'fullName' => $message->getSender()->getFullName(),
],
'quotedMessage' => $message->getQuotedMessage() ? [
'id' => $message->getQuotedMessage()->getId(),
'content' => $message->getQuotedMessage()->getContent(),
'sender' => $message->getQuotedMessage()->getSender()->getFullName(),
] : null,
]
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/messages/{messageId}/edit', name: 'chat_edit_message', methods: ['PUT'])]
public function editMessage(int $messageId, Request $request): JsonResponse
{
$message = $this->messageRepository->find($messageId);
if (!$message) {
return $this->json([
'success' => false,
'message' => 'پیام یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['content']) || empty($data['content'])) {
return $this->json([
'success' => false,
'message' => 'متن پیام الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
$success = $this->chatService->editMessage($message, $user, $data['content']);
if ($success) {
return $this->json([
'success' => true,
'message' => 'پیام با موفقیت ویرایش شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'شما نمی‌توانید این پیام را ویرایش کنید'
], Response::HTTP_FORBIDDEN);
}
}
#[Route('/messages/{messageId}/reactions', name: 'chat_add_reaction', methods: ['POST'])]
public function addReaction(int $messageId, Request $request): JsonResponse
{
$message = $this->messageRepository->find($messageId);
if (!$message) {
return $this->json([
'success' => false,
'message' => 'پیام یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['emoji']) || empty($data['emoji'])) {
return $this->json([
'success' => false,
'message' => 'ایموجی الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
$success = $this->chatService->addReaction($message, $user, $data['emoji']);
if ($success) {
return $this->json([
'success' => true,
'message' => 'واکنش اضافه شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'خطا در اضافه کردن واکنش'
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/messages/{messageId}/reactions', name: 'chat_remove_reaction', methods: ['DELETE'])]
public function removeReaction(int $messageId, Request $request): JsonResponse
{
$message = $this->messageRepository->find($messageId);
if (!$message) {
return $this->json([
'success' => false,
'message' => 'پیام یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$data = json_decode($request->getContent(), true);
if (!isset($data['emoji']) || empty($data['emoji'])) {
return $this->json([
'success' => false,
'message' => 'ایموجی الزامی است'
], Response::HTTP_BAD_REQUEST);
}
/** @var User $user */
$user = $this->security->getUser();
$success = $this->chatService->removeReaction($message, $user, $data['emoji']);
if ($success) {
return $this->json([
'success' => true,
'message' => 'واکنش حذف شد'
]);
} else {
return $this->json([
'success' => false,
'message' => 'خطا در حذف واکنش'
], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/users/search', name: 'chat_search_users', methods: ['GET'])]
public function searchUsers(Request $request): JsonResponse
{
$searchTerm = $request->query->get('q', '');
if (empty($searchTerm)) {
return $this->json([
'success' => false,
'message' => 'عبارت جستجو الزامی است'
], Response::HTTP_BAD_REQUEST);
}
$users = $this->chatService->searchUsers($searchTerm);
$data = [];
foreach ($users as $user) {
$data[] = [
'id' => $user->getId(),
'fullName' => $user->getFullName(),
'email' => $user->getEmail(),
];
}
return $this->json([
'success' => true,
'data' => $data
]);
}
#[Route('/channels/{channelId}/stats', name: 'chat_channel_stats', methods: ['GET'])]
public function getChannelStats(string $channelId): JsonResponse
{
$channel = $this->channelRepository->findByChannelId($channelId);
if (!$channel) {
return $this->json([
'success' => false,
'message' => 'کانال یافت نشد'
], Response::HTTP_NOT_FOUND);
}
$stats = $this->chatService->getChannelStats($channel);
$messageStats = $this->chatService->getMessageStats($channel);
return $this->json([
'success' => true,
'data' => [
'memberCount' => $stats['memberCount'],
'messageCount' => $stats['messageCount'],
'messageStats' => $messageStats,
]
]);
}
}

View file

@ -14,6 +14,7 @@ use App\Service\Notification;
use App\Service\Provider;
use App\Service\registryMGR;
use App\Service\SMS;
use App\AiTool\TicketService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

View file

@ -0,0 +1,684 @@
<?php
namespace App\Controller\System;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\KernelInterface;
#[Route('/api/admin/debug')]
class DebugController extends AbstractController
{
private string $logsDir;
private Filesystem $filesystem;
private string $environment;
public function __construct(string $kernelLogsDir, KernelInterface $kernel)
{
$this->logsDir = $kernelLogsDir;
$this->filesystem = new Filesystem();
$this->environment = $kernel->getEnvironment();
}
#[Route('/logs', name: 'debug_logs_list', methods: ['GET'])]
public function getLogs(Request $request): JsonResponse
{
try {
$page = (int) $request->query->get('page', 1);
$limit = (int) $request->query->get('limit', 50);
$search = (string) $request->query->get('search', '');
$level = (string) $request->query->get('level', '');
$date = (string) $request->query->get('date', '');
// Handle sorting parameters safely
$sortBy = 'timestamp';
$sortDesc = true;
// Get sortBy parameter safely
$sortByParam = $request->query->get('sortBy');
if (is_string($sortByParam) && !empty($sortByParam)) {
$sortBy = $sortByParam;
}
// Get sortDesc parameter safely
$sortDescParam = $request->query->get('sortDesc');
if (is_string($sortDescParam)) {
$sortDesc = $sortDescParam === 'true' || $sortDescParam === '1';
} elseif (is_bool($sortDescParam)) {
$sortDesc = $sortDescParam;
}
// محدود کردن تعداد آیتم‌ها برای جلوگیری از مصرف حافظه زیاد
$limit = min($limit, 100);
$logs = $this->parseLogFilesOptimized($page, $limit, $search, $level, $date, $sortBy, $sortDesc);
return $this->json([
'success' => true,
'data' => $logs['items'],
'total' => $logs['total'],
'page' => $page,
'limit' => $limit,
'totalPages' => ceil($logs['total'] / $limit),
'environment' => $this->environment
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در دریافت لاگ‌ها: ' . $e->getMessage()
], 500);
}
}
#[Route('/logs/{id}', name: 'debug_log_detail', methods: ['GET'])]
public function getLogDetail(string $id): JsonResponse
{
try {
$logDetail = $this->getLogDetailById($id);
if (!$logDetail) {
return $this->json([
'success' => false,
'message' => 'لاگ مورد نظر یافت نشد'
], 404);
}
return $this->json([
'success' => true,
'data' => $logDetail
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در دریافت جزئیات لاگ: ' . $e->getMessage()
], 500);
}
}
#[Route('/logs', name: 'debug_logs_delete', methods: ['DELETE'])]
public function deleteLogs(Request $request): JsonResponse
{
try {
$data = json_decode($request->getContent(), true);
$logIds = $data['ids'] ?? [];
$deleteAll = $data['deleteAll'] ?? false;
if ($deleteAll) {
$this->clearAllLogs();
return $this->json([
'success' => true,
'message' => 'تمام لاگ‌ها با موفقیت حذف شدند'
]);
}
if (empty($logIds)) {
return $this->json([
'success' => false,
'message' => 'هیچ لاگی برای حذف انتخاب نشده'
], 400);
}
$deletedCount = $this->deleteLogsByIds($logIds);
return $this->json([
'success' => true,
'message' => "{$deletedCount} لاگ با موفقیت حذف شد",
'deletedCount' => $deletedCount
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در حذف لاگ‌ها: ' . $e->getMessage()
], 500);
}
}
#[Route('/logs/export', name: 'debug_logs_export', methods: ['GET'])]
public function exportLogs(Request $request): JsonResponse
{
try {
$format = $request->query->get('format', 'json');
$date = $request->query->get('date', '');
$level = $request->query->get('level', '');
$logs = $this->getLogsForExport($date, $level);
if ($format === 'csv') {
$csvData = $this->convertToCsv($logs);
return new JsonResponse($csvData, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="logs_' . $this->environment . '_' . date('Y-m-d') . '.csv"'
]);
}
return $this->json([
'success' => true,
'data' => $logs,
'total' => count($logs),
'environment' => $this->environment
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در صادرات لاگ‌ها: ' . $e->getMessage()
], 500);
}
}
#[Route('/system-info', name: 'debug_system_info', methods: ['GET'])]
public function getSystemInfo(): JsonResponse
{
try {
$info = [
'environment' => $this->environment,
'php_version' => PHP_VERSION,
'symfony_version' => \Symfony\Component\HttpKernel\Kernel::VERSION,
'memory_usage' => memory_get_usage(true),
'memory_peak' => memory_get_peak_usage(true),
'disk_free_space' => disk_free_space($this->logsDir),
'disk_total_space' => disk_total_space($this->logsDir),
'log_files_count' => $this->getLogFilesCount(),
'log_files_size' => $this->getLogFilesSize(),
'last_error_log' => $this->getLastErrorLog(),
'server_info' => [
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'php_sapi' => php_sapi_name(),
'max_execution_time' => ini_get('max_execution_time'),
'memory_limit' => ini_get('memory_limit'),
]
];
return $this->json([
'success' => true,
'data' => $info
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات سیستم: ' . $e->getMessage()
], 500);
}
}
private function parseLogFilesOptimized(int $page, int $limit, string $search, string $level, string $date, string $sortBy = 'timestamp', bool $sortDesc = true): array
{
$finder = new Finder();
// فقط فایل‌های لاگ مربوط به محیط فعلی را پیدا کن
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
// فایل‌های مربوط به محیط فعلی
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
})
->sortByModifiedTime();
$allLogs = [];
$id = 1;
$maxLogs = 5000; // محدود کردن تعداد کل لاگ‌ها برای جلوگیری از مصرف حافظه
foreach ($finder as $file) {
// بررسی اندازه فایل قبل از خواندن
if ($file->getSize() > 50 * 1024 * 1024) { // فایل‌های بزرگتر از 50MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
$lineNumber = 0;
while (($line = fgets($handle)) !== false && count($allLogs) < $maxLogs) {
$lineNumber++;
// محدود کردن تعداد خطوط خوانده شده
if ($lineNumber > 10000) {
break;
}
if (empty(trim($line))) continue;
$logEntry = $this->parseLogLine($line, $file->getFilename(), $id++);
if ($logEntry) {
// فیلتر بر اساس جستجو
if ($search && !$this->matchesSearch($logEntry, $search)) {
continue;
}
// فیلتر بر اساس سطح
if ($level && $logEntry['level'] !== $level) {
continue;
}
// فیلتر بر اساس تاریخ
if ($date && $logEntry['date'] !== $date) {
continue;
}
$allLogs[] = $logEntry;
}
// بررسی مصرف حافظه
if (memory_get_usage() > 100 * 1024 * 1024) { // بیش از 100MB
break 2;
}
}
fclose($handle);
}
// اعمال مرتب‌سازی
usort($allLogs, function($a, $b) use ($sortBy, $sortDesc) {
$aValue = $a[$sortBy] ?? '';
$bValue = $b[$sortBy] ?? '';
// برای تاریخ و زمان، از timestamp استفاده کن
if ($sortBy === 'timestamp') {
$aValue = strtotime($aValue);
$bValue = strtotime($bValue);
}
if ($sortDesc) {
return $aValue < $bValue ? 1 : -1;
} else {
return $aValue > $bValue ? 1 : -1;
}
});
$totalCount = count($allLogs);
$offset = ($page - 1) * $limit;
$items = array_slice($allLogs, $offset, $limit);
return [
'items' => $items,
'total' => $totalCount
];
}
private function parseLogLine(string $line, string $filename, int $id): ?array
{
// فرمت لاگ Symfony: [timestamp] level: message {"context"} []
$pattern = '/^\[([^\]]+)\]\s+([^:]+):\s+(.+?)(?:\s+\{([^}]*)\}\s+\[\])?$/';
if (preg_match($pattern, $line, $matches)) {
$timestamp = $matches[1];
$level = strtoupper(trim($matches[2]));
$message = trim($matches[3]);
$context = isset($matches[4]) ? $matches[4] : '';
// محدود کردن طول پیام
if (strlen($message) > 1000) {
$message = substr($message, 0, 1000) . '...';
}
// پردازش context اگر وجود داشته باشد
$extra = [];
if (!empty($context)) {
// تلاش برای پارس کردن context به عنوان JSON
$contextData = json_decode('{' . $context . '}', true);
if ($contextData) {
$extra = $contextData;
} else {
$extra = ['context' => $context];
}
}
return [
'id' => $id,
'timestamp' => $timestamp,
'date' => date('Y-m-d', strtotime($timestamp)),
'time' => date('H:i:s', strtotime($timestamp)),
'level' => $level,
'message' => $message,
'filename' => $filename,
'environment' => $this->environment,
'extra' => $extra,
'raw' => substr($line, 0, 500)
];
}
// اگر فرمت استاندارد تطبیق نکرد، تلاش برای فرمت‌های دیگر
$patterns = [
// فرمت JSON
'/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)\s+(\w+)\s+(.+)$/',
// فرمت استاندارد
'/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+): (.+)$/',
// فرمت ساده
'/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(\w+)\s+(.+)$/'
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $line, $matches)) {
$timestamp = $matches[1];
$level = strtoupper($matches[2]);
$message = $matches[3];
// محدود کردن طول پیام
if (strlen($message) > 1000) {
$message = substr($message, 0, 1000) . '...';
}
// استخراج اطلاعات اضافی از JSON
$extra = [];
if (strpos($message, '{') === 0) {
$jsonData = json_decode($message, true);
if ($jsonData) {
$message = $jsonData['message'] ?? $message;
$extra = $jsonData;
}
}
return [
'id' => $id,
'timestamp' => $timestamp,
'date' => date('Y-m-d', strtotime($timestamp)),
'time' => date('H:i:s', strtotime($timestamp)),
'level' => $level,
'message' => $message,
'filename' => $filename,
'environment' => $this->environment,
'extra' => $extra,
'raw' => substr($line, 0, 500)
];
}
}
// اگر هیچ الگویی تطبیق نکرد، لاگ را با اطلاعات حداقلی برگردان
return [
'id' => $id,
'timestamp' => date('Y-m-d H:i:s'),
'date' => date('Y-m-d'),
'time' => date('H:i:s'),
'level' => 'UNKNOWN',
'message' => substr($line, 0, 500),
'filename' => $filename,
'environment' => $this->environment,
'extra' => [],
'raw' => substr($line, 0, 500)
];
}
private function matchesSearch(array $logEntry, string $search): bool
{
$search = strtolower($search);
return strpos(strtolower($logEntry['message']), $search) !== false ||
strpos(strtolower($logEntry['level']), $search) !== false ||
strpos(strtolower($logEntry['filename']), $search) !== false;
}
private function getLogDetailById(string $id): ?array
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
foreach ($finder as $file) {
if ($file->getSize() > 10 * 1024 * 1024) { // فایل‌های بزرگتر از 10MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
$lineId = 1;
while (($line = fgets($handle)) !== false) {
if (empty(trim($line))) {
$lineId++;
continue;
}
if ($lineId == $id) {
fclose($handle);
return $this->parseLogLine($line, $file->getFilename(), $lineId);
}
$lineId++;
}
fclose($handle);
}
return null;
}
private function deleteLogsByIds(array $ids): int
{
$deletedCount = 0;
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
foreach ($finder as $file) {
if ($file->getSize() > 50 * 1024 * 1024) { // فایل‌های بزرگتر از 50MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
$lines = [];
$lineId = 1;
$fileModified = false;
while (($line = fgets($handle)) !== false) {
if (!in_array($lineId, $ids)) {
$lines[] = $line;
} else {
$deletedCount++;
$fileModified = true;
}
$lineId++;
}
fclose($handle);
if ($fileModified) {
$this->filesystem->dumpFile($file->getPathname(), implode('', $lines));
}
}
return $deletedCount;
}
private function clearAllLogs(): void
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
foreach ($finder as $file) {
$this->filesystem->remove($file->getPathname());
}
}
private function getLogsForExport(string $date, string $level): array
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
$logs = [];
$id = 1;
$maxLogs = 1000; // محدود کردن تعداد لاگ‌های صادر شده
foreach ($finder as $file) {
if ($file->getSize() > 10 * 1024 * 1024) { // فایل‌های بزرگتر از 10MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
while (($line = fgets($handle)) !== false && count($logs) < $maxLogs) {
if (empty(trim($line))) continue;
$logEntry = $this->parseLogLine($line, $file->getFilename(), $id++);
if ($logEntry) {
if ($date && $logEntry['date'] !== $date) continue;
if ($level && $logEntry['level'] !== $level) continue;
$logs[] = $logEntry;
}
}
fclose($handle);
}
return $logs;
}
private function convertToCsv(array $logs): string
{
$csv = "ID,Date,Time,Level,Message,Filename,Environment\n";
foreach ($logs as $log) {
$csv .= sprintf(
"%d,%s,%s,%s,%s,%s,%s\n",
$log['id'],
$log['date'],
$log['time'],
$log['level'],
str_replace(',', ';', $log['message']),
$log['filename'],
$log['environment']
);
}
return $csv;
}
private function getLogFilesCount(): int
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
return iterator_count($finder);
}
private function getLogFilesSize(): int
{
$size = 0;
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
foreach ($finder as $file) {
$size += $file->getSize();
}
return $size;
}
private function getLastErrorLog(): ?array
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
})
->sortByModifiedTime();
$lastError = null;
$lastTimestamp = 0;
foreach ($finder as $file) {
if ($file->getSize() > 5 * 1024 * 1024) { // فایل‌های بزرگتر از 5MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
while (($line = fgets($handle)) !== false) {
if (empty(trim($line))) continue;
$logEntry = $this->parseLogLine($line, $file->getFilename(), 1);
if ($logEntry && in_array($logEntry['level'], ['ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY'])) {
$timestamp = strtotime($logEntry['timestamp']);
if ($timestamp > $lastTimestamp) {
$lastTimestamp = $timestamp;
$lastError = $logEntry;
}
}
}
fclose($handle);
}
return $lastError;
}
}

View file

@ -0,0 +1,282 @@
<?php
namespace App\Entity;
use App\Repository\ChatChannelRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ChatChannelRepository::class)]
class ChatChannel
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(length: 1000, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 50, unique: true)]
private ?string $channelId = null;
#[ORM\Column]
private bool $isPublic = true;
#[ORM\Column]
private bool $isActive = true;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private ?User $createdBy = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
#[ORM\OneToMany(mappedBy: 'channel', targetEntity: ChatMessage::class, orphanRemoval: true)]
private Collection $messages;
#[ORM\OneToMany(mappedBy: 'channel', targetEntity: ChatChannelMember::class, orphanRemoval: true)]
private Collection $members;
#[ORM\Column(length: 255, nullable: true)]
private ?string $avatar = null;
#[ORM\Column]
private int $messageCount = 0;
#[ORM\Column]
private int $memberCount = 0;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $lastMessageAt = null;
public function __construct()
{
$this->messages = new ArrayCollection();
$this->members = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
$this->channelId = $this->generateChannelId();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getChannelId(): ?string
{
return $this->channelId;
}
public function setChannelId(string $channelId): static
{
$this->channelId = $channelId;
return $this;
}
public function isPublic(): bool
{
return $this->isPublic;
}
public function setIsPublic(bool $isPublic): static
{
$this->isPublic = $isPublic;
return $this;
}
public function isActive(): bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): static
{
$this->isActive = $isActive;
return $this;
}
public function getCreatedBy(): ?User
{
return $this->createdBy;
}
public function setCreatedBy(?User $createdBy): static
{
$this->createdBy = $createdBy;
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;
}
/**
* @return Collection<int, ChatMessage>
*/
public function getMessages(): Collection
{
return $this->messages;
}
public function addMessage(ChatMessage $message): static
{
if (!$this->messages->contains($message)) {
$this->messages->add($message);
$message->setChannel($this);
}
return $this;
}
public function removeMessage(ChatMessage $message): static
{
if ($this->messages->removeElement($message)) {
if ($message->getChannel() === $this) {
$message->setChannel(null);
}
}
return $this;
}
/**
* @return Collection<int, ChatChannelMember>
*/
public function getMembers(): Collection
{
return $this->members;
}
public function addMember(ChatChannelMember $member): static
{
if (!$this->members->contains($member)) {
$this->members->add($member);
$member->setChannel($this);
}
return $this;
}
public function removeMember(ChatChannelMember $member): static
{
if ($this->members->removeElement($member)) {
if ($member->getChannel() === $this) {
$member->setChannel(null);
}
}
return $this;
}
public function getAvatar(): ?string
{
return $this->avatar;
}
public function setAvatar(?string $avatar): static
{
$this->avatar = $avatar;
return $this;
}
public function getMessageCount(): int
{
return $this->messageCount;
}
public function setMessageCount(int $messageCount): static
{
$this->messageCount = $messageCount;
return $this;
}
public function getMemberCount(): int
{
return $this->memberCount;
}
public function setMemberCount(int $memberCount): static
{
$this->memberCount = $memberCount;
return $this;
}
public function getLastMessageAt(): ?\DateTimeImmutable
{
return $this->lastMessageAt;
}
public function setLastMessageAt(?\DateTimeImmutable $lastMessageAt): static
{
$this->lastMessageAt = $lastMessageAt;
return $this;
}
private function generateChannelId(): string
{
return 'CH' . strtoupper(uniqid());
}
public function isUserMember(User $user): bool
{
return $this->members->exists(function($key, $member) use ($user) {
return $member->getUser() === $user && $member->isActive();
});
}
public function isUserAdmin(User $user): bool
{
return $this->members->exists(function($key, $member) use ($user) {
return $member->getUser() === $user && $member->isAdmin() && $member->isActive();
});
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace App\Entity;
use App\Repository\ChatChannelMemberRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ChatChannelMemberRepository::class)]
class ChatChannelMember
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: ChatChannel::class, inversedBy: 'members')]
#[ORM\JoinColumn(nullable: false)]
private ?ChatChannel $channel = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
#[ORM\Column]
private bool $isAdmin = false;
#[ORM\Column]
private bool $isActive = true;
#[ORM\Column]
private ?\DateTimeImmutable $joinedAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $lastSeenAt = null;
#[ORM\Column]
private int $unreadCount = 0;
public function __construct()
{
$this->joinedAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getChannel(): ?ChatChannel
{
return $this->channel;
}
public function setChannel(?ChatChannel $channel): static
{
$this->channel = $channel;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
public function isAdmin(): bool
{
return $this->isAdmin;
}
public function setIsAdmin(bool $isAdmin): static
{
$this->isAdmin = $isAdmin;
return $this;
}
public function isActive(): bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): static
{
$this->isActive = $isActive;
return $this;
}
public function getJoinedAt(): ?\DateTimeImmutable
{
return $this->joinedAt;
}
public function setJoinedAt(\DateTimeImmutable $joinedAt): static
{
$this->joinedAt = $joinedAt;
return $this;
}
public function getLastSeenAt(): ?\DateTimeImmutable
{
return $this->lastSeenAt;
}
public function setLastSeenAt(?\DateTimeImmutable $lastSeenAt): static
{
$this->lastSeenAt = $lastSeenAt;
return $this;
}
public function getUnreadCount(): int
{
return $this->unreadCount;
}
public function setUnreadCount(int $unreadCount): static
{
$this->unreadCount = $unreadCount;
return $this;
}
public function incrementUnreadCount(): static
{
$this->unreadCount++;
return $this;
}
public function resetUnreadCount(): static
{
$this->unreadCount = 0;
return $this;
}
}

View file

@ -0,0 +1,274 @@
<?php
namespace App\Entity;
use App\Repository\ChatMessageRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ChatMessageRepository::class)]
class ChatMessage
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: ChatChannel::class, inversedBy: 'messages')]
#[ORM\JoinColumn(nullable: false)]
private ?ChatChannel $channel = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private ?User $sender = null;
#[ORM\Column(type: 'text')]
private ?string $content = null;
#[ORM\Column(length: 20)]
private ?string $messageType = 'text';
#[ORM\Column]
private ?\DateTimeImmutable $sentAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $editedAt = null;
#[ORM\Column]
private bool $isEdited = false;
#[ORM\Column]
private bool $isDeleted = false;
#[ORM\ManyToOne(targetEntity: self::class)]
private ?self $quotedMessage = null;
#[ORM\Column(type: 'json', nullable: true)]
private array $attachments = [];
#[ORM\Column(type: 'json', nullable: true)]
private array $reactions = [];
#[ORM\Column]
private int $replyCount = 0;
#[ORM\Column]
private int $viewCount = 0;
public function __construct()
{
$this->sentAt = new \DateTimeImmutable();
$this->messageType = 'text';
$this->attachments = [];
$this->reactions = [];
}
public function getId(): ?int
{
return $this->id;
}
public function getChannel(): ?ChatChannel
{
return $this->channel;
}
public function setChannel(?ChatChannel $channel): static
{
$this->channel = $channel;
return $this;
}
public function getSender(): ?User
{
return $this->sender;
}
public function setSender(?User $sender): static
{
$this->sender = $sender;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): static
{
$this->content = $content;
return $this;
}
public function getMessageType(): ?string
{
return $this->messageType;
}
public function setMessageType(string $messageType): static
{
$this->messageType = $messageType;
return $this;
}
public function getSentAt(): ?\DateTimeImmutable
{
return $this->sentAt;
}
public function setSentAt(\DateTimeImmutable $sentAt): static
{
$this->sentAt = $sentAt;
return $this;
}
public function getEditedAt(): ?\DateTimeImmutable
{
return $this->editedAt;
}
public function setEditedAt(?\DateTimeImmutable $editedAt): static
{
$this->editedAt = $editedAt;
return $this;
}
public function isEdited(): bool
{
return $this->isEdited;
}
public function setIsEdited(bool $isEdited): static
{
$this->isEdited = $isEdited;
return $this;
}
public function isDeleted(): bool
{
return $this->isDeleted;
}
public function setIsDeleted(bool $isDeleted): static
{
$this->isDeleted = $isDeleted;
return $this;
}
public function getQuotedMessage(): ?self
{
return $this->quotedMessage;
}
public function setQuotedMessage(?self $quotedMessage): static
{
$this->quotedMessage = $quotedMessage;
return $this;
}
public function getAttachments(): array
{
return $this->attachments;
}
public function setAttachments(array $attachments): static
{
$this->attachments = $attachments;
return $this;
}
public function addAttachment(array $attachment): static
{
$this->attachments[] = $attachment;
return $this;
}
public function getReactions(): array
{
return $this->reactions;
}
public function setReactions(array $reactions): static
{
$this->reactions = $reactions;
return $this;
}
public function addReaction(string $emoji, int $userId): static
{
if (!isset($this->reactions[$emoji])) {
$this->reactions[$emoji] = [];
}
if (!in_array($userId, $this->reactions[$emoji])) {
$this->reactions[$emoji][] = $userId;
}
return $this;
}
public function removeReaction(string $emoji, int $userId): static
{
if (isset($this->reactions[$emoji])) {
$this->reactions[$emoji] = array_filter(
$this->reactions[$emoji],
fn($id) => $id !== $userId
);
if (empty($this->reactions[$emoji])) {
unset($this->reactions[$emoji]);
}
}
return $this;
}
public function getReplyCount(): int
{
return $this->replyCount;
}
public function setReplyCount(int $replyCount): static
{
$this->replyCount = $replyCount;
return $this;
}
public function getViewCount(): int
{
return $this->viewCount;
}
public function setViewCount(int $viewCount): static
{
$this->viewCount = $viewCount;
return $this;
}
public function incrementViewCount(): static
{
$this->viewCount++;
return $this;
}
public function isEmoji(): bool
{
return $this->messageType === 'emoji';
}
public function isFile(): bool
{
return $this->messageType === 'file';
}
public function isImage(): bool
{
return $this->messageType === 'image';
}
public function isVideo(): bool
{
return $this->messageType === 'video';
}
public function isAudio(): bool
{
return $this->messageType === 'audio';
}
}

View file

@ -33,7 +33,7 @@ class Commodity
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private $des;
#[ORM\Column(type: 'bigint')]
#[ORM\Column(type: 'string', length: 255)]
private $code;
#[ORM\Column(nullable: true)]

View file

@ -0,0 +1,196 @@
<?php
namespace App\Repository;
use App\Entity\ChatChannel;
use App\Entity\ChatChannelMember;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ChatChannelMember>
*
* @method ChatChannelMember|null find($id, $lockMode = null, $lockVersion = null)
* @method ChatChannelMember|null findOneBy(array $criteria, array $orderBy = null)
* @method ChatChannelMember[] findAll()
* @method ChatChannelMember[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ChatChannelMemberRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ChatChannelMember::class);
}
public function save(ChatChannelMember $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(ChatChannelMember $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* Find member by channel and user
*/
public function findByChannelAndUser(ChatChannel $channel, User $user): ?ChatChannelMember
{
return $this->createQueryBuilder('m')
->where('m.channel = :channel')
->andWhere('m.user = :user')
->setParameter('channel', $channel)
->setParameter('user', $user)
->getQuery()
->getOneOrNullResult();
}
/**
* Find active members of a channel
*/
public function findActiveMembers(ChatChannel $channel): array
{
return $this->createQueryBuilder('m')
->join('m.user', 'u')
->where('m.channel = :channel')
->andWhere('m.isActive = :isActive')
->setParameter('channel', $channel)
->setParameter('isActive', true)
->orderBy('m.isAdmin', 'DESC')
->addOrderBy('m.joinedAt', 'ASC')
->getQuery()
->getResult();
}
/**
* Find admin members of a channel
*/
public function findAdminMembers(ChatChannel $channel): array
{
return $this->createQueryBuilder('m')
->join('m.user', 'u')
->where('m.channel = :channel')
->andWhere('m.isAdmin = :isAdmin')
->andWhere('m.isActive = :isActive')
->setParameter('channel', $channel)
->setParameter('isAdmin', true)
->setParameter('isActive', true)
->orderBy('m.joinedAt', 'ASC')
->getQuery()
->getResult();
}
/**
* Check if user is member of channel
*/
public function isUserMember(ChatChannel $channel, User $user): bool
{
$member = $this->findByChannelAndUser($channel, $user);
return $member !== null && $member->isActive();
}
/**
* Check if user is admin of channel
*/
public function isUserAdmin(ChatChannel $channel, User $user): bool
{
$member = $this->findByChannelAndUser($channel, $user);
return $member !== null && $member->isActive() && $member->isAdmin();
}
/**
* Get member count for a channel
*/
public function getMemberCount(ChatChannel $channel): int
{
return $this->createQueryBuilder('m')
->select('COUNT(m.id)')
->where('m.channel = :channel')
->andWhere('m.isActive = :isActive')
->setParameter('channel', $channel)
->setParameter('isActive', true)
->getQuery()
->getSingleScalarResult();
}
/**
* Get admin count for a channel
*/
public function getAdminCount(ChatChannel $channel): int
{
return $this->createQueryBuilder('m')
->select('COUNT(m.id)')
->where('m.channel = :channel')
->andWhere('m.isAdmin = :isAdmin')
->andWhere('m.isActive = :isActive')
->setParameter('channel', $channel)
->setParameter('isAdmin', true)
->setParameter('isActive', true)
->getQuery()
->getSingleScalarResult();
}
/**
* Find channels where user is member
*/
public function findUserChannels(User $user): array
{
return $this->createQueryBuilder('m')
->join('m.channel', 'c')
->where('m.user = :user')
->andWhere('m.isActive = :isActive')
->andWhere('c.isActive = :channelActive')
->setParameter('user', $user)
->setParameter('isActive', true)
->setParameter('channelActive', true)
->orderBy('c.lastMessageAt', 'DESC')
->getQuery()
->getResult();
}
/**
* Update last seen time for member
*/
public function updateLastSeen(ChatChannelMember $member): void
{
$member->setLastSeenAt(new \DateTimeImmutable());
$this->save($member, true);
}
/**
* Reset unread count for member
*/
public function resetUnreadCount(ChatChannelMember $member): void
{
$member->resetUnreadCount();
$this->save($member, true);
}
/**
* Increment unread count for all members except sender
*/
public function incrementUnreadCountForChannel(ChatChannel $channel, User $sender): void
{
$this->createQueryBuilder('m')
->update()
->set('m.unreadCount', 'm.unreadCount + 1')
->where('m.channel = :channel')
->andWhere('m.user != :sender')
->andWhere('m.isActive = :isActive')
->setParameter('channel', $channel)
->setParameter('sender', $sender)
->setParameter('isActive', true)
->getQuery()
->execute();
}
}

View file

@ -0,0 +1,172 @@
<?php
namespace App\Repository;
use App\Entity\ChatChannel;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ChatChannel>
*
* @method ChatChannel|null find($id, $lockMode = null, $lockVersion = null)
* @method ChatChannel|null findOneBy(array $criteria, array $orderBy = null)
* @method ChatChannel[] findAll()
* @method ChatChannel[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ChatChannelRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ChatChannel::class);
}
public function save(ChatChannel $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(ChatChannel $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* Find public channels that match search term
*/
public function findPublicChannelsBySearch(string $searchTerm, int $limit = 20): array
{
return $this->createQueryBuilder('c')
->where('c.isPublic = :isPublic')
->andWhere('c.isActive = :isActive')
->andWhere('c.name LIKE :searchTerm OR c.description LIKE :searchTerm OR c.channelId LIKE :searchTerm')
->setParameter('isPublic', true)
->setParameter('isActive', true)
->setParameter('searchTerm', '%' . $searchTerm . '%')
->orderBy('c.lastMessageAt', 'DESC')
->addOrderBy('c.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Find channels that user is member of
*/
public function findUserChannels(User $user, int $limit = 50): array
{
return $this->createQueryBuilder('c')
->join('c.members', 'm')
->where('m.user = :user')
->andWhere('m.isActive = :isActive')
->andWhere('c.isActive = :channelActive')
->setParameter('user', $user)
->setParameter('isActive', true)
->setParameter('channelActive', true)
->orderBy('c.lastMessageAt', 'DESC')
->addOrderBy('c.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Find channel by channel ID
*/
public function findByChannelId(string $channelId): ?ChatChannel
{
return $this->createQueryBuilder('c')
->where('c.channelId = :channelId')
->andWhere('c.isActive = :isActive')
->setParameter('channelId', $channelId)
->setParameter('isActive', true)
->getQuery()
->getOneOrNullResult();
}
/**
* Find channels created by user
*/
public function findChannelsCreatedBy(User $user): array
{
return $this->createQueryBuilder('c')
->where('c.createdBy = :user')
->andWhere('c.isActive = :isActive')
->setParameter('user', $user)
->setParameter('isActive', true)
->orderBy('c.createdAt', 'DESC')
->getQuery()
->getResult();
}
/**
* Get channel statistics
*/
public function getChannelStats(ChatChannel $channel): array
{
// Get member count
$memberCount = $this->getEntityManager()->createQueryBuilder()
->select('COUNT(m.id)')
->from('App\Entity\ChatChannelMember', 'm')
->where('m.channel = :channel')
->andWhere('m.isActive = :isActive')
->setParameter('channel', $channel)
->setParameter('isActive', true)
->getQuery()
->getSingleScalarResult();
// Get message count
$messageCount = $this->getEntityManager()->createQueryBuilder()
->select('COUNT(msg.id)')
->from('App\Entity\ChatMessage', 'msg')
->where('msg.channel = :channel')
->andWhere('msg.isDeleted = :isDeleted')
->setParameter('channel', $channel)
->setParameter('isDeleted', false)
->getQuery()
->getSingleScalarResult();
return [
'memberCount' => $memberCount,
'messageCount' => $messageCount
];
}
/**
* Find popular public channels ordered by member count and message count
*/
public function findPopularPublicChannels(int $limit = 10): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
return $qb->select('c')
->from('App\Entity\ChatChannel', 'c')
->leftJoin('c.members', 'm')
->leftJoin('c.messages', 'msg')
->where('c.isPublic = :isPublic')
->andWhere('c.isActive = :isActive')
->andWhere('m.isActive = :memberActive OR m.id IS NULL')
->andWhere('msg.isDeleted = :msgDeleted OR msg.id IS NULL')
->setParameter('isPublic', true)
->setParameter('isActive', true)
->setParameter('memberActive', true)
->setParameter('msgDeleted', false)
->groupBy('c.id')
->orderBy('COUNT(DISTINCT m.id)', 'DESC')
->addOrderBy('COUNT(DISTINCT msg.id)', 'DESC')
->addOrderBy('c.lastMessageAt', 'DESC')
->addOrderBy('c.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}

View file

@ -0,0 +1,191 @@
<?php
namespace App\Repository;
use App\Entity\ChatChannel;
use App\Entity\ChatMessage;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ChatMessage>
*
* @method ChatMessage|null find($id, $lockMode = null, $lockVersion = null)
* @method ChatMessage|null findOneBy(array $criteria, array $orderBy = null)
* @method ChatMessage[] findAll()
* @method ChatMessage[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ChatMessageRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ChatMessage::class);
}
public function save(ChatMessage $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(ChatMessage $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* Find messages for a channel with pagination
*/
public function findChannelMessages(ChatChannel $channel, int $limit = 50, int $offset = 0): array
{
return $this->createQueryBuilder('m')
->where('m.channel = :channel')
->andWhere('m.isDeleted = :isDeleted')
->setParameter('channel', $channel)
->setParameter('isDeleted', false)
->orderBy('m.sentAt', 'DESC')
->setMaxResults($limit)
->setFirstResult($offset)
->getQuery()
->getResult();
}
/**
* Find messages after a specific message ID
*/
public function findMessagesAfter(ChatChannel $channel, int $messageId, int $limit = 50): array
{
return $this->createQueryBuilder('m')
->where('m.channel = :channel')
->andWhere('m.id > :messageId')
->andWhere('m.isDeleted = :isDeleted')
->setParameter('channel', $channel)
->setParameter('messageId', $messageId)
->setParameter('isDeleted', false)
->orderBy('m.sentAt', 'ASC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Find messages before a specific message ID
*/
public function findMessagesBefore(ChatChannel $channel, int $messageId, int $limit = 50): array
{
return $this->createQueryBuilder('m')
->where('m.channel = :channel')
->andWhere('m.id < :messageId')
->andWhere('m.isDeleted = :isDeleted')
->setParameter('channel', $channel)
->setParameter('messageId', $messageId)
->setParameter('isDeleted', false)
->orderBy('m.sentAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Search messages in a channel
*/
public function searchMessages(ChatChannel $channel, string $searchTerm, int $limit = 50): array
{
return $this->createQueryBuilder('m')
->where('m.channel = :channel')
->andWhere('m.isDeleted = :isDeleted')
->andWhere('m.content LIKE :searchTerm')
->setParameter('channel', $channel)
->setParameter('isDeleted', false)
->setParameter('searchTerm', '%' . $searchTerm . '%')
->orderBy('m.sentAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Find messages by user in a channel
*/
public function findUserMessagesInChannel(ChatChannel $channel, User $user, int $limit = 50): array
{
return $this->createQueryBuilder('m')
->where('m.channel = :channel')
->andWhere('m.sender = :user')
->andWhere('m.isDeleted = :isDeleted')
->setParameter('channel', $channel)
->setParameter('user', $user)
->setParameter('isDeleted', false)
->orderBy('m.sentAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Get total message count for a channel
*/
public function getChannelMessageCount(ChatChannel $channel): int
{
return $this->createQueryBuilder('m')
->select('COUNT(m.id)')
->where('m.channel = :channel')
->andWhere('m.isDeleted = :isDeleted')
->setParameter('channel', $channel)
->setParameter('isDeleted', false)
->getQuery()
->getSingleScalarResult();
}
/**
* Get message statistics for a channel
*/
public function getChannelMessageStats(ChatChannel $channel): array
{
$qb = $this->createQueryBuilder('m')
->select('COUNT(m.id) as total, SUM(CASE WHEN m.messageType = :emoji THEN 1 ELSE 0 END) as emoji_count')
->where('m.channel = :channel')
->andWhere('m.isDeleted = :isDeleted')
->setParameter('channel', $channel)
->setParameter('isDeleted', false)
->setParameter('emoji', 'emoji');
$result = $qb->getQuery()->getSingleResult();
return [
'totalMessages' => $result['total'],
'emojiMessages' => $result['emoji_count'],
'textMessages' => $result['total'] - $result['emoji_count']
];
}
/**
* Find recent messages for user across all channels
*/
public function findRecentMessagesForUser(User $user, int $limit = 20): array
{
return $this->createQueryBuilder('m')
->join('m.channel', 'c')
->join('c.members', 'cm')
->where('cm.user = :user')
->andWhere('cm.isActive = :memberActive')
->andWhere('c.isActive = :channelActive')
->andWhere('m.isDeleted = :isDeleted')
->setParameter('user', $user)
->setParameter('memberActive', true)
->setParameter('channelActive', true)
->setParameter('isDeleted', false)
->orderBy('m.sentAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}

View file

@ -9,6 +9,9 @@ use App\Service\registryMGR;
use App\Service\Log;
use App\Service\Provider;
use App\Service\AGI\Promps\PromptService;
use App\Service\Explore;
use App\Service\Jdate;
use App\Service\SMS;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
@ -23,6 +26,10 @@ class AGIService
private $promptService;
private $httpClient;
private $httpKernel;
private $explore;
private $jdate;
private $sms;
private $uploadDirectory;
public function __construct(
EntityManagerInterface $entityManager,
@ -31,7 +38,11 @@ class AGIService
Provider $provider,
PromptService $promptService,
HttpClientInterface $httpClient,
HttpKernelInterface $httpKernel
HttpKernelInterface $httpKernel,
Explore $explore,
Jdate $jdate,
SMS $sms,
string $uploadDirectory
) {
$this->em = $entityManager;
$this->registryMGR = $registryMGR;
@ -40,6 +51,10 @@ class AGIService
$this->promptService = $promptService;
$this->httpClient = $httpClient;
$this->httpKernel = $httpKernel;
$this->explore = $explore;
$this->jdate = $jdate;
$this->sms = $sms;
$this->uploadDirectory = $uploadDirectory;
}
/**
@ -337,19 +352,19 @@ class AGIService
return $accountingDocService->searchRowsAi($params, $params['acc'] ?? null);
// ابزارهای مربوط به تیکت
case 'getTicketsList':
$cogTicketService = new \App\Cog\TicketService($this->em);
$cogTicketService = new \App\Cog\TicketService($this->em, $this->explore, $this->jdate, $this->registryMGR, $this->sms, $this->uploadDirectory);
$ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->getTicketsListAi($params, $params['acc'] ?? null);
case 'getTicketInfo':
$cogTicketService = new \App\Cog\TicketService($this->em);
$cogTicketService = new \App\Cog\TicketService($this->em, $this->explore, $this->jdate, $this->registryMGR, $this->sms, $this->uploadDirectory);
$ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->getTicketInfoByCode($params['code'] ?? null, $params['acc'] ?? null);
case 'addOrUpdateTicket':
$cogTicketService = new \App\Cog\TicketService($this->em);
$cogTicketService = new \App\Cog\TicketService($this->em, $this->explore, $this->jdate, $this->registryMGR, $this->sms, $this->uploadDirectory);
$ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->addOrUpdateTicketAi($params, $params['acc'] ?? null, $params['code'] ?? 0);
case 'replyToTicket':
$cogTicketService = new \App\Cog\TicketService($this->em);
$cogTicketService = new \App\Cog\TicketService($this->em, $this->explore, $this->jdate, $this->registryMGR, $this->sms, $this->uploadDirectory);
$ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->replyToTicketAi($params, $params['acc'] ?? null);
default:

View file

@ -86,6 +86,8 @@ class PromptService
switch ($key) {
case 'person':
return $this->personPromptService->getAllPersonPrompts();
case 'ticket':
return $this->ticketPromptService->getAllTicketPrompts();
// در آینده موارد بیشتر اضافه خواهند شد
// case 'accounting':
// return $this->accountingPromptService->getAllAccountingPrompts();

View file

@ -2,204 +2,292 @@
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class TicketService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* دریافت ابزارهای مربوط به تیکت‌ها
* دریافت تمام ابزارهای بخش تیکت‌ها برای function calling
* @return array
*/
public function getTools(): array
{
return [
[
'name' => 'analyze_ticket',
'description' => 'تحلیل و دسته‌بندی تیکت',
'parameters' => [
'type' => 'object',
'properties' => [
'ticket_body' => [
'type' => 'string',
'description' => 'متن تیکت'
]
],
'required' => ['ticket_body']
$tools = [];
// ابزار getTicketsList
$ticketsListPrompt = $this->getTicketsListPrompt();
$ticketsListData = json_decode($ticketsListPrompt, true);
if ($ticketsListData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $ticketsListData['tool'],
'description' => $ticketsListData['description'],
'parameters' => $ticketsListData['parameters']
]
],
[
'name' => 'draft_ticket_response',
'description' => 'تهیه پیش‌نویس پاسخ تیکت',
'parameters' => [
'type' => 'object',
'properties' => [
'ticket_body' => [
'type' => 'string',
'description' => 'متن تیکت'
],
'ticket_title' => [
'type' => 'string',
'description' => 'عنوان تیکت'
],
'history' => [
'type' => 'array',
'description' => 'تاریخچه مکالمات قبلی',
'items' => [
'type' => 'object',
'properties' => [
'sender' => [
'type' => 'string',
'description' => 'فرستنده پیام'
],
'message' => [
'type' => 'string',
'description' => 'متن پیام'
]
]
]
]
],
'required' => ['ticket_body', 'ticket_title']
];
}
// ابزار getTicketInfo
$ticketInfoPrompt = $this->getTicketInfoPrompt();
$ticketInfoData = json_decode($ticketInfoPrompt, true);
if ($ticketInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $ticketInfoData['tool'],
'description' => $ticketInfoData['description'],
'parameters' => $ticketInfoData['parameters']
]
]
];
}
/**
* پرامپت برای بررسی متن تیکت و دسته‌بندی آن
*/
public function getTicketAnalysisPrompt(string $ticketBody): string
{
return <<<PROMPT
لطفاً این تیکت پشتیبانی را بررسی و دسته‌بندی کنید:
متن تیکت:
{$ticketBody}
لطفاً موارد زیر را مشخص کنید:
1. موضوع اصلی تیکت
2. اولویت (کم، متوسط، زیاد)
3. بخش مربوطه (مالی، فنی، عمومی)
4. پیشنهاد برای پاسخ
PROMPT;
}
/**
* پرامپت برای تولید پیش‌نویس پاسخ به تیکت
*/
public function getDraftResponsePrompt(string $ticketBody, string $ticketTitle, array $history = []): string
{
$historyText = '';
if (!empty($history)) {
$historyText = "تاریخچه مکالمات قبلی:\n";
foreach ($history as $message) {
$historyText .= sprintf(
"- %s: %s\n",
$message['sender'],
$message['message']
);
}
];
}
return <<<PROMPT
لطفاً یک پیش‌نویس پاسخ مناسب برای این تیکت پشتیبانی آماده کنید:
عنوان تیکت: {$ticketTitle}
متن تیکت:
{$ticketBody}
{$historyText}
لطفاً یک پاسخ حرفه‌ای و دقیق با در نظر گرفتن نکات زیر آماده کنید:
1. لحن مؤدبانه و حرفه‌ای
2. پاسخگویی به تمام نکات مطرح شده در تیکت
3. ارائه راهکارهای عملی
4. درخواست اطلاعات تکمیلی در صورت نیاز
PROMPT;
// ابزار addOrUpdateTicket
$addOrUpdatePrompt = $this->getAddOrUpdateTicketPrompt();
$addOrUpdateData = json_decode($addOrUpdatePrompt, true);
if ($addOrUpdateData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $addOrUpdateData['tool'],
'description' => $addOrUpdateData['description'],
'parameters' => $addOrUpdateData['parameters']
]
];
}
// ابزار replyToTicket
$replyPrompt = $this->getReplyToTicketPrompt();
$replyData = json_decode($replyPrompt, true);
if ($replyData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $replyData['tool'],
'description' => $replyData['description'],
'parameters' => $replyData['parameters']
]
];
}
return $tools;
}
/**
* پرامپت برای پیشنهاد اقدامات بعدی برای تیکت
* تولید تمام پرامپ‌های بخش تیکت‌ها
* @return string
*/
public function getNextActionPrompt(string $ticketBody, string $currentStatus, array $previousActions = []): string
public function getAllTicketPrompts(): string
{
$previousActionsText = '';
if (!empty($previousActions)) {
$previousActionsText = "اقدامات قبلی:\n";
foreach ($previousActions as $action) {
$previousActionsText .= "- {$action}\n";
}
}
return <<<PROMPT
لطفاً اقدامات بعدی مناسب برای این تیکت را پیشنهاد دهید:
متن تیکت:
{$ticketBody}
وضعیت فعلی: {$currentStatus}
{$previousActionsText}
لطفاً موارد زیر را مشخص کنید:
1. آیا نیاز به ارجاع به بخش دیگری هست؟
2. آیا نیاز به اطلاعات تکمیلی از کاربر هست؟
3. اولویت رسیدگی به این تیکت
4. پیشنهاد برای اقدام بعدی
PROMPT;
$prompts = [];
$prompts[] = $this->getTicketsListPrompt();
$prompts[] = $this->getTicketInfoPrompt();
$prompts[] = $this->getAddOrUpdateTicketPrompt();
$prompts[] = $this->getReplyToTicketPrompt();
return implode("\n\n", $prompts);
}
/**
* پرامپت برای خلاصه‌سازی تیکت و تاریخچه آن
* پرامپ برای دریافت لیست تیکت‌ها
*/
public function getTicketSummaryPrompt(array $ticketHistory): string
public function getTicketsListPrompt(): string
{
$historyText = '';
foreach ($ticketHistory as $entry) {
$historyText .= sprintf(
"- %s (%s): %s\n",
$entry['date'],
$entry['user'],
$entry['message']
);
return '{
"tool": "getTicketsList",
"description": "دریافت لیست تیکت‌های پشتیبانی با فیلتر و صفحه‌بندی",
"endpoint": "/api/ticket/list",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"page": {"type": "integer", "description": "شماره صفحه"},
"itemsPerPage": {"type": "integer", "description": "تعداد آیتم در هر صفحه"},
"search": {"type": "string", "description": "متن جست‌وجو (عنوان، کد، و غیره)"},
"status": {"type": "array", "items": {"type": "string"}, "description": "فیلتر وضعیت تیکت‌ها (اختیاری)"},
"priority": {"type": "array", "items": {"type": "string"}, "description": "فیلتر اولویت تیکت‌ها (اختیاری)"},
"sortBy": {"type": ["string", "null"], "description": "فیلد مرتب‌سازی (اختیاری)"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (مورد نیاز برای backend)"}
},
"required": ["page", "itemsPerPage", "search"]
},
"output": {
"items": [
{
"id": "integer",
"code": "string",
"title": "string",
"body": "string",
"status": "string",
"priority": "string",
"dateSubmit": "integer",
"submitter": "object",
"main": "integer",
"fileName": "string|null"
}
],
"total": "integer",
"unfilteredTotal": "integer"
},
"examples": {
"input": {"page":1,"itemsPerPage":10,"search":"مشکل","status":["در حال پیگیری","بسته شده"],"priority":["کم","متوسط","زیاد"],"sortBy":null,"acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {
"items": [
{
"id": 1,
"code": "TKT001",
"title": "مشکل در ورود به سیستم",
"body": "نمی‌توانم وارد سیستم شوم",
"status": "در حال پیگیری",
"priority": "متوسط",
"dateSubmit": 1703123456,
"submitter": {"id": 1, "name": "کاربر نمونه"},
"main": 0,
"fileName": null
}
return <<<PROMPT
لطفاً خلاصه‌ای از این تیکت و تاریخچه آن تهیه کنید:
تاریخچه تیکت:
{$historyText}
لطفاً موارد زیر را در خلاصه مشخص کنید:
1. موضوع اصلی و مشکل گزارش شده
2. اقدامات انجام شده
3. وضعیت فعلی
4. نکات مهم برای پیگیری
PROMPT;
],
"total": 1,
"unfilteredTotal": 5
}
}
}';
}
/**
* پرامپت برای دسته‌بندی خودکار تیکت‌ها
* پرامپ برای دریافت اطلاعات تیکت
*/
public function getTicketCategorizationPrompt(array $tickets): string
public function getTicketInfoPrompt(): string
{
$ticketsText = '';
foreach ($tickets as $ticket) {
$ticketsText .= sprintf(
"عنوان: %s\nمتن: %s\n\n",
$ticket['title'],
$ticket['body']
);
return '{
"tool": "getTicketInfo",
"description": "دریافت اطلاعات کامل یک تیکت بر اساس کد",
"endpoint": "/api/ticket/info/{code}",
"method": "GET",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "کد تیکت (مثل TKT001, TKT002)"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (مورد نیاز برای backend)"}
},
"required": ["code"]
},
"output": {
"id": "integer",
"code": "string",
"title": "string",
"body": "string",
"status": "string",
"priority": "string",
"dateSubmit": "integer",
"submitter": "object",
"main": "integer",
"fileName": "string|null",
"replies": [
{
"id": "integer",
"body": "string",
"dateSubmit": "integer",
"submitter": "object",
"fileName": "string|null"
}
]
},
"examples": {
"input": {"code": "TKT001"},
"output": {
"id": 1,
"code": "TKT001",
"title": "مشکل در ورود به سیستم",
"body": "نمی‌توانم وارد سیستم شوم",
"status": "در حال پیگیری",
"priority": "متوسط",
"dateSubmit": 1703123456,
"submitter": {"id": 1, "name": "کاربر نمونه"},
"main": 0,
"fileName": null,
"replies": [
{
"id": 2,
"body": "لطفاً مرورگر خود را پاک کنید و دوباره تلاش کنید",
"dateSubmit": 1703124000,
"submitter": {"id": 2, "name": "پشتیبان"},
"fileName": null
}
]
}
}
}';
}
return <<<PROMPT
لطفاً این تیکت‌ها را بر اساس موضوع و محتوا دسته‌بندی کنید:
/**
* پرامپ برای افزودن یا ویرایش تیکت
*/
public function getAddOrUpdateTicketPrompt(): string
{
return '{
"tool": "addOrUpdateTicket",
"description": "برای ویرایش یک تیکت ابتدا باید با ابزار جست‌وجوی تیکت (getTicketsList) تیکت مورد نظر را پیدا کنید. اگر چند نتیجه یافت شد، باید از کاربر بپرسید کدام را می‌خواهد ویرایش کند و کد (code) آن را دریافت کنید. سپس با ارسال کد و اطلاعات جدید به این ابزار، ویرایش انجام می‌شود. اگر code برابر 0 یا ارسال نشود، تیکت جدید ایجاد خواهد شد. افزودن تیکت جدید یا ویرایش تیکت موجود",
"endpoint": "/api/ticket/mod/{code}",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "عنوان تیکت (مورد نیاز)"},
"body": {"type": "string", "description": "متن تیکت (مورد نیاز)"},
"priority": {"type": "string", "description": "اولویت تیکت (کم، متوسط، زیاد)"},
"status": {"type": "string", "description": "وضعیت تیکت (جدید، در حال پیگیری، بسته شده)"},
"code": {"type": ["integer", "string"], "description": "کد تیکت (0 برای جدید، در غیر این صورت برای ویرایش)"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (مورد نیاز برای backend)"}
},
"required": ["title", "body"]
},
"output": {
"Success": "boolean",
"result": "integer",
"message": "string"
},
"examples": {
"input": {"title":"مشکل جدید","body":"نمی‌توانم فایل آپلود کنم","priority":"متوسط","status":"جدید","code":0,"acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {"Success":true,"result":1,"message":"تیکت با موفقیت ایجاد شد"}
}
}';
}
تیکت‌ها:
{$ticketsText}
لطفاً برای هر تیکت موارد زیر را مشخص کنید:
1. دسته اصلی (مالی، فنی، پشتیبانی عمومی، آموزش)
2. زیر دسته
3. برچسب‌های پیشنهادی
4. اولویت پیشنهادی
PROMPT;
/**
* پرامپ برای پاسخ به تیکت
*/
public function getReplyToTicketPrompt(): string
{
return '{
"tool": "replyToTicket",
"description": "ارسال پاسخ به یک تیکت موجود",
"endpoint": "/api/ticket/reply",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"ticketCode": {"type": "string", "description": "کد تیکت (مورد نیاز)"},
"body": {"type": "string", "description": "متن پاسخ (مورد نیاز)"},
"status": {"type": "string", "description": "وضعیت جدید تیکت (اختیاری)"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (مورد نیاز برای backend)"}
},
"required": ["ticketCode", "body"]
},
"output": {
"Success": "boolean",
"result": "integer",
"message": "string"
},
"examples": {
"input": {"ticketCode":"TKT001","body":"لطفاً مرورگر خود را پاک کنید و دوباره تلاش کنید","status":"در حال پیگیری","acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {"Success":true,"result":1,"message":"پاسخ با موفقیت ارسال شد"}
}
}';
}
}

View file

@ -0,0 +1,340 @@
<?php
namespace App\Service;
use App\Entity\ChatChannel;
use App\Entity\ChatChannelMember;
use App\Entity\ChatMessage;
use App\Entity\User;
use App\Repository\ChatChannelMemberRepository;
use App\Repository\ChatChannelRepository;
use App\Repository\ChatMessageRepository;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
class ChatService
{
public function __construct(
private EntityManagerInterface $entityManager,
private ChatChannelRepository $channelRepository,
private ChatMessageRepository $messageRepository,
private ChatChannelMemberRepository $memberRepository,
private UserRepository $userRepository,
private Security $security
) {}
/**
* Create a new channel
*/
public function createChannel(string $name, string $description, bool $isPublic, User $creator): ChatChannel
{
$channel = new ChatChannel();
$channel->setName($name);
$channel->setDescription($description);
$channel->setIsPublic($isPublic);
$channel->setCreatedBy($creator);
$channel->setMemberCount(1); // Creator is the first member
// Add creator as admin member
$member = new ChatChannelMember();
$member->setChannel($channel);
$member->setUser($creator);
$member->setIsAdmin(true);
$this->entityManager->persist($channel);
$this->entityManager->persist($member);
$this->entityManager->flush();
return $channel;
}
/**
* Join a channel
*/
public function joinChannel(ChatChannel $channel, User $user): bool
{
// Check if already a member
if ($this->memberRepository->isUserMember($channel, $user)) {
return false;
}
// For private channels, only admins can add members
if (!$channel->isPublic()) {
$currentUser = $this->security->getUser();
if (!$this->memberRepository->isUserAdmin($channel, $currentUser)) {
return false;
}
}
$member = new ChatChannelMember();
$member->setChannel($channel);
$member->setUser($user);
$member->setIsAdmin(false);
$this->entityManager->persist($member);
// Update channel member count
$channel->setMemberCount($channel->getMemberCount() + 1);
$this->entityManager->flush();
return true;
}
/**
* Leave a channel
*/
public function leaveChannel(ChatChannel $channel, User $user): bool
{
$member = $this->memberRepository->findByChannelAndUser($channel, $user);
if (!$member) {
return false;
}
$member->setIsActive(false);
// Update channel member count
$channel->setMemberCount($channel->getMemberCount() - 1);
$this->entityManager->flush();
return true;
}
/**
* Add member to channel (admin only)
*/
public function addMemberToChannel(ChatChannel $channel, User $user, User $admin): bool
{
if (!$this->memberRepository->isUserAdmin($channel, $admin)) {
return false;
}
return $this->joinChannel($channel, $user);
}
/**
* Remove member from channel (admin only)
*/
public function removeMemberFromChannel(ChatChannel $channel, User $user, User $admin): bool
{
if (!$this->memberRepository->isUserAdmin($channel, $admin)) {
return false;
}
$member = $this->memberRepository->findByChannelAndUser($channel, $user);
if (!$member) {
return false;
}
$member->setIsActive(false);
// Update channel member count
$channel->setMemberCount($channel->getMemberCount() - 1);
$this->entityManager->flush();
return true;
}
/**
* Send message to channel
*/
public function sendMessage(ChatChannel $channel, User $sender, string $content, string $messageType = 'text', ?ChatMessage $quotedMessage = null): ChatMessage
{
// Check if user is member
if (!$this->memberRepository->isUserMember($channel, $sender)) {
throw new \Exception('User is not a member of this channel');
}
$message = new ChatMessage();
$message->setChannel($channel);
$message->setSender($sender);
$message->setContent($content);
$message->setMessageType($messageType);
if ($quotedMessage) {
$message->setQuotedMessage($quotedMessage);
}
$this->entityManager->persist($message);
// Update channel stats
$channel->setMessageCount($channel->getMessageCount() + 1);
$channel->setLastMessageAt(new \DateTimeImmutable());
// Increment unread count for other members
$this->memberRepository->incrementUnreadCountForChannel($channel, $sender);
$this->entityManager->flush();
return $message;
}
/**
* Edit message
*/
public function editMessage(ChatMessage $message, User $user, string $newContent): bool
{
if ($message->getSender() !== $user) {
return false;
}
$message->setContent($newContent);
$message->setIsEdited(true);
$message->setEditedAt(new \DateTimeImmutable());
$this->entityManager->flush();
return true;
}
/**
* Delete message
*/
public function deleteMessage(ChatMessage $message, User $user): bool
{
if ($message->getSender() !== $user) {
return false;
}
$message->setIsDeleted(true);
$this->entityManager->flush();
return true;
}
/**
* Add reaction to message
*/
public function addReaction(ChatMessage $message, User $user, string $emoji): bool
{
$message->addReaction($emoji, $user->getId());
$this->entityManager->flush();
return true;
}
/**
* Remove reaction from message
*/
public function removeReaction(ChatMessage $message, User $user, string $emoji): bool
{
$message->removeReaction($emoji, $user->getId());
$this->entityManager->flush();
return true;
}
/**
* Mark messages as read
*/
public function markMessagesAsRead(ChatChannel $channel, User $user): void
{
$member = $this->memberRepository->findByChannelAndUser($channel, $user);
if ($member) {
$this->memberRepository->resetUnreadCount($member);
$this->memberRepository->updateLastSeen($member);
}
}
/**
* Search users by email or name
*/
public function searchUsers(string $searchTerm, int $limit = 20): array
{
return $this->userRepository->createQueryBuilder('u')
->where('u.email LIKE :searchTerm OR u.fullName LIKE :searchTerm')
->andWhere('u.active = :active')
->setParameter('searchTerm', '%' . $searchTerm . '%')
->setParameter('active', true)
->orderBy('u.fullName', 'ASC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Get user's channels
*/
public function getUserChannels(User $user): array
{
return $this->channelRepository->findUserChannels($user);
}
/**
* Get channel messages
*/
public function getChannelMessages(ChatChannel $channel, int $limit = 50, int $offset = 0): array
{
return $this->messageRepository->findChannelMessages($channel, $limit, $offset);
}
/**
* Get total message count for a channel
*/
public function getChannelMessageCount(ChatChannel $channel): int
{
return $this->messageRepository->getChannelMessageCount($channel);
}
/**
* Search public channels
*/
public function searchPublicChannels(string $searchTerm): array
{
return $this->channelRepository->findPublicChannelsBySearch($searchTerm);
}
/**
* Get popular public channels
*/
public function getPopularPublicChannels(int $limit = 10): array
{
return $this->channelRepository->findPopularPublicChannels($limit);
}
/**
* Get channel statistics
*/
public function getChannelStats(ChatChannel $channel): array
{
return [
'memberCount' => $channel->getMemberCount(),
'messageCount' => $channel->getMessageCount()
];
}
/**
* Get message statistics
*/
public function getMessageStats(ChatChannel $channel): array
{
return $this->messageRepository->getChannelMessageStats($channel);
}
/**
* Check if user is member of channel
*/
public function isUserMember(ChatChannel $channel, User $user): bool
{
return $this->memberRepository->isUserMember($channel, $user);
}
/**
* Check if user is admin of channel
*/
public function isUserAdmin(ChatChannel $channel, User $user): bool
{
return $this->memberRepository->isUserAdmin($channel, $user);
}
/**
* Get channel members
*/
public function getChannelMembers(ChatChannel $channel): array
{
return $this->memberRepository->findActiveMembers($channel);
}
}

View file

@ -99,6 +99,7 @@ class twigFunctions
$numberOfUnits = floor($hash / $unit);
return $numberOfUnits . ' ' . $text;
}
return '0 کیلوهش';
}
public function getHesabixLastVersionNumber(): string
@ -111,7 +112,8 @@ class twigFunctions
public function systemSettings()
{
return $this->em->getRepository(Settings::class)->findAll()[0];
$settings = $this->em->getRepository(Settings::class)->findAll();
return $settings[0] ?? null;
}
public function getCurrentUrl()
@ -123,14 +125,16 @@ class twigFunctions
{
// اگر پلاگین accpro فعال نباشد، مقدار پیش‌فرض را برمی‌گرداند
if (!$pluginService->isActive('accpro', $bid)) {
return $side === 'left' ? $this->getStaticData('system', 'footerLeft') : $this->getStaticData('system', 'footerRight');
$defaultText = $side === 'left' ? $this->getStaticData('system', 'footerLeft') : $this->getStaticData('system', 'footerRight');
return $defaultText ?? '';
}
// دریافت تنظیمات چاپ
$printOptions = $this->em->getRepository(PrintOptions::class)->findOneBy(['bid' => $bid]);
if (!$printOptions) {
return $side === 'left' ? $this->getStaticData('system', 'footerLeft') : $this->getStaticData('system', 'footerRight');
$defaultText = $side === 'left' ? $this->getStaticData('system', 'footerLeft') : $this->getStaticData('system', 'footerRight');
return $defaultText ?? '';
}
// دریافت متن پانویس بر اساس سمت
@ -138,7 +142,8 @@ class twigFunctions
// اگر متن null یا خالی باشد، مقدار پیش‌فرض را برمی‌گرداند
if ($footerText === null || $footerText === '') {
return $side === 'left' ? $this->getStaticData('system', 'footerLeft') : $this->getStaticData('system', 'footerRight');
$defaultText = $side === 'left' ? $this->getStaticData('system', 'footerLeft') : $this->getStaticData('system', 'footerRight');
return $defaultText ?? '';
}
return $footerText;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View file

@ -74,6 +74,12 @@
},
},
methods: {
openCalculator() {
this.dialog = true;
},
closeCalculator() {
this.dialog = false;
},
handleButtonClick(btn) {
this.handleInput(btn);
this.activeButton = btn;

View file

@ -0,0 +1,214 @@
<template>
<div>
<!-- دیالوگ پاپآپ ماشین حساب -->
<v-dialog v-model="dialog" max-width="350" @keydown="handleKeydown">
<v-card>
<!-- نوار ابزار در بالای ماشین حساب -->
<v-toolbar color="primary" dark>
<v-toolbar-title>ماشین حساب</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="dialog = false" @keyup.enter="dialog = false" tabindex="0">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<!-- نمایشگر ماشین حساب -->
<v-text-field
:value="formattedDisplay"
readonly
class="display mb-4"
variant="outlined"
tabindex="-1"
></v-text-field>
<!-- دکمههای ماشین حساب -->
<v-row class="mt-2">
<v-col cols="3" v-for="btn in buttons" :key="btn" class="pa-1">
<v-btn
:class="{ 'active-btn': activeButton === btn }"
:color="getButtonColor(btn)"
flat
block
height="50"
@click="handleButtonClick(btn)"
@keyup.enter="handleButtonClick(btn)"
tabindex="0"
>
{{ btn }}
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
data() {
return {
dialog: false,
display: "0",
current: "",
previous: "",
operation: null,
activeButton: null,
waitingForPercent: false,
numberButtons: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."], // دکمههای اعداد
operatorButtons: ["+", "-", "*", "/", "%"], // دکمههای عملگر
actionButtons: ["C", "="], // دکمههای عملیاتی
buttons: ["7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", "0", ".", "%", "+", "C", "="],
};
},
computed: {
formattedDisplay() {
if (this.display === "خطا") return this.display;
const num = parseFloat(this.display);
return isNaN(num) ? this.display : num.toLocaleString("fa-IR");
},
},
methods: {
openCalculator() {
this.dialog = true;
},
closeCalculator() {
this.dialog = false;
},
handleButtonClick(btn) {
this.handleInput(btn);
this.activeButton = btn;
setTimeout(() => {
this.activeButton = null;
}, 100);
},
handleInput(btn) {
if (btn === "C") {
this.clear();
} else if (btn === "=") {
this.calculate();
} else if (this.isOperator(btn)) {
this.setOperation(btn);
} else {
this.appendNumber(btn);
}
},
isOperator(btn) {
return this.operatorButtons.includes(btn);
},
appendNumber(btn) {
if (this.current === "0" && btn !== ".") {
this.current = btn;
} else {
this.current += btn;
}
this.display = this.current;
},
setOperation(op) {
if (this.current === "") return;
if (op === "%") {
if (this.operation === "+" || this.operation === "-") {
const base = parseFloat(this.previous);
const percentage = (base * parseFloat(this.current)) / 100;
this.current = percentage.toString();
this.calculate();
} else {
const curr = parseFloat(this.current);
this.current = (curr / 100).toString();
}
} else {
if (this.previous !== "") {
this.calculate();
}
this.operation = op;
this.previous = this.current;
this.current = "";
}
},
calculate() {
if (this.current === "" || this.previous === "") return;
const prev = parseFloat(this.previous);
const curr = parseFloat(this.current);
let result = 0;
switch (this.operation) {
case "+":
result = prev + curr;
break;
case "-":
result = prev - curr;
break;
case "*":
result = prev * curr;
break;
case "/":
if (curr === 0) {
this.display = "خطا";
return;
}
result = prev / curr;
break;
default:
return;
}
this.display = result.toString();
this.current = result.toString();
this.previous = "";
this.operation = null;
},
clear() {
this.display = "0";
this.current = "";
this.previous = "";
this.operation = null;
},
handleKeydown(event) {
const key = event.key;
if (key >= "0" && key <= "9" || key === ".") {
this.appendNumber(key);
} else if (["+", "-", "*", "/", "%"].includes(key)) {
this.setOperation(key);
} else if (key === "Enter" || key === "=") {
this.calculate();
} else if (key === "Escape" || key === "c" || key === "C") {
this.clear();
}
},
getButtonColor(btn) {
if (this.actionButtons.includes(btn)) {
return "error";
} else if (this.operatorButtons.includes(btn)) {
return "primary";
} else {
return "default";
}
},
},
};
</script>
<style scoped>
.display {
font-size: 1.5rem;
text-align: right;
direction: ltr;
margin-bottom: 16px;
}
.active-btn {
transform: scale(0.95);
}
.v-card-text {
padding: 20px;
}
.v-btn {
font-size: 1.2rem;
font-weight: 500;
}
</style>

View file

@ -745,6 +745,7 @@ const fa_lang = {
},
"person_card": {
accounting_status: 'وضعیت حسابداری',
transfer_cheque: 'واگذاری چک',
"title": "کارت حساب اشخاص",
"account_card": "کارت حساب",
"account_status": "وضعیت حساب",

View file

@ -186,6 +186,14 @@ const router = createRouter({
'login': true
}
},
{
path: 'manager/debug',
component: () => import('../views/user/manager/debug/debug.vue'),
meta: {
'title': 'دیباگ سیستم',
'login': true
}
},
{
path: 'manager/changes/mod/:id',
component: () => import('../views/user/manager/reportchange/mod.vue'),
@ -737,6 +745,12 @@ const router = createRouter({
component: () =>
import('../views/wizard/home.vue'),
},
{
path: 'chat/home',
name: 'chat_home',
component: () =>
import('../views/chat/home.vue'),
},
{
path: 'plugin-center/list',
name: 'plugin_center_list',

View file

@ -0,0 +1,431 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import axios from 'axios';
export interface User {
id: number;
fullName: string;
email: string;
}
export interface Channel {
id: number;
channelId: string;
name: string;
description: string;
isPublic: boolean;
messageCount: number;
lastMessageAt?: string;
createdAt: string;
isAdmin: boolean;
}
export interface Message {
id: number;
content: string;
messageType: string;
sentAt: string;
isEdited: boolean;
editedAt?: string;
sender: User;
quotedMessage?: {
id: number;
content: string;
sender: string;
};
reactions: Record<string, number[]>;
attachments: any[];
}
export const useChatStore = defineStore('chat', () => {
// State
const channels = ref<Channel[]>([]);
const currentChannel = ref<Channel | null>(null);
const messages = ref<Message[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
// Computed
const userChannels = computed(() => channels.value);
const currentMessages = computed(() => messages.value);
const isLoading = computed(() => loading.value);
const hasError = computed(() => error.value !== null);
// Actions
const loadUserChannels = async () => {
try {
loading.value = true;
error.value = null;
const response = await axios.get('/api/chat/channels');
if (response.data.success) {
channels.value = response.data.data;
} else {
error.value = 'خطا در بارگذاری کانال‌ها';
}
} catch (err) {
console.error('Error loading channels:', err);
error.value = 'خطا در بارگذاری کانال‌ها';
} finally {
loading.value = false;
}
};
const selectChannel = async (channel: Channel) => {
currentChannel.value = channel;
await loadChannelMessages(channel.channelId);
};
const loadChannelMessages = async (channelId: string) => {
try {
loading.value = true;
error.value = null;
const response = await axios.get(`/api/chat/channels/${channelId}/messages`);
if (response.data.success) {
messages.value = response.data.data.reverse(); // Show newest first
} else {
error.value = 'خطا در بارگذاری پیام‌ها';
}
} catch (err) {
console.error('Error loading messages:', err);
error.value = 'خطا در بارگذاری پیام‌ها';
} finally {
loading.value = false;
}
};
const sendMessage = async (content: string, messageType: string = 'text', quotedMessageId?: number) => {
if (!currentChannel.value) return;
try {
loading.value = true;
error.value = null;
const payload: any = {
content,
messageType
};
if (quotedMessageId) {
payload.quotedMessageId = quotedMessageId;
}
const response = await axios.post(`/api/chat/channels/${currentChannel.value.channelId}/messages`, payload);
if (response.data.success) {
const newMessage = response.data.data;
messages.value.push(newMessage);
return newMessage;
} else {
error.value = 'خطا در ارسال پیام';
}
} catch (err) {
console.error('Error sending message:', err);
error.value = 'خطا در ارسال پیام';
} finally {
loading.value = false;
}
};
const createChannel = async (name: string, description: string, isPublic: boolean) => {
try {
loading.value = true;
error.value = null;
const response = await axios.post('/api/chat/channels', {
name,
description,
isPublic
});
if (response.data.success) {
const newChannel = response.data.data;
channels.value.push(newChannel);
return newChannel;
} else {
error.value = 'خطا در ایجاد کانال';
}
} catch (err) {
console.error('Error creating channel:', err);
error.value = 'خطا در ایجاد کانال';
} finally {
loading.value = false;
}
};
const joinChannel = async (channelId: string) => {
try {
loading.value = true;
error.value = null;
const response = await axios.post(`/api/chat/channels/${channelId}/join`);
if (response.data.success) {
await loadUserChannels(); // Reload channels to include the new one
return true;
} else {
error.value = response.data.message || 'خطا در پیوستن به کانال';
return false;
}
} catch (err) {
console.error('Error joining channel:', err);
error.value = 'خطا در پیوستن به کانال';
return false;
} finally {
loading.value = false;
}
};
const searchChannels = async (searchTerm: string) => {
try {
loading.value = true;
error.value = null;
const response = await axios.get(`/api/chat/channels/search?q=${encodeURIComponent(searchTerm)}`);
if (response.data.success) {
return response.data.data;
} else {
error.value = 'خطا در جستجوی کانال‌ها';
return [];
}
} catch (err) {
console.error('Error searching channels:', err);
error.value = 'خطا در جستجوی کانال‌ها';
return [];
} finally {
loading.value = false;
}
};
const searchUsers = async (searchTerm: string) => {
try {
loading.value = true;
error.value = null;
const response = await axios.get(`/api/chat/users/search?q=${encodeURIComponent(searchTerm)}`);
if (response.data.success) {
return response.data.data;
} else {
error.value = 'خطا در جستجوی کاربران';
return [];
}
} catch (err) {
console.error('Error searching users:', err);
error.value = 'خطا در جستجوی کاربران';
return [];
} finally {
loading.value = false;
}
};
const editMessage = async (messageId: number, newContent: string) => {
try {
loading.value = true;
error.value = null;
const response = await axios.put(`/api/chat/messages/${messageId}/edit`, {
content: newContent
});
if (response.data.success) {
// Update the message in the store
const messageIndex = messages.value.findIndex(m => m.id === messageId);
if (messageIndex !== -1) {
messages.value[messageIndex].content = newContent;
messages.value[messageIndex].isEdited = true;
messages.value[messageIndex].editedAt = new Date().toISOString();
}
return true;
} else {
error.value = response.data.message || 'خطا در ویرایش پیام';
return false;
}
} catch (err) {
console.error('Error editing message:', err);
error.value = 'خطا در ویرایش پیام';
return false;
} finally {
loading.value = false;
}
};
const addReaction = async (messageId: number, emoji: string) => {
try {
loading.value = true;
error.value = null;
const response = await axios.post(`/api/chat/messages/${messageId}/reactions`, {
emoji
});
if (response.data.success) {
// Update the message reactions in the store
const messageIndex = messages.value.findIndex(m => m.id === messageId);
if (messageIndex !== -1) {
if (!messages.value[messageIndex].reactions[emoji]) {
messages.value[messageIndex].reactions[emoji] = [];
}
// Add current user to reactions (you'll need to get current user ID)
const currentUserId = 1; // This should come from auth store
if (!messages.value[messageIndex].reactions[emoji].includes(currentUserId)) {
messages.value[messageIndex].reactions[emoji].push(currentUserId);
}
}
return true;
} else {
error.value = response.data.message || 'خطا در اضافه کردن واکنش';
return false;
}
} catch (err) {
console.error('Error adding reaction:', err);
error.value = 'خطا در اضافه کردن واکنش';
return false;
} finally {
loading.value = false;
}
};
const addMember = async (channelId: string, userId: number) => {
try {
loading.value = true;
error.value = null;
const response = await axios.post(`/api/chat/channels/${channelId}/members`, {
userId
});
if (response.data.success) {
return true;
} else {
error.value = response.data.message || 'خطا در اضافه کردن عضو';
return false;
}
} catch (err) {
console.error('Error adding member:', err);
error.value = 'خطا در اضافه کردن عضو';
return false;
} finally {
loading.value = false;
}
};
const removeMember = async (channelId: string, userId: number) => {
try {
loading.value = true;
error.value = null;
const response = await axios.delete(`/api/chat/channels/${channelId}/members/${userId}`);
if (response.data.success) {
return true;
} else {
error.value = response.data.message || 'خطا در حذف عضو';
return false;
}
} catch (err) {
console.error('Error removing member:', err);
error.value = 'خطا در حذف عضو';
return false;
} finally {
loading.value = false;
}
};
const getChannelMembers = async (channelId: string) => {
try {
loading.value = true;
error.value = null;
const response = await axios.get(`/api/chat/channels/${channelId}/members`);
if (response.data.success) {
return response.data.data;
} else {
error.value = 'خطا در دریافت لیست اعضا';
return [];
}
} catch (err) {
console.error('Error getting channel members:', err);
error.value = 'خطا در دریافت لیست اعضا';
return [];
} finally {
loading.value = false;
}
};
const leaveChannel = async (channelId: string) => {
try {
loading.value = true;
error.value = null;
const response = await axios.post(`/api/chat/channels/${channelId}/leave`);
if (response.data.success) {
// Remove channel from user channels
channels.value = channels.value.filter(c => c.channelId !== channelId);
if (currentChannel.value?.channelId === channelId) {
currentChannel.value = null;
messages.value = [];
}
return true;
} else {
error.value = response.data.message || 'خطا در خروج از کانال';
return false;
}
} catch (err) {
console.error('Error leaving channel:', err);
error.value = 'خطا در خروج از کانال';
return false;
} finally {
loading.value = false;
}
};
const clearError = () => {
error.value = null;
};
const reset = () => {
channels.value = [];
currentChannel.value = null;
messages.value = [];
loading.value = false;
error.value = null;
};
return {
// State
channels,
currentChannel,
messages,
loading,
error,
// Computed
userChannels,
currentMessages,
isLoading,
hasError,
// Actions
loadUserChannels,
selectChannel,
loadChannelMessages,
sendMessage,
createChannel,
joinChannel,
searchChannels,
searchUsers,
editMessage,
addReaction,
addMember,
removeMember,
getChannelMembers,
leaveChannel,
clearError,
reset
};
});

View file

@ -10,9 +10,10 @@ import Notifications_btn from '@/components/application/buttons/notifications_bt
import Year_cob from '@/components/application/combobox/year_cob.vue';
import Currency_cob from '@/components/application/combobox/currency_cob.vue';
import clock from '@/components/application/clock.vue';
import CalculatorButton from '@/components/application/buttons/CalculatorButton.vue'
import CalculatorDialog from '@/components/application/buttons/CalculatorDialog.vue'
import SecretDialog from '@/components/application/buttons/SecretDialog.vue';
import ShortcutsButton from '@/components/application/buttons/ShortcutsButton.vue';
export default {
data() {
return {
@ -292,7 +293,8 @@ export default {
getShortcutKey(path) {
const shortcut = this.shortcuts.find(s => s.path === path);
return shortcut ? shortcut.key : '';
}
},
},
components: {
Profile_btn,
@ -300,7 +302,7 @@ export default {
Year_cob,
Currency_cob,
clock,
CalculatorButton,
CalculatorDialog,
SecretDialog,
ShortcutsButton
}
@ -971,15 +973,35 @@ 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-if="permissions.ai">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn class="" stacked v-bind="props" to="/acc/wizard/home">
<v-icon>mdi-robot</v-icon>
<v-btn stacked v-bind="props">
<v-icon>mdi-toolbox</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-list>
<v-list-item v-if="permissions.ai" to="/acc/wizard/home">
<template v-slot:prepend>
<v-icon>mdi-robot</v-icon>
</template>
<v-list-item-title>هوش مصنوعی</v-list-item-title>
</v-list-item>
<v-list-item to="/acc/chat/home">
<template v-slot:prepend>
<v-icon>mdi-chat</v-icon>
</template>
<v-list-item-title>گفتوگو</v-list-item-title>
</v-list-item>
<v-list-item @click="$refs.calculatorDialog.openCalculator()">
<template v-slot:prepend>
<v-icon>mdi-calculator</v-icon>
</template>
<v-list-item-title>ماشین حساب</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<ShortcutsButton />
<CalculatorButton />
<CalculatorDialog ref="calculatorDialog" />
<SecretDialog />
<v-dialog v-model="showShortcutsDialog" max-width="800" scrollable>
<v-card>
@ -1054,6 +1076,8 @@ export default {
<Notifications_btn />
<Profile_btn />
</v-app-bar>
<v-main>
<div class="position-relative">
<RouterView />

View file

@ -384,6 +384,7 @@ export default {
modify_cheque: '/acc/accounting/view/',
modify_cheque_output: '/acc/accounting/view/',
pass_cheque: '/acc/accounting/view/',
transfer_cheque: '/acc/accounting/view/',
};
return routes[type] + code;
},
@ -403,6 +404,7 @@ export default {
modify_cheque: this.$t('pages.person_card.modify_cheque'),
pass_cheque: this.$t('pages.person_card.pass_cheque'),
modify_cheque_output: this.$t('pages.person_card.modify_cheque_output'),
transfer_cheque: this.$t('pages.person_card.transfer_cheque'),
};
return labels[type] || type;
},

View file

@ -307,10 +307,13 @@ const fetchData = async () => {
const selectedTransactionFilters = transactionFilters.value
.filter((filter) => filter.checked)
.map((filter) => filter.value);
// تبدیل سورتهای Vuetify به فرمت مورد نیاز سرور
const sortBy = serverOptions.value.sortBy.map((sort) => ({
key: sort.key,
order: sort.order === 'asc' ? 'ASC' : 'DESC',
}));
const response = await axios.post('/api/person/list', {
page: serverOptions.value.page,
itemsPerPage: serverOptions.value.rowsPerPage,

View file

@ -121,7 +121,7 @@
<PrintDialog
v-model="modal"
:plugins="plugins"
@print="printInvoice"
@print="handlePrint"
@cancel="modal = false"
/>
<!-- End Print Modal -->
@ -187,10 +187,9 @@
<script>
import axios from "axios";
import { ref, defineComponent } from "vue";
import PrintDialog from '@/components/PrintDialog.vue';
export default defineComponent ({
export default {
name: "list",
components: {
PrintDialog
@ -232,7 +231,7 @@ export default defineComponent ({
sumTotal: 0,
itemsSelected: [],
searchValue: '',
loading: ref(true),
loading: true,
items: [],
orgItems: [],
headers: [
@ -335,13 +334,16 @@ export default defineComponent ({
}
});
},
printInvoice(pdf = true, cloudePrinters = true) {
handlePrint(printOptions) {
this.printInvoice(true, true, printOptions);
},
printInvoice(pdf = true, cloudePrinters = true, printOptions = null) {
this.loading = true;
axios.post('/api/preinvoice/print/invoice', {
'code': this.printOptions.selectedPrintCode,
'pdf': pdf,
'printers': cloudePrinters,
'printOptions': this.printOptions
'printOptions': printOptions || this.printOptions
}).then((response) => {
this.loading = false;
window.open(this.$API_URL + '/front/print/' + response.data.id, '_blank', 'noreferrer');
@ -419,7 +421,7 @@ export default defineComponent ({
deep: false
}
}
})
}
</script>
<style scoped>

View file

@ -1,671 +0,0 @@
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import HomeView from '../views/dashboard.vue'
import PersonHome from '../views/persons/list.vue'
const router = createRouter({
history: createWebHashHistory(
import.meta.env.BASE_URL),
routes: [{
path: '/',
name: 'app_home',
component: HomeView
},
{
path: '/acc/business/printtemplates',
name: 'business_printtemplates',
component: () =>
import ('../views/printers/templates.vue')
},
{
path: '/acc/printers/list',
name: 'printers_list',
component: () =>
import ('../views/printers/list.vue')
}, {
path: '/acc/reports/list',
name: 'reports_list',
component: () =>
import ('../views/reports/reports.vue')
},
{
path: '/acc/reports/persons/debtors',
name: 'person_debtors_list',
component: () =>
import ('../views/reports/persons/debtors.vue'),
},
{
path: '/acc/reports/acc/balance_sheet',
name: 'acc_balanceSheet_list',
component: () =>
import ('../views/reports/balanceSheet.vue'),
},
{
path: '/acc/reports/commodity/buysell',
name: 'commodity_report_buysell',
component: () =>
import ('../views/reports/commodity/buysellByCommodity.vue'),
},
{
path: '/acc/reports/persons/depositors',
name: 'person_depositors_list',
component: () =>
import ('../views/reports/persons/depositors.vue'),
},
{
path: '/acc/reports/persons/buysell',
name: 'person_buysell_by_person',
component: () =>
import ('../views/reports/persons/buysellByPerson.vue'),
},
{
path: '/acc/costs/list',
name: 'costs_list',
component: () =>
import ('../views/costs/list.vue'),
},
{
path: '/acc/costs/mod/:id?',
name: 'costs_mod',
component: () =>
import ('../views/costs/mod.vue'),
},
{
path: '/acc/incomes/list',
name: 'incomes_list',
component: () =>
import ('../views/incomes/list.vue'),
},
{
path: '/acc/incomes/mod/:id?',
name: 'incomes_mod',
component: () =>
import ('../views/incomes/mod.vue'),
},
{
path: '/acc/accounting/list',
name: 'accounting_list_doc',
component: () =>
import ('../views/accounting/list.vue'),
},
{
path: '/acc/accounting/table',
name: 'accounting_table',
component: () =>
import ('../views/accounting/table.vue'),
},
{
path: '/acc/accounting/close_year',
name: 'accounting_close_year',
component: () =>
import ('../views/accounting/closeyear.vue'),
},
{
path: '/acc/accounting/view/:id?',
name: 'accounting_view_doc',
component: () =>
import ('../views/accounting/viewDoc.vue'),
},
{
path: '/acc/banks/list',
name: 'banks_list',
component: () =>
import ('../views/bank/list.vue')
},
{
path: '/acc/banks/card/view/:id?',
name: 'bank_card_view',
component: () =>
import ('../views/bank/card.vue'),
},
{
path: '/acc/banks/mod/:id?',
name: 'bank_mod',
component: () =>
import ('../views/bank/mod.vue'),
},
{
path: '/acc/salary/list',
name: 'salary_list',
component: () =>
import ('../views/salary/list.vue')
},
{
path: '/acc/salary/card/view/:id?',
name: 'salary_card_view',
component: () =>
import ('../views/salary/card.vue'),
},
{
path: '/acc/salary/mod/:id?',
name: 'salary_mod',
component: () =>
import ('../views/salary/mod.vue'),
},
{
path: '/acc/wallet/view',
name: 'wallet_view',
component: () =>
import ('../views/wallet/view.vue')
},
{
path: '/acc/cashdesk/list',
name: 'cashdesk_list',
component: () =>
import ('../views/cashdesk/list.vue')
},
{
path: '/acc/cashdesk/card/view/:id?',
name: 'cashdesk_card_view',
component: () =>
import ('../views/cashdesk/card.vue'),
},
{
path: '/acc/cashdesk/mod/:id?',
name: 'cashdesk_mod',
component: () =>
import ('../views/cashdesk/mod.vue'),
},
{
path: '/acc/transfer/list',
name: 'transfer_list',
component: () =>
import ('../views/transfer/list.vue')
},
{
path: '/acc/transfer/mod/:id?',
name: 'transfer_mod',
component: () =>
import ('../views/transfer/mod.vue'),
},
{
path: '/acc/persons/receive/list',
name: 'person_receive_list',
component: () =>
import ('../views/persons/receive/list.vue')
},
{
path: '/acc/persons/send/list',
name: 'person_send_list',
component: () =>
import ('../views/persons/send/list.vue')
},
{
path: '/acc/persons/receive/mod/:id?',
name: 'person_receive_mod',
component: () =>
import ('../views/persons/receive/mod.vue'),
},
{
path: '/acc/persons/send/mod/:id?',
name: 'person_send_mod',
component: () =>
import ('../views/persons/send/mod.vue'),
},
{
path: '/acc/persons/card/view/:id?',
name: 'person_card_view',
component: () =>
import ('../views/persons/card.vue'),
},
{
path: '/acc/persons/list',
name: 'person_list',
component: PersonHome
},
{
path: '/acc/persons/mod/:id?',
name: 'person_new',
component: () =>
import ('../views/persons/insert.vue'),
},
{
path: '/acc/business/settings',
name: 'business_settings',
component: () =>
import ('../views/settings/bussiness.vue'),
},
{
path: '/acc/business/printoptions',
name: 'print_settings',
component: () =>
import ('../views/settings/print.vue'),
},
{
path: '/acc/business/avatar',
name: 'business_avatar',
component: () =>
import ('../views/settings/avatar.vue'),
},
{
path: '/acc/business/extramoneys',
name: 'business_extramoneys',
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',
component: () =>
import ('../views/settings/logs.vue'),
},
{
path: '/acc/business/apis',
name: 'business_apis',
component: () =>
import ('../views/api/list.vue'),
},
{
path: '/acc/business/users',
name: 'business_users',
component: () =>
import ('../views/settings/user_rolls.vue'),
},
{
path: '/acc/business/user/roll/edit/:email',
name: 'business_user_roll_edit',
component: () =>
import ('../views/settings/user_perm_edit.vue'),
},
{
path: '/acc/commodity/cat/list',
name: 'commodity_cat_list',
component: () =>
import ('../views/commodity/cat/list.vue'),
},
{
path: '/acc/commodity/pricelist/list',
name: 'commodity_pricelist_list',
component: () =>
import ('../views/commodity/priceList/list.vue'),
},
{
path: '/acc/commodity/pricelist/mod/:id?',
name: 'commodity_pricelist_mod',
component: () =>
import ('../views/commodity/priceList/mod.vue'),
},
{
path: '/acc/commodity/pricelist/view/:id?',
name: 'commodity_pricelist_view',
component: () =>
import ('../views/commodity/priceList/view.vue'),
}, {
path: '/acc/commodity/pricelist/list/mod/:id?',
name: 'commodity_pricelist_list_mod',
component: () =>
import ('../views/commodity/priceList/pricelistedit.vue'),
},
{
path: '/acc/commodity/drop/list',
name: 'commodity_drop_list',
component: () =>
import ('../views/commodity/drop/list.vue'),
},
{
path: '/acc/commodity/drop/mod/:id?',
name: 'commodity_drop_mod',
component: () =>
import ('../views/commodity/drop/mod.vue'),
},
{
path: '/acc/commodity/list',
name: 'commodity_list',
component: () =>
import ('../views/commodity/list.vue'),
},
{
path: '/acc/commodity/mod/:id?',
name: 'commodity_mod',
component: () =>
import ('../views/commodity/mod.vue'),
},
{
path: '/acc/cheque/mod/:id?',
name: 'cheque_mod',
component: () =>
import ('../views/cheque/mod.vue'),
},
{
path: '/acc/cheque/list',
name: 'cheque_list',
component: () =>
import ('../views/cheque/list.vue'),
},
{
path: '/acc/buy/mod/:id?',
name: 'buy_mod',
component: () =>
import ('../views/buy/mod.vue'),
},
{
path: '/acc/buy/list',
name: 'buy_list',
component: () =>
import ('../views/buy/list.vue'),
},
{
path: '/acc/buy/view/:id?',
name: 'buy_view',
component: () =>
import ('../views/buy/viewInvoice.vue'),
},
{
path: '/acc/sell/mod/:id?',
name: 'sell_mod',
component: () =>
import ('../views/sell/mod.vue'),
},
{
path: '/acc/sell/fast-mod/:id?',
name: 'sell_fast_mod',
component: () =>
import ('../views/sell/fastMod.vue'),
},
{
path: '/acc/sell/list',
name: 'sell_list',
component: () =>
import ('../views/sell/list.vue'),
},
{
path: '/acc/sell/view/:id?',
name: 'sell_view',
component: () =>
import ('../views/sell/viewInvoice.vue'),
},
{
path: '/acc/presell/mod/:id?',
name: 'presell_mod',
component: () =>
import ('../views/presell/mod.vue'),
},
{
path: '/acc/presell/list',
name: 'presell_list',
component: () =>
import ('../views/presell/list.vue'),
},
{
path: '/acc/presell/view/:id?',
name: 'presell_view',
component: () =>
import ('../views/presell/viewInvoice.vue'),
},
{
path: '/acc/rfbuy/mod/:id?',
name: 'rfbuy_mod',
component: () =>
import ('../views/rfbuy/mod.vue'),
},
{
path: '/acc/rfbuy/list',
name: 'rfbuy_list',
component: () =>
import ('../views/rfbuy/list.vue'),
},
{
path: '/acc/rfbuy/view/:id?',
name: 'rfbuy_view',
component: () =>
import ('../views/rfbuy/viewInvoice.vue'),
},
{
path: '/acc/rfsell/mod/:id?',
name: 'rfsell_mod',
component: () =>
import ('../views/rfsell/mod.vue'),
},
{
path: '/acc/rfsell/list',
name: 'rfsell_list',
component: () =>
import ('../views/rfsell/list.vue'),
},
{
path: '/acc/rfsell/view/:id?',
name: 'rfsell_view',
component: () =>
import ('../views/rfsell/viewInvoice.vue'),
},
{
path: '/acc/plugin-center/list',
name: 'plugin_center_list',
component: () =>
import ('../views/store/plugin-world.vue'),
},
{
path: '/acc/plugin-center/my',
name: 'plugin_center_my',
component: () =>
import ('../views/store/plugin-my.vue'),
},
{
path: '/acc/plugin-center/invoice',
name: 'plugin_center_invoice',
component: () =>
import ('../views/store/plugin-invoice.vue'),
},
{
path: '/acc/plugin-center/view-end/:id?',
name: 'plugin_center_view_prodect',
component: () =>
import ('../views/store/viewProdect.vue'),
},
{
path: '/acc/plugins/apartemanma/intro',
name: 'plugin_apartemanma_intro',
component: () =>
import ('../views/plugins/amartemanma/intro.vue'),
},
{
path: '/acc/plugins/accpro/intro',
name: 'plugin_accpro_intro',
component: () =>
import ('../views/plugins/accpro/intro.vue'),
},
{
path: '/acc/plugins/repservice/intro',
name: 'plugin_repservice_intro',
component: () =>
import ('../views/plugins/repservice/intro.vue'),
}, {
path: '/acc/plugin/repservice/order/mod/:id?',
name: 'plugin_repservice_order_mod',
component: () =>
import ('../views/plugins/repservice/mod.vue'),
},
{
path: '/acc/plugin/repservice/order/view/:id?',
name: 'plugin_repservice_order_view',
component: () =>
import ('../views/plugins/repservice/view.vue'),
},
{
path: '/acc/plugin/repservice/order/list',
name: 'plugin_repservice_order_list',
component: () =>
import ('../views/plugins/repservice/list.vue'),
},
{
path: '/acc/plugins/restamap/intro',
name: 'plugin_restamap_intro',
component: () =>
import ('../views/plugins/resamap/intro.vue'),
},
{
path: '/acc/plugins/noghre/intro',
name: 'plugin_noghre_intro',
component: () =>
import ('../views/plugins/noghre/intro.vue'),
},
{
path: '/acc/plugins/cc/intro',
name: 'plugin_cc_intro',
component: () =>
import ('../views/plugins/cc/intro.vue'),
},
{
path: '/acc/plugins/onlinestore/intro',
name: 'plugin_onlinestore_intro',
component: () =>
import ('../views/plugins/onlinestore/intro.vue'),
},
{
path: '/acc/notifications/list',
name: 'notification_list',
component: () =>
import ('../views/notifications/notifications.vue'),
},
{
path: '/acc/sms/panel',
name: 'sms_panel_dashboard',
component: () =>
import ('../views/smspanel/smspanel.vue'),
},
{
path: '/acc/plugin/noghre/employees/list',
name: 'plugin_noghre_employees_list',
component: () =>
import ('../views/plugins/noghre/employess/list.vue'),
},
{
path: '/acc/plugin/noghre/employees/mod/:id?',
name: 'plugin_noghre_employees_mod',
component: () =>
import ('../views/plugins/noghre/employess/mod.vue'),
},
{
path: '/acc/plugin/noghre/order/list',
name: 'plugin_noghre_order_list',
component: () =>
import ('../views/plugins/noghre/order/list.vue'),
},
{
path: '/acc/plugin/noghre/order/mod/:id?',
name: 'plugin_noghre_order_mod',
component: () =>
import ('../views/plugins/noghre/order/mod.vue'),
},
{
path: '/acc/plugin/noghre/order/view/:id?',
name: 'plugin_noghre_order_view',
component: () =>
import ('../views/plugins/noghre/order/view.vue'),
},
{
path: '/acc/plugin/noghre/pays/view/:id?',
name: 'plugin_noghre_pays_view',
component: () =>
import ('../views/plugins/noghre/pays.vue'),
},
{
path: '/acc/storeroom/commodity/check/exist',
name: 'storeroom_commodity_check_exist',
component: () =>
import ('../views/storeroom/commodityCheck/checkByStoreroom.vue'),
},
{
path: '/acc/storeroom/new/ticket/type',
name: 'storeroom_new_ticket_type',
component: () =>
import ('../views/storeroom/io/modalNew.vue'),
},
{
path: '/acc/storeroom/tickets/list',
name: 'storeroom_tickets_list',
component: () =>
import ('../views/storeroom/io/ticketList.vue'),
},
{
path: '/acc/storeroom/ticket/view/:id',
name: 'storeroom_ticket_view',
component: () =>
import ('../views/storeroom/io/view.vue'),
},
{
path: '/acc/storeroom/new/ticket/buy/:doc/:storeID',
name: 'storeroom_new_ticket_buy',
component: () =>
import ('../views/storeroom/io/buy.vue'),
},
{
path: '/acc/storeroom/new/ticket/sell/:doc/:storeID',
name: 'storeroom_new_ticket_sell',
component: () =>
import ('../views/storeroom/io/sell.vue'),
},
{
path: '/acc/storeroom/new/ticket/rfbuy/:doc/:storeID',
name: 'storeroom_new_ticket_rfbuy',
component: () =>
import ('../views/storeroom/io/rfbuy.vue'),
},
{
path: '/acc/storeroom/new/ticket/rfsell/:doc/:storeID',
name: 'storeroom_new_ticket_rfsell',
component: () =>
import ('../views/storeroom/io/rfsell.vue'),
},
{
path: '/acc/storeroom/list',
name: 'storeroom_list',
component: () =>
import ('../views/storeroom/list.vue'),
},
{
path: '/acc/storeroom/mod/:id?',
name: 'storeroom_mod',
component: () =>
import ('../views/storeroom/mod.vue'),
},
{
path: '/acc/archive/list',
name: 'archive_list',
component: () =>
import ('../views/archive/view_files.vue'),
},
{
path: '/acc/archive/order/new',
name: 'order_new',
component: () =>
import ('../views/archive/order_new.vue'),
},
{
path: '/acc/archive/order/list',
name: 'order_list',
component: () =>
import ('../views/archive/orders_list.vue'),
},
{
path: '/acc/shareholders/list',
name: 'shareholders_list',
component: () =>
import ('../views/shareholder/list.vue'),
},
{
path: "/:catchAll(.*)",
name: "not-found",
component: () =>
import ("../views/NotFound.vue"),
meta: {
'title': 'صفحه یافت نشد',
}
},
]
})
router.beforeEach((to, from) => {
const width = Math.max(
document.documentElement.clientWidth,
window.innerWidth || 0
)
if (width <= 992) {
Dashmix.layout('sidebar_close');
}
return true
})
export default router

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,669 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<div>
<v-icon class="mr-2">mdi-bug</v-icon>
مدیریت دیباگ سیستم
<v-chip
v-if="environment"
:color="getEnvironmentColor(environment)"
size="small"
class="ml-2"
>
{{ environment.toUpperCase() }}
</v-chip>
</div>
<div class="d-flex align-center">
<v-btn
color="error"
variant="outlined"
:disabled="selectedLogs.length === 0"
prepend-icon="mdi-delete"
class="mr-2"
@click="showDeleteDialog = true"
>
حذف انتخاب شده ({{ selectedLogs.length }})
</v-btn>
<v-btn
color="warning"
variant="outlined"
prepend-icon="mdi-delete-sweep"
@click="showDeleteAllDialog = true"
>
حذف همه
</v-btn>
</div>
</v-card-title>
<!-- فیلترها -->
<v-card-text>
<v-row>
<v-col cols="12" md="3">
<v-text-field
v-model="filters.search"
label="جستجو"
prepend-icon="mdi-magnify"
clearable
@update:model-value="debouncedLoadLogs"
/>
</v-col>
<v-col cols="12" md="2">
<v-select
v-model="filters.level"
label="سطح لاگ"
:items="logLevels"
clearable
@update:model-value="loadLogs"
/>
</v-col>
<v-col cols="12" md="2">
<v-text-field
v-model="filters.date"
label="تاریخ"
type="date"
clearable
@update:model-value="loadLogs"
/>
</v-col>
<v-col cols="12" md="2">
<v-select
v-model="pagination.limit"
label="تعداد در صفحه"
:items="[10, 25, 50, 100]"
@update:model-value="loadLogs"
/>
</v-col>
</v-row>
</v-card-text>
<!-- اطلاعات سیستم -->
<v-card-text v-if="systemInfo">
<v-alert
type="info"
variant="tonal"
class="mb-4"
>
<div>
<strong>اطلاعات سیستم:</strong>
محیط: <v-chip :color="getEnvironmentColor(systemInfo.environment)" size="small">{{ systemInfo.environment.toUpperCase() }}</v-chip> |
فایلهای لاگ: {{ systemInfo.log_files_count }} |
حجم کل: {{ formatBytes(systemInfo.log_files_size) }} |
حافظه استفاده شده: {{ formatBytes(systemInfo.memory_usage) }}
</div>
</v-alert>
</v-card-text>
<!-- جدول لاگها -->
<v-data-table
v-model="selectedLogs"
:headers="headers"
:items="logs"
:loading="loading"
:items-per-page="pagination.limit"
:page="pagination.page"
:total-items="pagination.total"
:sort-by="sortBy"
:sort-desc="sortDesc"
show-select
item-key="id"
class="elevation-1"
@update:options="handleTableUpdate"
>
<template v-slot:item.level="{ item }">
<v-chip
:color="getLevelColor(item.level)"
size="small"
variant="flat"
>
{{ item.level }}
</v-chip>
</template>
<template v-slot:item.timestamp="{ item }">
<div>
<div class="text-body-2">{{ item.date }}</div>
<div class="text-caption text-grey">{{ item.time }}</div>
</div>
</template>
<template v-slot:item.message="{ item }">
<div class="text-truncate" style="max-width: 300px;">
{{ item.message }}
</div>
</template>
<template v-slot:item.environment="{ item }">
<v-chip
:color="getEnvironmentColor(item.environment)"
size="small"
variant="flat"
>
{{ item.environment.toUpperCase() }}
</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
size="small"
color="primary"
variant="text"
@click="viewLogDetail(item)"
prepend-icon="mdi-eye"
>
مشاهده
</v-btn>
</template>
</v-data-table>
<!-- صفحهبندی -->
<v-card-actions class="justify-center">
<v-pagination
v-model="pagination.page"
:length="pagination.totalPages"
:total-visible="7"
@update:model-value="loadLogs"
/>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<!-- دیالوگ جزئیات لاگ -->
<v-dialog v-model="showDetailDialog" max-width="800px">
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-file-document</v-icon>
جزئیات لاگ
<v-chip
v-if="selectedLog?.environment"
:color="getEnvironmentColor(selectedLog.environment)"
size="small"
class="ml-2"
>
{{ selectedLog.environment.toUpperCase() }}
</v-chip>
</v-card-title>
<v-card-text>
<v-row v-if="selectedLog">
<v-col cols="12" md="6">
<v-list>
<v-list-item>
<template v-slot:prepend>
<v-icon>mdi-calendar</v-icon>
</template>
<v-list-item-title>تاریخ</v-list-item-title>
<v-list-item-subtitle>{{ selectedLog.date }} {{ selectedLog.time }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon>mdi-alert-circle</v-icon>
</template>
<v-list-item-title>سطح</v-list-item-title>
<v-list-item-subtitle>
<v-chip :color="getLevelColor(selectedLog.level)" size="small">
{{ selectedLog.level }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon>mdi-file</v-icon>
</template>
<v-list-item-title>فایل</v-list-item-title>
<v-list-item-subtitle>{{ selectedLog.filename }}</v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="selectedLog.environment">
<template v-slot:prepend>
<v-icon>mdi-server</v-icon>
</template>
<v-list-item-title>محیط</v-list-item-title>
<v-list-item-subtitle>
<v-chip :color="getEnvironmentColor(selectedLog.environment)" size="small">
{{ selectedLog.environment.toUpperCase() }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-col>
<v-col cols="12" md="6">
<v-textarea
v-model="selectedLog.message"
label="پیام"
readonly
rows="4"
variant="outlined"
/>
</v-col>
<v-col cols="12" v-if="selectedLog.extra && Object.keys(selectedLog.extra).length > 0">
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-title>
اطلاعات اضافی
</v-expansion-panel-title>
<v-expansion-panel-text>
<pre class="text-body-2">{{ JSON.stringify(selectedLog.extra, null, 2) }}</pre>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
<v-col cols="12">
<v-textarea
v-model="selectedLog.raw"
label="متن خام"
readonly
rows="6"
variant="outlined"
/>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" @click="showDetailDialog = false">
بستن
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- دیالوگ حذف انتخاب شده -->
<v-dialog v-model="showDeleteDialog" max-width="400px">
<v-card>
<v-card-title class="text-h6">
<v-icon class="mr-2" color="error">mdi-delete</v-icon>
حذف لاگهای انتخاب شده
</v-card-title>
<v-card-text>
آیا از حذف {{ selectedLogs.length }} لاگ انتخاب شده اطمینان دارید؟
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showDeleteDialog = false">انصراف</v-btn>
<v-btn color="error" @click="deleteSelectedLogs" :loading="deleting">
حذف
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- دیالوگ حذف همه -->
<v-dialog v-model="showDeleteAllDialog" max-width="400px">
<v-card>
<v-card-title class="text-h6">
<v-icon class="mr-2" color="warning">mdi-delete-sweep</v-icon>
حذف تمام لاگها
</v-card-title>
<v-card-text>
آیا از حذف تمام لاگهای سیستم اطمینان دارید؟ این عملیات غیرقابل بازگشت است.
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showDeleteAllDialog = false">انصراف</v-btn>
<v-btn color="warning" @click="deleteAllLogs" :loading="deleting">
حذف همه
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- اسنکبار -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script>
import { ref, reactive, onMounted, computed } from 'vue'
import axios from 'axios'
export default {
name: 'Debug',
setup() {
const loading = ref(false)
const loadingSystemInfo = ref(false)
const deleting = ref(false)
const logs = ref([])
const selectedLogs = ref([])
const selectedLog = ref(null)
const showDetailDialog = ref(false)
const showDeleteDialog = ref(false)
const showDeleteAllDialog = ref(false)
const systemInfo = ref(null)
const environment = ref('')
const filters = reactive({
search: '',
level: '',
date: ''
})
const pagination = reactive({
page: 1,
limit: 50,
total: 0,
totalPages: 0
})
const snackbar = reactive({
show: false,
message: '',
color: 'success',
timeout: 3000
})
// متغیرهای مرتبسازی
const sortBy = ref(['timestamp'])
const sortDesc = ref([true])
const headers = [
{ title: 'تاریخ', key: 'timestamp', sortable: true },
{ title: 'سطح', key: 'level', sortable: true },
{ title: 'پیام', key: 'message', sortable: false },
{ title: 'فایل', key: 'filename', sortable: true },
{ title: 'محیط', key: 'environment', sortable: true },
{ title: 'عملیات', key: 'actions', sortable: false }
]
const logLevels = [
'DEBUG',
'INFO',
'WARNING',
'ERROR',
'CRITICAL',
'ALERT',
'EMERGENCY'
]
// Debounce برای جستجو
let searchTimeout = null
const debouncedLoadLogs = () => {
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
pagination.page = 1
loadLogs()
}, 500)
}
const getLevelColor = (level) => {
const colors = {
'DEBUG': 'grey',
'INFO': 'blue',
'WARNING': 'orange',
'ERROR': 'red',
'CRITICAL': 'red-darken-2',
'ALERT': 'red-darken-3',
'EMERGENCY': 'red-darken-4'
}
return colors[level] || 'grey'
}
const getEnvironmentColor = (env) => {
const colors = {
'dev': 'green',
'prod': 'red',
'test': 'orange'
}
return colors[env] || 'blue'
}
const formatBytes = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const loadLogs = async () => {
try {
loading.value = true
// Get current sorting values
const currentSortBy = sortBy.value[0] || 'timestamp'
const currentSortDesc = sortDesc.value[0] || true
// Create simple params object
const params = {
page: pagination.page,
limit: pagination.limit,
search: filters.search || '',
level: filters.level || '',
date: filters.date || '',
sortBy: currentSortBy,
sortDesc: currentSortDesc
}
console.log('Sending params:', params)
const response = await axios.get('/api/admin/debug/logs', { params })
if (response.data.success) {
logs.value = response.data.data
pagination.total = response.data.total
pagination.totalPages = response.data.totalPages
environment.value = response.data.environment
} else {
showSnackbar('خطا در دریافت لاگ‌ها', 'error')
}
} catch (error) {
console.error('Error loading logs:', error)
showSnackbar('خطا در دریافت لاگ‌ها', 'error')
} finally {
loading.value = false
}
}
const loadSystemInfo = async () => {
try {
loadingSystemInfo.value = true
const response = await axios.get('/api/admin/debug/system-info')
if (response.data.success) {
systemInfo.value = response.data.data
environment.value = response.data.data.environment
}
} catch (error) {
console.error('Error loading system info:', error)
} finally {
loadingSystemInfo.value = false
}
}
const handleTableUpdate = (options) => {
try {
console.log('Table update options:', options)
let shouldReload = false
// بررسی تغییرات صفحهبندی
if (options && options.page !== undefined && options.page !== pagination.page) {
pagination.page = options.page
shouldReload = true
}
// بررسی تغییرات مرتبسازی
if (options && options.sortBy && Array.isArray(options.sortBy) && options.sortBy.length > 0) {
const newSortBy = options.sortBy[0]
// بررسی وجود sortDesc و مقدار آن
const newSortDesc = options.sortDesc && Array.isArray(options.sortDesc) && options.sortDesc.length > 0
? options.sortDesc[0]
: true
console.log('Sorting changed:', { newSortBy, newSortDesc, currentSortBy: sortBy.value[0], currentSortDesc: sortDesc.value[0] })
if (newSortBy !== sortBy.value[0] || newSortDesc !== sortDesc.value[0]) {
sortBy.value = [newSortBy]
sortDesc.value = [newSortDesc]
shouldReload = true
}
}
if (shouldReload) {
loadLogs()
}
} catch (error) {
console.error('Error in handleTableUpdate:', error)
}
}
const viewLogDetail = async (log) => {
try {
const response = await axios.get(`/api/admin/debug/logs/${log.id}`)
if (response.data.success) {
selectedLog.value = response.data.data
showDetailDialog.value = true
} else {
showSnackbar('خطا در دریافت جزئیات لاگ', 'error')
}
} catch (error) {
console.error('Error loading log detail:', error)
showSnackbar('خطا در دریافت جزئیات لاگ', 'error')
}
}
const deleteSelectedLogs = async () => {
try {
deleting.value = true
const logIds = selectedLogs.value.map(log => log.id)
const response = await axios.delete('/api/admin/debug/logs', {
data: { ids: logIds }
})
if (response.data.success) {
showSnackbar(response.data.message, 'success')
selectedLogs.value = []
pagination.page = 1
loadLogs()
loadSystemInfo()
} else {
showSnackbar('خطا در حذف لاگ‌ها', 'error')
}
} catch (error) {
console.error('Error deleting logs:', error)
showSnackbar('خطا در حذف لاگ‌ها', 'error')
} finally {
deleting.value = false
showDeleteDialog.value = false
}
}
const deleteAllLogs = async () => {
try {
deleting.value = true
const response = await axios.delete('/api/admin/debug/logs', {
data: { deleteAll: true }
})
if (response.data.success) {
showSnackbar(response.data.message, 'success')
selectedLogs.value = []
pagination.page = 1
loadLogs()
loadSystemInfo()
} else {
showSnackbar('خطا در حذف لاگ‌ها', 'error')
}
} catch (error) {
console.error('Error deleting all logs:', error)
showSnackbar('خطا در حذف لاگ‌ها', 'error')
} finally {
deleting.value = false
showDeleteAllDialog.value = false
}
}
const showSnackbar = (message, color = 'success') => {
snackbar.message = message
snackbar.color = color
snackbar.show = true
}
onMounted(() => {
loadLogs()
loadSystemInfo()
})
return {
loading,
loadingSystemInfo,
deleting,
logs,
selectedLogs,
selectedLog,
showDetailDialog,
showDeleteDialog,
showDeleteAllDialog,
systemInfo,
environment,
filters,
pagination,
snackbar,
headers,
logLevels,
getLevelColor,
getEnvironmentColor,
formatBytes,
loadLogs,
loadSystemInfo,
handleTableUpdate,
viewLogDetail,
deleteSelectedLogs,
deleteAllLogs,
showSnackbar,
debouncedLoadLogs,
sortBy,
sortDesc
}
}
}
</script>
<style scoped>
.v-data-table {
border-radius: 8px;
}
.v-chip {
font-weight: 500;
}
pre {
background-color: #f5f5f5;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
}
</style>

View file

@ -113,6 +113,7 @@ export default defineComponent({
{ text: 'تاریخچه سیستم', url: '/profile/manager/logs/list', icon: 'mdi-history', visible: true },
{ text: 'کیف پول', url: '/profile/manager/wallet/list', icon: 'mdi-wallet', visible: true },
{ text: 'اطلاعیه‌ها', url: '/profile/manager/statments/list', icon: 'mdi-bell', visible: true },
{ text: 'دیباگ سیستم', url: '/profile/manager/debug', icon: 'mdi-bug', visible: true },
],
adminSettings: [
{ text: 'پیامک', url: '/profile/manager/system/sms/settings', icon: 'mdi-message-alert', visible: true },