From a7636fbc42f9585144231bee7fa62768d188062b Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Sun, 3 Aug 2025 12:38:15 +0000 Subject: [PATCH] add chat system --- AI_PERSON_INTEGRATION.md | 127 -- .../migrations/Version20241201000000.php | 79 +- hesabixCore/src/Controller/ChatController.php | 619 ++++++ hesabixCore/src/Entity/ChatChannel.php | 268 +++ hesabixCore/src/Entity/ChatChannelMember.php | 137 ++ hesabixCore/src/Entity/ChatMessage.php | 274 +++ .../ChatChannelMemberRepository.php | 196 ++ .../src/Repository/ChatChannelRepository.php | 172 ++ .../src/Repository/ChatMessageRepository.php | 176 ++ hesabixCore/src/Service/ChatService.php | 316 ++++ .../application/buttons/CalculatorButton.vue | 6 + .../application/buttons/CalculatorDialog.vue | 214 +++ webUI/src/router/index.ts | 6 + webUI/src/stores/chatStore.ts | 431 +++++ webUI/src/views/acc/App.vue | 40 +- webUI/src/views/acc/router/index.js | 671 ------- webUI/src/views/chat/home.vue | 1653 +++++++++++++++++ 17 files changed, 4572 insertions(+), 813 deletions(-) delete mode 100644 AI_PERSON_INTEGRATION.md create mode 100644 hesabixCore/src/Controller/ChatController.php create mode 100644 hesabixCore/src/Entity/ChatChannel.php create mode 100644 hesabixCore/src/Entity/ChatChannelMember.php create mode 100644 hesabixCore/src/Entity/ChatMessage.php create mode 100644 hesabixCore/src/Repository/ChatChannelMemberRepository.php create mode 100644 hesabixCore/src/Repository/ChatChannelRepository.php create mode 100644 hesabixCore/src/Repository/ChatMessageRepository.php create mode 100644 hesabixCore/src/Service/ChatService.php create mode 100644 webUI/src/components/application/buttons/CalculatorDialog.vue create mode 100644 webUI/src/stores/chatStore.ts delete mode 100755 webUI/src/views/acc/router/index.js create mode 100644 webUI/src/views/chat/home.vue diff --git a/AI_PERSON_INTEGRATION.md b/AI_PERSON_INTEGRATION.md deleted file mode 100644 index 0ea5125..0000000 --- a/AI_PERSON_INTEGRATION.md +++ /dev/null @@ -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 مرورگر و لاگ‌های سرور ثبت می‌شوند. - -## پشتیبانی - -برای گزارش مشکلات یا درخواست ویژگی‌های جدید، لطفاً با تیم توسعه تماس بگیرید. \ No newline at end of file diff --git a/hesabixCore/migrations/Version20241201000000.php b/hesabixCore/migrations/Version20241201000000.php index dfaaa62..7c3f4ed 100644 --- a/hesabixCore/migrations/Version20241201000000.php +++ b/hesabixCore/migrations/Version20241201000000.php @@ -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'); } } \ No newline at end of file diff --git a/hesabixCore/src/Controller/ChatController.php b/hesabixCore/src/Controller/ChatController.php new file mode 100644 index 0000000..f2c2300 --- /dev/null +++ b/hesabixCore/src/Controller/ChatController.php @@ -0,0 +1,619 @@ +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(), + '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' => $this->chatService->getChannelStats($channel)['memberCount'], + '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', 50); + $offset = (int) $request->query->get('offset', 0); + + $messages = $this->chatService->getChannelMessages($channel, $limit, $offset); + + $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 + ]); + } + + #[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(), + ], + ] + ]); + } 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('/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, + ] + ]); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/ChatChannel.php b/hesabixCore/src/Entity/ChatChannel.php new file mode 100644 index 0000000..75ed9aa --- /dev/null +++ b/hesabixCore/src/Entity/ChatChannel.php @@ -0,0 +1,268 @@ +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 + */ + 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 + */ + 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 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(); + }); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/ChatChannelMember.php b/hesabixCore/src/Entity/ChatChannelMember.php new file mode 100644 index 0000000..b50e751 --- /dev/null +++ b/hesabixCore/src/Entity/ChatChannelMember.php @@ -0,0 +1,137 @@ +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; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/ChatMessage.php b/hesabixCore/src/Entity/ChatMessage.php new file mode 100644 index 0000000..c4e1204 --- /dev/null +++ b/hesabixCore/src/Entity/ChatMessage.php @@ -0,0 +1,274 @@ +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'; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/ChatChannelMemberRepository.php b/hesabixCore/src/Repository/ChatChannelMemberRepository.php new file mode 100644 index 0000000..90d712d --- /dev/null +++ b/hesabixCore/src/Repository/ChatChannelMemberRepository.php @@ -0,0 +1,196 @@ + + * + * @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(); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/ChatChannelRepository.php b/hesabixCore/src/Repository/ChatChannelRepository.php new file mode 100644 index 0000000..c2dc77c --- /dev/null +++ b/hesabixCore/src/Repository/ChatChannelRepository.php @@ -0,0 +1,172 @@ + + * + * @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(); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/ChatMessageRepository.php b/hesabixCore/src/Repository/ChatMessageRepository.php new file mode 100644 index 0000000..549ea2d --- /dev/null +++ b/hesabixCore/src/Repository/ChatMessageRepository.php @@ -0,0 +1,176 @@ + + * + * @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 message statistics for a channel + */ + public function getChannelMessageStats(ChatChannel $channel): array + { + $qb = $this->createQueryBuilder('m') + ->select('COUNT(m.id) as total, COUNT(CASE WHEN m.messageType = :emoji THEN 1 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(); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/ChatService.php b/hesabixCore/src/Service/ChatService.php new file mode 100644 index 0000000..d37cdc6 --- /dev/null +++ b/hesabixCore/src/Service/ChatService.php @@ -0,0 +1,316 @@ +setName($name); + $channel->setDescription($description); + $channel->setIsPublic($isPublic); + $channel->setCreatedBy($creator); + + // 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); + $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); + $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); + $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); + } + + /** + * 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 $this->channelRepository->getChannelStats($channel); + } + + /** + * 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); + } +} \ No newline at end of file diff --git a/webUI/src/components/application/buttons/CalculatorButton.vue b/webUI/src/components/application/buttons/CalculatorButton.vue index db43199..d468c11 100755 --- a/webUI/src/components/application/buttons/CalculatorButton.vue +++ b/webUI/src/components/application/buttons/CalculatorButton.vue @@ -74,6 +74,12 @@ }, }, methods: { + openCalculator() { + this.dialog = true; + }, + closeCalculator() { + this.dialog = false; + }, handleButtonClick(btn) { this.handleInput(btn); this.activeButton = btn; diff --git a/webUI/src/components/application/buttons/CalculatorDialog.vue b/webUI/src/components/application/buttons/CalculatorDialog.vue new file mode 100644 index 0000000..d9ad05f --- /dev/null +++ b/webUI/src/components/application/buttons/CalculatorDialog.vue @@ -0,0 +1,214 @@ + + + + + \ No newline at end of file diff --git a/webUI/src/router/index.ts b/webUI/src/router/index.ts index f28339d..999dd0d 100755 --- a/webUI/src/router/index.ts +++ b/webUI/src/router/index.ts @@ -737,6 +737,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', diff --git a/webUI/src/stores/chatStore.ts b/webUI/src/stores/chatStore.ts new file mode 100644 index 0000000..09f35f1 --- /dev/null +++ b/webUI/src/stores/chatStore.ts @@ -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; + attachments: any[]; +} + +export const useChatStore = defineStore('chat', () => { + // State + const channels = ref([]); + const currentChannel = ref(null); + const messages = ref([]); + const loading = ref(false); + const error = ref(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 + }; +}); \ No newline at end of file diff --git a/webUI/src/views/acc/App.vue b/webUI/src/views/acc/App.vue index 38e4798..cb496bd 100755 --- a/webUI/src/views/acc/App.vue +++ b/webUI/src/views/acc/App.vue @@ -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 { {{ business.name }} - + - + + + + هوش مصنوعی + + + + گفت‌و‌گو + + + + ماشین حساب + + + - + @@ -1054,6 +1076,8 @@ export default { + +
diff --git a/webUI/src/views/acc/router/index.js b/webUI/src/views/acc/router/index.js deleted file mode 100755 index 43cd189..0000000 --- a/webUI/src/views/acc/router/index.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/webUI/src/views/chat/home.vue b/webUI/src/views/chat/home.vue new file mode 100644 index 0000000..9938aeb --- /dev/null +++ b/webUI/src/views/chat/home.vue @@ -0,0 +1,1653 @@ + + + + + +